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
