article

Thursday, April 24, 2025

Laravel 12 React CRUD with File Upload | React Starter kit | InertiaJS ShadCN/UI

Laravel 12 React CRUD with File Upload | React Starter kit | InertiaJS ShadCN/UI


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

Related Post