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