Download Laravel App
https://laravel.com/docs/12.x/installation
Connecting our Database
open .env file root directory.
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=8889
DB_DATABASE=laravel12dev
DB_USERNAME=root
DB_PASSWORD=root
Database Migration
php artisan migrate
myapp>php artisan migrate
Migration table created successfully.
check database table
Create tables Model php artisan make:model Product -m myapp>php artisan make:model Product -m Open new products migrations yourproject/database/migrations laravelproject\database\migrations\_create_products_table.php
//laravelproject\database\migrations\_create_products_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('price');
$table->text('description');
$table->string('photo');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('products');
}
};
run myapp>php artisan migrate
update Product Model
app/models/Product.php
//app/models/Product.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = [
'title',
'price',
'description',
'photo'
];
}
php artisan make:controller ProductController --resource
change it with the following codes: app\Http\Controllers\ProductController.php
//app\Http\Controllers\ProductController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Inertia;
use App\Models\Product;
use Illuminate\Support\Facades\Storage; //php artisan storage:link = php artisan storage:link = http://127.0.0.1:8000/storage/1.jpg
class ProductController extends Controller
{
public function index()
{
return Inertia::render('products/index', [
'products' => Product::all(),
]);
}
public function create()
{
return Inertia::render('products/create');
}
public function store(Request $request)
{
$data = $request->validate([
'title' => 'required',
'price' => 'required',
'description' => 'required',
'photo' => 'required|image',
]);
if ($request->hasFile('photo')) {
$data['photo'] = Storage::disk('public')->put('products', $request->file('photo'));
}
Product::create([
'title' => $request->title,
'price' => $request->price,
'description' => $request->description,
'photo' => $data['photo'],
]);
return to_route('products.index')->with('success', 'Product Successfully added');
}
public function edit(Product $product)
{
return Inertia::render('products/edit', [
'product' => $product,
]);
}
public function update(Request $request, Product $product)
{
$data = $request->validate([
'title' => 'required',
'price' => 'required',
'description' => 'required',
'photo' => 'required|image',
]);
if ($request->hasFile('photo')) {
Storage::disk('public')->delete($product->photo);
$data['photo'] = Storage::disk('public')->put('products', $request->file('photo'));
}
$product->update([
'title' => $request->title,
'price' => $request->price,
'description' => $request->description,
'photo' => $data['photo'],
]);
return to_route('products.index')->with('success', 'Product Successfully Updated');
}
public function destroy(Product $product)
{
Storage::disk('public')->delete($product->photo);
$product->delete();
return to_route('products.index')->with('success', 'Product Successfully Deleted');
}
}
Frontend with React and InertiaJS Add sidebar menu product mainNavItems from reactjs\rescourses\js\components\app-sidebar.tsx
File: products/index.tsx
reactjs\resources\js\pages\products\index.tsx
//reactjs\resources\js\pages\products\index.tsx
import AppLayout from '@/layouts/app-layout';
import { type BreadcrumbItem } from '@/types';
import { Head, Link, usePage } from '@inertiajs/react';
import { useEffect } from 'react';
import { toast } from "sonner"
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Product',
href: '/products',
},
];
interface Flash {
success?: string;
error?: string;
}
export default function Products({ products }) {
const { flash } = usePage<{ flash: Flash }>().props;
useEffect(() => {
if (flash.success) {
toast.success(flash.success);
}
}, [flash.success]);
return (
<AppLayout breadcrumbs={breadcrumbs}>
<Head title="Product" />
<div className="container mx-auto p-4">
<div className="flex justify-between items-center mb-4">
<h1 className="text-2xl font-bold">Products</h1>
<button className="bg-gray-500 text-white px-4 py-1 rounded hover:bg-gray-600">
<Link href="/products/create">Create New Product</Link>
</button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full bg-white shadow rounded-lg">
<thead>
<tr className="bg-gray-200 text-black">
<th className="py-2 px-4 text-left border-b">ID</th>
<th className="py-2 px-4 text-left border-b">Title</th>
<th className="py-2 px-4 text-left border-b">Photo</th>
<th className="py-2 px-4 text-left border-b">Price</th>
<th className="py-2 px-4 text-left border-b">Description</th>
<th className="py-2 px-4 text-left border-b">Actions</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr className="hover:bg-gray-50 text-black" key={product.id}>
<td className="py-2 px-4 border-b">{product.id}</td>
<td className="py-2 px-4 border-b">{product.title}</td>
<td className="py-2 px-4 border-b">
<img src={`http://127.0.0.1:8000/storage/${product.photo}`} alt="" height={50} width={90} />
</td>
<td className="py-2 px-4 border-b">{product.price}</td>
<td className="py-2 px-4 border-b">{product.description}</td>
<td className="py-2 px-4 border-b">
<button className="bg-green-500 text-white px-2 py-1 rounded hover:bg-green-600 mr-2">
<Link href={`/products/${product.id}/edit`}>Edit</Link>
</button>
<Link
className="bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600"
method="delete"
onClick={(e) => {
if (!confirm('Are you sure?')) {
e.preventDefault();
}
}}
href={route('products.destroy', product.id)}
>
Delete
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</AppLayout>
);
}
inertiajs Flash messages inertiajs com/shared-data#flash-messages add flash to reactjs\app\http\Middleware\HandleInertiaRequests.php
public function share(Request $request): array
{
return [
'flash' =>[
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error')
],
];
}
add flash to app layout = reactjs\resources\js\layout\app-layout.tsx
import { Toaster } from "@/components/ui/sonner" //npx shadcn@latest add sonner
{children}
<Toaster />
File: products/create.tsx reactjs\resources\js\pages\products\create.tsx
//reactjs\resources\js\pages\products\create.tsx
import AppLayout from '@/layouts/app-layout';
import { type BreadcrumbItem } from '@/types';
import { Head, useForm } from '@inertiajs/react';
import { LoaderCircle } from 'lucide-react';
import { FormEventHandler } from 'react';
import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from "@/components/ui/textarea"
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Create Product',
href: '/products/create',
},
];
type CreateForm = {
title: string;
price: string;
description: string;
photo: null;
};
interface CreateProps {
status?: string;
}
export default function ProductCreate({ status }: CreateProps) {
const { data, setData, post, processing, errors } = useForm<Required<CreateForm>>({
title: '',
price: '',
description: '',
photo: '',
});
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setData('photo', file);
}
}
const submit: FormEventHandler = (e) => {
e.preventDefault();
post(route('products.store'));
};
return (
<AppLayout breadcrumbs={breadcrumbs}>
<Head title="Create Product" />
<div className="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
<div className="flex justify-end">Create New Product</div>
<div className="p-10 border-sidebar-border/70 dark:border-sidebar-border relative min-h-[100vh] flex-1 overflow-hidden rounded-xl border md:min-h-min">
<form className="flex flex-col gap-6" onSubmit={submit}>
<div className="grid gap-6">
<div className="grid gap-2">
<Label htmlFor="title">Title</Label>
<Input
id="title"
type="text"
autoFocus
tabIndex={1}
value={data.title}
onChange={(e) => setData('title', e.target.value)}
placeholder="Title"
/>
<InputError message={errors.title} />
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="price">Price</Label>
</div>
<Input
id="price"
type="text"
tabIndex={2}
value={data.price}
onChange={(e) => setData('price', e.target.value)}
placeholder="Price"
/>
<InputError message={errors.price} />
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="description">Description</Label>
</div>
<Textarea
id="description"
placeholder="Description"
value={data.description}
onChange={(e) => setData('description', e.target.value)}
/>
<InputError message={errors.description} />
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="photo">Photo</Label>
</div>
<Input
id="photo"
type="file"
placeholder="Description"
onChange={handleFileChange}
/>
<InputError message={errors.photo} />
</div>
<Button type="submit" className="mt-4 w-full" tabIndex={4} disabled={processing}>
{processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
Submit
</Button>
</div>
</form>
{status && <div className="mb-4 text-center text-sm font-medium text-green-600">{status}</div>}
</div>
</div>
</AppLayout>
);
}
File: products/edit.tsx
reactjs\resources\js\pages\products\edit.tsx
//reactjs\resources\js\pages\products\edit.tsx
import AppLayout from '@/layouts/app-layout';
import { type BreadcrumbItem } from '@/types';
import { Head, usePage, router } from '@inertiajs/react';
import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from "@/components/ui/textarea"
import { useState } from 'react';
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Products',
href: '/products',
},
];
export default function Products({ product }) {
//console.log(product);
const [title, setTitle] = useState<string>(product.title);
const [price, setPrice] = useState<string>(product.price);
const [description, setDescription] = useState<string>(product.description);
const [photo, setPhoto] = useState<File | null>(product.photo);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const { errors } = usePage().props;
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setPhoto(file); //console.log(file.name);
setImagePreview(URL.createObjectURL(file));
}
}
function submit(e: React.FormEvent) {
e.preventDefault();
router.post(route('products.update', product.id), {
_method: 'put',
title,
price,
description,
photo,
});
};
return (
<AppLayout breadcrumbs={breadcrumbs}>
<Head title="Edit Product" />
<div className="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
<div className="flex justify-end">Edit Product</div>
<div className="p-10 border-sidebar-border/70 dark:border-sidebar-border relative min-h-[100vh] flex-1 overflow-hidden rounded-xl border md:min-h-min">
<form className="flex flex-col gap-6" onSubmit={submit}>
<div className="grid gap-6">
<div className="grid gap-2">
<Label htmlFor="title">Title</Label>
<Input
id="title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
/>
<InputError message={errors.title} />
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="price">Price</Label>
</div>
<Input
id="price"
type="text"
value={price}
onChange={(e) => setPrice(e.target.value)}
placeholder="Price"
/>
<InputError message={errors.price} />
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="description">Description</Label>
</div>
<Textarea
id="description"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<InputError message={errors.description} />
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="photo">Photo</Label>
</div>
<Input
id="photo"
type="file"
placeholder="Description"
onChange={handleFileChange}
/>
<InputError message={errors.photo} />
<img src={`http://127.0.0.1:8000/storage/${product.photo}`} alt="" height={50} width={90} />
{imagePreview && <img src={imagePreview} alt="Preview" height={350} width={390}/>}
</div>
<Button type="submit" className="mt-4 w-full" tabIndex={4}>
Update
</Button>
</div>
</form>
</div>
</div>
</AppLayout>
);
}
Routes routes/web.php
//routes/web.php
use App\Http\Controllers\ProductController;
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('dashboard', function () {
return Inertia::render('dashboard');
})->name('dashboard');
Route::resource('products', ProductController::class);
});
Run php artisan serve and npm run dev myapp>composer run dev Starting Laravel development server: http://127.0.0.1:8000
