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=laravel12DB
DB_USERNAME=root
DB_PASSWORD=root
Database Migration
php artisan migrate
myapp>php artisan migrate
Migration table created successfully.
check database table
php artisan make:controller ProductController 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\Str; class ProductController extends Controller { public function create() { return Inertia::render('admin/products/create'); } public function store(Request $request) { $request->validate([ 'name' => 'string|required|max:255', 'description' => 'string|nullable', 'images' => 'nullable|array', 'images.*' => 'image|max:2048', ]); // Slug $slug = Str::slug($request->name); // Image $images = []; if ($request->hasFile('images')) { foreach ($request->file('images') as $image) { $images[] = $image->store('products/images', 'public'); } } $new_product = [ 'name' => $request->name, 'slug' => $slug, 'description' => $request->description, 'price' => $request->price, 'images' => $images, ]; $prod = Product::create($new_product); //dd($prod); return redirect()->route('dashboard.products.index')->with('success', 'Product created successfully!'); } }Create tables Product 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('name'); $table->string('slug')->unique(); $table->decimal('price', 10, 2)->default(0); $table->text('description')->nullable(); $table->json('images')->nullable(); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('products'); } };myapp>php artisan migrate
Migration table created successfully.
check database table
update Product Model
app/models/Product.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Product extends Model { protected $fillable = [ 'name', 'slug', 'price', 'description', 'images', ]; protected $casts = [ 'images' => 'array', 'price' => 'decimal:2', ]; }Frontend with React and InertiaJS
Add sidebar menu product mainNavItems from reactjs\rescourses\js\components\app-sidebar.tsx
File: pages\admin\products\create.tsx
reactjs\resources\js\pages\users\create.tsx
//reactjs\resources\js\pages\admin\products\create.tsx import AppLayout from '@/layouts/app-layout'; import { type BreadcrumbItem } from '@/types'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Textarea } from "@/components/ui/textarea" import { Head, router, useForm } from '@inertiajs/react'; import { CompactFileInput } from '@/components/ImageUploadInput'; import { useState } from 'react'; import { LoaderCircle } from 'lucide-react'; import InputError from '@/components/input-error'; import { CreateProductItem } from '@/types/products'; const breadcrumbs: BreadcrumbItem[] = [ { title: 'Create Product', href: '/dashboard/products/create', }, ]; export default function Dashboard() { const [images, setImages] = useState<File[]>([]); const { data, setData, processing, errors, reset } = useForm<Required<CreateProductItem>>({ name: '', slug: '', image: null, description: '', price: 0, images: null, }); const handleSubmit: React.FormEventHandler = (e) => { e.preventDefault(); data.images = images; console.log(data.images); router.post('/dashboard/products', data, { onFinish: () => { reset(); console.log("success"); }, }); }; 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 onSubmit={handleSubmit} className="space-y-4"> <div className="grid gap-6"> <div className="grid gap-2"> <Label htmlFor="title">Product Name</Label> <Input id="name" value={data.name} onChange={(e) => setData('name', e.target.value)} /> <InputError message={errors.name} className="mt-2" /> </div> </div> <div className="grid gap-6"> <div className="grid gap-2"> <Label htmlFor="price">Price</Label> <Input id="price" value={data.price} onChange={(e) => setData('price', Number(e.target.value))} /> <InputError message={errors.price} className="mt-2" /> </div> </div> <div className="grid gap-2"> <div className="flex items-center"> <Label htmlFor="message">Product Description</Label> </div> <Textarea value={data.description} onChange={(e) => setData('description', e.target.value)} placeholder="Type your description here." id="message" /> <InputError message={errors.description} /> </div> <div className="grid gap-2"> <h2 className="mb-3 text-lg font-semibold">Upload product Images</h2> <div className="rounded border p-4"> <CompactFileInput multiple={true} maxSizeMB={1} onChange={setImages} /> </div> </div> <Button type="submit" className="mt-4 w-full" disabled={processing}> {processing && <LoaderCircle className="h-4 w-4 animate-spin" />} Add Product </Button> </form> </div> </div> </AppLayout> ); }File: js\components\ImageUploadInput.tsx
reactjs\resources\js\components\ImageUploadInput.tsx
//reactjs\resources\js\components\ImageUploadInput.tsx 'use client'; import React, { useState } from 'react'; // Shared types type ImageFile = { id: string; file: File; preview: string; }; type FileInputProps = { multiple?: boolean; maxSizeMB?: number; onChange?: (files: File[]) => void; acceptedFileTypes?: string; }; // Compact File Input Component export const CompactFileInput: React.FC<FileInputProps> = ({ multiple = false, maxSizeMB = 1, onChange, acceptedFileTypes = 'image/*' }) => { const [images, setImages] = useState<ImageFile[]>([]); const [error, setError] = useState<string | null>(null); const fileInputRef = React.useRef<HTMLInputElement>(null); const maxSizeBytes = maxSizeMB * 1024 * 1024; // Convert MB to bytes const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const selectedFiles = Array.from(e.target.files || []); console.log(selectedFiles); // Reset error setError(null); // Validate file size const oversizedFiles = selectedFiles.filter((file) => file.size > maxSizeBytes); if (oversizedFiles.length > 0) { setError(`File(s) exceed the ${maxSizeMB}MB limit`); return; } // Handle single file mode if (!multiple && selectedFiles.length > 0) { // Remove previous image and URL images.forEach((img) => URL.revokeObjectURL(img.preview)); const file = selectedFiles[0]; const newImage = { id: Math.random().toString(36).substr(2, 9), file, preview: URL.createObjectURL(file), }; setImages([newImage]); onChange?.([file]); return; } // Handle multiple files const newImages = selectedFiles.map((file) => ({ id: Math.random().toString(36).substr(2, 9), file, preview: URL.createObjectURL(file), })); setImages((prev) => [...prev, ...newImages]); onChange?.(selectedFiles); }; const removeImage = (id: string) => { setImages((prev) => { const updatedImages = prev.filter((img) => { if (img.id === id) { URL.revokeObjectURL(img.preview); return false; } return true; }); // Notify parent component onChange?.(updatedImages.map((img) => img.file)); return updatedImages; }); }; const handleBrowseClick = () => { fileInputRef.current?.click(); }; return ( <div className="compact-file-input"> <div className="input-container flex items-center gap-2 rounded border border-gray-300 p-2"> <button type="button" onClick={handleBrowseClick} className="rounded bg-blue-500 px-3 py-1 text-sm text-white hover:bg-blue-600"> Browse </button> <span className="text-sm text-gray-500">{multiple ? 'Choose images' : 'Choose an image'}</span> <input ref={fileInputRef} type="file" accept={acceptedFileTypes} multiple={multiple} onChange={handleFileChange} className="hidden" /> </div> {error && <div className="mt-1 text-xs text-red-500">{error}</div>} {images.length > 0 && ( <div className="image-previews mt-2 flex flex-wrap gap-2"> {images.map((img) => ( <div key={img.id} className="preview-container relative h-16 w-16 overflow-hidden rounded border"> <img src={img.preview} alt="Preview" className="h-full w-full object-cover" /> <button type="button" onClick={() => removeImage(img.id)} className="absolute top-0 right-0 flex h-5 w-5 items-center justify-center rounded-full bg-red-500 text-xs text-white" > × </button> </div> ))} </div> )} </div> ); };File: js\components\ui\textarea.tsx
reactjs\resources\js\components\ui\textarea.tsx
//reactjs\resources\js\components\ui\textarea.tsx import * as React from "react" import { cn } from "@/lib/utils" function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { return ( <textarea data-slot="textarea" className={cn( "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", className )} {...props} /> ) } export { Textarea }File: js\lib\utils.ts
reactjs\resources\js\lib\utils.ts
import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }File: js\types\products.ts
reactjs\resources\js\types\products.ts
export interface ProductItem { id: number; name: string; slug: string; price: number; description: string; images: string | null; }
//routes/web.php use Illuminate\Support\Facades\Route; use Inertia\Inertia; use App\Http\Controllers\ProductController; Route::get('/', function () { return Inertia::render('welcome'); })->name('home'); Route::middleware(['auth', 'verified'])->group(function () { Route::get('dashboard', function () { return Inertia::render('dashboard'); })->name('dashboard'); Route::get('/dashboard/products/create', [ProductController::class, 'create'])->name('dashboard.products.create'); Route::post('/dashboard/products', [ProductController::class, 'store'])->name('dashboard.products.store'); }); require __DIR__.'/settings.php'; require __DIR__.'/auth.php';Run php artisan serve and npm run dev myapp>composer run dev
Starting Laravel development server: http://127.0.0.1:8000