article

Monday, September 8, 2025

Laravel 12 Multiple Upload Fle React | React Starter Kit

Laravel 12 Multiple Upload Fle React | React Starter Kit

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

Related Post