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 Post Model php artisan make:model Post -m myapp>php artisan make:model Post -m Open new products migrations yourproject/database/migrations laravelproject\database\migrations\_create_posts_table.php
//laravelproject\database\migrations\_create_posts_table.php <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('content'); $table->integer('categories_id'); $table->boolean('poststatus')->default(false); $table->string('slug'); $table->string('picture')->nullable(); // Add picture column (nullable) $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('posts'); } };Create tables Category Model php artisan make:model Category -m myapp>php artisan make:model Category -m Open new products migrations yourproject/database/migrations laravelproject\database\migrations\_create_categories_table.php
//laravelproject\database\migrations\_create_categories_table.php return new class extends Migration { public function up(): void { Schema::create('categories', function (Blueprint $table) { $table->id(); $table->string('title'); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('categories'); } };run
myapp>php artisan migrate
update Post Model
app/models/Post.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Post extends Model { use HasFactory; protected $fillable = [ 'title', 'content', 'poststatus', 'slug', 'picture', 'categories_id' ]; protected $casts = [ 'poststatus' => 'boolean' ]; public function categories(): BelongsTo { return $this->belongsTo(Category::class, 'categories_id'); } }update Category Model
app/models/Category.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Category extends Model { use HasFactory; protected $fillable = [ 'title', 'user_id' ]; public function user(): BelongsTo { return $this->belongsTo(User::class); } }Create PostController
php artisan make:controller PostController
change it with the following codes:
app\Http\Controllers\PostController.php
//app\Http\Controllers\PostController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Inertia\Inertia; use App\Models\Category; use App\Models\Post; use Illuminate\Support\Str; use Illuminate\Support\Facades\Storage; class PostController extends Controller { public function index() { $query = Post::with('categories') ->whereHas('categories', function ($query) { $query->where('user_id', auth()->id()); }) ->orderBy('created_at', 'desc'); // Handle search if (request()->has('search')) { $search = request('search'); $query->where(function ($q) use ($search) { $q->where('title', 'like', "%{$search}%") ->orWhere('content', 'like', "%{$search}%"); }); } // Handle filter if (request()->has('filter') && request('filter') !== 'all') { $query->where('poststatus', request('filter') === 'published'); } $posts = $query->paginate(2); $categories = Category::where('user_id', auth()->id())->get(); return Inertia::render('posts/index', [ 'posts' => $posts, 'categories' => $categories, 'filters' => [ 'search' => request('search', ''), 'filter' => request('filter', 'all'), ], 'flash' => [ 'success' => session('success'), 'error' => session('error') ] ]); } public function create() { $categories = Category::where('user_id', auth()->id())->get(); return Inertia::render('posts/create', [ 'categories' => $categories, 'flash' => [ 'success' => session('success'), 'error' => session('error') ] ]); } public function store(Request $request) { $validated = $request->validate([ 'title' => 'required|string|max:255', 'content' => 'required|string', 'categories_id' => 'required|exists:categories,id', 'poststatus' => 'boolean', 'picture' => 'required|image', ]); $title = $request->input('title'); $validated['slug'] = Str::slug($title); if ($request->hasFile('picture')) { $validated['picture'] = Storage::disk('public')->put('posts', $request->file('picture')); } Post::create($validated); return redirect()->route('posts.index')->with('success', 'Post created successfully!'); } public function edit(Post $post) { $categories = Category::where('user_id', auth()->id())->get(); return Inertia::render('posts/edit', [ 'post' => $post, 'categories' => $categories, ]); } public function update(Request $request, Post $post) { $data = $request->validate([ 'title' => 'required|string|max:255', 'content' => 'required|string', 'categories_id' => 'required|exists:categories,id', 'poststatus' => 'boolean', 'picture' => 'required|image', ]); $title = $request->input('title'); $data['slug'] = Str::slug($title); if ($request->hasFile('picture')) { Storage::disk('public')->delete($post->picture); $data['picture'] = Storage::disk('public')->put('posts', $request->file('picture')); } $post->update($data); return redirect()->route('posts.index')->with('success', 'Post updated successfully!'); } public function destroy(Post $post) { Storage::disk('public')->delete($post->picture); $post->delete(); return redirect()->route('posts.index')->with('success', 'Post deleted successfully!'); } }Create CategoryController
php artisan make:controller CategoryController
change it with the following codes:
app\Http\Controllers\CategoryController.php
//app\Http\Controllers\CategoryController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Inertia\Inertia; use App\Models\Category; class CategoryController extends Controller { public function index() { $categories = Category::where('user_id', auth()->id()) ->get(); return Inertia::render('category/index', [ 'categories' => $categories, 'flash' => [ 'success' => session('success'), 'error' => session('error') ] ]); } public function store(Request $request) { $validated = $request->validate([ 'title' => 'required|string|max:255' ]); Category::create([ ...$validated, 'user_id' => auth()->id() ]); return redirect()->route('category.index')->with('success', 'Category created successfully!'); } public function update(Request $request, Category $category) { $validated = $request->validate([ 'title' => 'required|string|max:255' ]); $category->update($validated); return redirect()->route('category.index')->with('success', 'Categroy updated successfully! ' . $validated['title'] . ' '); } public function destroy(Category $category) { $category->delete(); return redirect()->route('category.index')->with('success', 'Category deleted successfully!'); } }Frontend with React and InertiaJS
Add sidebar menu product mainNavItems from reactjs\rescourses\js\components\app-sidebar.tsx
Index.tsx File: pages\posts\index.tsx
reactjs\resources\js\pages\posts\index.tsx
//reactjs\resources\js\pages\posts\index.tsx import AppLayout from '@/layouts/app-layout'; import { type BreadcrumbItem } from '@/types'; import { Head, router } from '@inertiajs/react'; import { Button } from '@/components/ui/button'; import { Plus, CheckCircle2, XCircle, List, CheckCircle, Search, ChevronLeft, ChevronRight } from 'lucide-react'; import { Link } from '@inertiajs/react'; import { useState, useEffect } from 'react'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; interface Post { id: number; title: string; content: string | null; poststatus: boolean; slug: string; picture: string; categories_id: number; categories: { id: number; title: string; }; } interface Category { id: number; title: string; } interface Props { posts: { data: Post[]; current_page: number; last_page: number; per_page: number; total: number; from: number; to: number; }; categories: Category[]; filters: { search: string; filter: string; }; flash?: { success?: string; error?: string; }; } const breadcrumbs: BreadcrumbItem[] = [ { title: 'Posts', href: '/admin/posts', }, ]; export default function PostsIndex({ posts, categories, filters, flash }: Props) { console.log(posts); //console.log(categories); const [showToast, setShowToast] = useState(false); const [toastMessage, setToastMessage] = useState(''); const [toastType, setToastType] = useState<'success' | 'error'>('success'); const [searchTerm, setSearchTerm] = useState(filters.search); const [completionFilter, setCompletionFilter] = useState<'all' | 'published' | 'draft'>(filters.filter as 'all' | 'published' | 'draft'); useEffect(() => { if (flash?.success) { setToastMessage(flash.success); setToastType('success'); setShowToast(true); } else if (flash?.error) { setToastMessage(flash.error); setToastType('error'); setShowToast(true); } }, [flash]); useEffect(() => { if (showToast) { const timer = setTimeout(() => { setShowToast(false); }, 3000); return () => clearTimeout(timer); } }, [showToast]); const handleDelete = (postId: number) => { console.log("deleted" + postId); router.delete(`/admin/posts/${postId}`, { onSuccess: () => { router.reload(); }, onError: () => { console.error("Failed to delete post."); }, }); }; const handleSearch = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); router.get(route('posts.index'), { search: searchTerm, filter: completionFilter, }, { preserveState: true, preserveScroll: true, }); }; const handleFilterChange = (value: 'all' | 'published' | 'draft') => { setCompletionFilter(value); router.get(route('posts.index'), { search: searchTerm, filter: value, }, { preserveState: true, preserveScroll: true, }); }; const handlePageChange = (page: number) => { router.get(route('posts.index'), { page, search: searchTerm, filter: completionFilter, }, { preserveState: true, preserveScroll: true, }); }; return ( <AppLayout breadcrumbs={breadcrumbs}> <Head title="Posts" /> <div className="flex h-full flex-1 flex-col gap-6 rounded-xl p-6 bg-gradient-to-br from-background to-muted/20"> {showToast && ( <div className={`fixed top-4 right-4 z-50 flex items-center gap-2 rounded-lg p-4 shadow-lg ${toastType === 'success' ? 'bg-green-500' : 'bg-red-500' } text-white animate-in fade-in slide-in-from-top-5`}> {toastType === 'success' ? ( <CheckCircle2 className="h-5 w-5" /> ) : ( <XCircle className="h-5 w-5" /> )} <span>{toastMessage}</span> </div>) } <div className="flex justify-between items-center"> <div> <h1 className="text-3xl font-bold tracking-tight">Posts</h1> <p className="text-muted-foreground mt-1">Manage your posts</p> </div> <Button variant="outline" className="bg-primary hover:bg-primary/90 text-black shadow-lg"> <Plus className="h-4 w-4 mr-2" /> <Link href="/admin/posts/create">New Post</Link> </Button> </div> <div className="flex gap-4 mb-4"> <form onSubmit={handleSearch} className="relative flex-1"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Input placeholder="Search post..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="pl-10" /> </form> <Select value={completionFilter} onValueChange={handleFilterChange} > <SelectTrigger className="w-[180px]"> <SelectValue placeholder="Filter by status" /> </SelectTrigger> <SelectContent> <SelectItem value="all">All Posts</SelectItem> <SelectItem value="published">Published</SelectItem> <SelectItem value="draft">Draft</SelectItem> </SelectContent> </Select> </div> <div className="rounded-md border"> <div className="relative w-full overflow-auto"> <table className="w-full caption-bottom text-sm"> <thead className="[&_tr]:border-b"> <tr className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"> <th className="h-12 px-4 text-left align-middle font-medium text-muted-foreground">Photo</th> <th className="h-12 px-4 text-left align-middle font-medium text-muted-foreground">Title</th> <th className="h-12 px-4 text-left align-middle font-medium text-muted-foreground">Content</th> <th className="h-12 px-4 text-left align-middle font-medium text-muted-foreground">Category</th> <th className="h-12 px-4 text-left align-middle font-medium text-muted-foreground">Status</th> <th className="h-12 px-4 text-right align-middle font-medium text-muted-foreground">Actions</th> </tr> </thead> <tbody className="[&_tr:last-child]:border-0"> {posts.data.map((post) => ( <tr key={post.id} className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"> <td className="p-4 align-middle font-medium"> {post.picture ? <img src={`http://127.0.0.1:8000/storage/${post.picture}`} alt="" height={50} width={90} /> : "No Picture"} </td> <td className="p-4 align-middle font-medium">{post.title}</td> <td className="p-4 align-middle max-w-[200px] truncate"> {post.content || 'No content'} </td> <td className="p-4 align-middle"> <div className="flex items-center gap-2"> <List className="h-4 w-4 text-muted-foreground" /> {post.categories.title} </div> </td> <td className="p-4 align-middle"> {post.poststatus ? ( <div className="flex items-center gap-2 text-green-500"> <CheckCircle className="h-4 w-4" /> <span>Published</span> </div> ) : ( <div className="flex items-center gap-2 text-yellow-500"> <span>Draft</span> </div>)} </td> <td className="p-4 align-middle text-right"> <div className="flex justify-end gap-2"> <Button className="bg-green-500 text-white px-2 py-1 rounded hover:bg-green-600 mr-2"> <Link href={`/admin/posts/${post.id}/edit`}>Edit Post</Link> </Button> <Button onClick={() => handleDelete(post.id)} className="bg-red-500 text-sm text-white px-3 py-2 rounded" > Delete </Button> </div> </td> </tr> ))} {posts.data.length === 0 && ( <tr> <td colSpan={6} className="p-4 text-center text-muted-foreground"> No record </td> </tr> )} </tbody> </table> </div> </div> {/* Pagination */} <div className="flex items-center justify-between px-2"> <div className="text-sm text-muted-foreground"> Showing {posts.from} to {posts.to} of {posts.total} results </div> <div className="flex items-center space-x-2"> <Button variant="outline" size="icon" onClick={() => handlePageChange(posts.current_page - 1)} disabled={posts.current_page === 1} > <ChevronLeft className="h-4 w-4" /> </Button> <div className="flex items-center space-x-1"> {Array.from({ length: posts.last_page }, (_, i) => i + 1).map((page) => ( <Button key={page} variant={page === posts.current_page ? "default" : "outline"} size="icon" onClick={() => handlePageChange(page)} > {page} </Button>))} </div> <Button variant="outline" size="icon" onClick={() => handlePageChange(posts.current_page + 1)} disabled={posts.current_page === posts.last_page} > <ChevronRight className="h-4 w-4" /> </Button> </div> </div> </div> </AppLayout> ); }create.tsx File: pages\posts\create.tsx
reactjs\resources\js\pages\posts\create.tsx
//reactjs\resources\js\pages\posts\create.tsx import AppLayout from '@/layouts/app-layout'; import { type BreadcrumbItem } from '@/types'; import { Head, useForm } from '@inertiajs/react'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Textarea } from "@/components/ui/textarea" import { Button } from '@/components/ui/button'; import { FormEventHandler, useState } from 'react'; import InputError from '@/components/input-error'; import { LoaderCircle } from 'lucide-react'; const breadcrumbs: BreadcrumbItem[] = [ { title: 'Create Post', href: '/admin/posts/create', }, ]; type CreateForm = { title: string; content: string; poststatus: boolean; picture: string; categories_id: number; categories: { id: number; title: string; }; }; interface Category { id: number; title: string; } interface CreateProps { categories: Category[]; status?: string; } export default function ProductCreate({ status, categories }: CreateProps) { //console.log(categories); const { data, setData, post, processing, errors } = useForm<Required<CreateForm>>({ title: '', content: '', poststatus: Boolean, picture: '', }); const [preview, setPreview] = useState<string>(""); const submit: FormEventHandler = (e) => { e.preventDefault(); console.log(data); post(route('posts.store')); }; const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.files && e.target.files[0]) { const file = e.target.files[0]; setData('picture', file); setPreview(URL.createObjectURL(file)); } }; const [isChecked, setIsChecked] = useState(false) const checkHandler = () => { setIsChecked(!isChecked) console.log(!isChecked); setData('poststatus', !isChecked) } 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 Post</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> <div className="grid gap-2"> <div className="flex items-center"> <Label htmlFor="content">Content</Label> </div> <Textarea id="content" placeholder="Content" tabIndex={2} value={data.content} onChange={(e) => setData('content', e.target.value)} /> <InputError message={errors.content} /> </div> <div className="grid gap-2"> <div className="flex items-center"> <Label htmlFor="category">Category</Label> </div> <select id="categories_id" className="block w-full p-2 mb-6 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" onChange={(e) => setData('categories_id', e.target.value)} > <option>Select a category</option> {categories.map((list) => ( <option key={list.id} value={list.id.toString()}>{list.title}</option> ))} </select> <InputError message={errors.categories_id} /> </div> <div className="grid gap-2"> <div className="flex items-center"> <Label htmlFor="picture">Picture</Label> </div> <Input id="picture" type="file" placeholder="Picture" onChange={handleFileChange} /> {preview && ( <div className="mb-3"> <p className="text-sm mb-1">Image Preview:</p> <img src={preview} alt="Preview" className="w-32 h-32 object-cover rounded" /> </div> )} <InputError message={errors.picture} /> </div> <div className="grid gap-2"> <input type="checkbox" id="poststatus" checked={isChecked} onChange={checkHandler} className="h-4 w-4 rounded border-gray-300 focus:ring-2 focus:ring-primary" /> <Label htmlFor="poststatus">Published</Label> </div> <Button type="submit" className="mt-4 w-full" tabIndex={4} disabled={processing}> {processing && <LoaderCircle className="h-4 w-4 animate-spin" />} Submit </Button> </form> {status && <div className="mb-4 text-center text-sm font-medium text-green-600">{status}</div>} </div> </div> </AppLayout> ); }edit.tsx File: pages\posts\edit.tsx
reactjs\resources\js\pages\posts\edit.tsx
//reactjs\resources\js\pages\posts\edit.tsx import AppLayout from '@/layouts/app-layout'; import { type BreadcrumbItem } from '@/types'; import { Head, usePage, router } from '@inertiajs/react'; 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'; import InputError from '@/components/input-error'; const breadcrumbs: BreadcrumbItem[] = [ { title: 'Posts', href: '/admin/posts/edit', }, ]; interface Category { id: number; title: string; } interface Props { categories: Category[]; } export default function EditPosts({ post, categories }: Props) { //console.log(categories); const [title, setTitle] = useState<string>(post.title); const [content, setContent] = useState<string>(post.content); const [poststatus, setPoststatus] = useState<boolean>(post.poststatus); const [categories_id, setCategory] = useState(post.categories_id); const [picture, setPicture] = useState<File | null>(post.picture); 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) { setPicture(file); //console.log(file.name); setImagePreview(URL.createObjectURL(file)); } } function submit(e: React.FormEvent) { e.preventDefault(); //console.log(title); router.post(route('posts.update', post.id), { _method: 'put', title, content, isChecked, poststatus, categories_id, picture, }); }; const [isChecked, setIsChecked] = useState(false) const checkHandler = () => { setIsChecked(!isChecked) console.log(!isChecked); setPoststatus(!isChecked) } return ( <AppLayout breadcrumbs={breadcrumbs}> <Head title="Edit Post" /> <div className="flex h-full flex-1 flex-col gap-4 rounded-xl p-4"> <div className="flex justify-end">Edit Post</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> <div className="grid gap-2"> <div className="flex items-center"> <Label htmlFor="content">Content</Label> </div> <Textarea id="content" placeholder="content" value={content} onChange={(e) => setContent(e.target.value)} /> <InputError message={errors.content} /> </div> <div className="grid gap-2"> <div className="flex items-center"> <Label htmlFor="category">Category</Label> </div> <select id="categories_id" className="block w-full p-2 mb-6 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" value={categories_id} onChange={e => setCategory(e.target.value)} > {categories.map((list) => ( <option key={list.id} value={list.id.toString()}>{list.title}</option> ))} </select> <InputError message={errors.categories_id} /> </div> <div className="grid gap-2"> <div className="flex items-center"> <Label htmlFor="picture">Picture</Label> </div> <Input id="picture" type="file" placeholder="Picture" onChange={handleFileChange} /> <InputError message={errors.picture} /> <img src={`http://127.0.0.1:8000/storage/${post.picture}`} alt="" height={50} width={90} /> {imagePreview && <img src={imagePreview} alt="Preview" height={350} width={390}/>} </div> <div className="grid gap-2"> <input type="checkbox" id="poststatus" checked={poststatus} onChange={checkHandler} className="h-4 w-4 rounded border-gray-300 focus:ring-2 focus:ring-primary" /> <Label htmlFor="poststatus">Published</Label> </div> <Button type="submit" className="mt-4 w-full" tabIndex={4}> Update </Button> </form> </div> </div> </AppLayout> ); }category/index.tsx File: pages\category\index.tsx
reactjs\resources\js\pages\category\index.tsx
//reactjs\resources\js\pages\category\index.tsx import AppLayout from '@/layouts/app-layout'; import { type BreadcrumbItem } from '@/types'; import { Head, router } from '@inertiajs/react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Plus, Pencil, Trash2, CheckCircle2, XCircle } from 'lucide-react'; import { DialogDescription } from '@radix-ui/react-dialog'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { useForm } from '@inertiajs/react'; import { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; interface Category { id: number; title: string; posts_count?: number; } interface Props { categories: Category[]; flash?: { success?: string; error?: string; }; } const breadcrumbs: BreadcrumbItem[] = [ { title: 'Categories', href: '/category', }, ]; export default function CategoryIndex({ categories, flash }: Props) { console.log(categories); const [isOpen, setIsOpen] = useState(false); const [showToast, setShowToast] = useState(false); const [toastMessage, setToastMessage] = useState(''); const [toastType, setToastType] = useState<'success' | 'error'>('success'); const [editingCategory, setEditingCategory] = useState<Category | null>(null); useEffect(() => { if (flash?.success) { setToastMessage(flash.success); setToastType('success'); setShowToast(true); } else if (flash?.error) { setToastMessage(flash.error); setToastType('error'); setShowToast(true); } }, [flash]); useEffect(() => { if (showToast) { const timer = setTimeout(() => { setShowToast(false); }, 3000); return () => clearTimeout(timer); } }, [showToast]); const { data, setData, post, put, processing, reset } = useForm({ title: '' }); const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (editingCategory) { console.log("updated"); put(route('category.update', editingCategory.id), { onSuccess: () => { setIsOpen(false); reset(); setEditingCategory(null); }, }); } else { post(route('category.store'), { onSuccess: () => { setIsOpen(false); reset(); }, }); } }; const handleEdit = (categories: Category) => { setEditingCategory(categories); setData({ title: categories.title, }); setIsOpen(true); }; const handleDelete = (id: number) => { router.delete(`/admin/category/${id}`, { onSuccess: () => { router.reload(); console.log("success"); }, onError: () => { console.error("Failed to delete post."); }, }); }; return ( <AppLayout breadcrumbs={breadcrumbs}> <Head title="Category" /> <div className="flex h-full flex-1 flex-col gap-4 rounded-xl p-4"> {showToast && ( <div className={`fixed top-4 right-4 z-50 flex items-center gap-2 rounded-lg p-4 shadow-lg ${toastType === 'success' ? 'bg-green-500' : 'bg-red-500' } text-white animate-in fade-in slide-in-from-top-5`}> {toastType === 'success' ? ( <CheckCircle2 className="h-5 w-5" /> ) : ( <XCircle className="h-5 w-5" /> )} <span>{toastMessage}</span> </div>)} <div className="flex justify-between items-center"> <h1 className="text-2xl font-bold">Category</h1> <Dialog open={isOpen} onOpenChange={setIsOpen}> <DialogTrigger asChild> <Button variant="outline" className="bg-primary hover:bg-primary/90 text-black shadow-lg"> <Plus className="h-4 w-4 mr-2" /> New Category </Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Create New Category</DialogTitle> <DialogDescription> Make changes to your category here. Click update when you're done. </DialogDescription> </DialogHeader> <form onSubmit={handleSubmit} className="space-y-4"> <div className="space-y-2"> <Label htmlFor="title">Title</Label> <Input id="title" value={data.title} onChange={(e) => setData('title', e.target.value)} required /> </div> <Button type="submit" disabled={processing}> Create </Button> </form> </DialogContent> </Dialog> </div> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> {categories.map((category) => ( <Card key={category.id} className="hover:bg-accent/50 transition-colors"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardTitle className="text-lg font-medium">{category.title}</CardTitle> <div className="flex gap-2"> <Button variant="ghost" size="icon" onClick={() => handleEdit(category)} > <Pencil className="h-4 w-4" /> </Button> <Button variant="ghost" size="icon" onClick={() => handleDelete(category.id)} className="text-destructive hover:text-destructive/90" > <Trash2 className="h-4 w-4" /> </Button> </div> </CardHeader> <CardContent> {category.posts_count !== undefined && ( <p className="text-sm text-muted-foreground mt-2"> {category.posts_count} Category </p> )} </CardContent> </Card> ))} </div> </div> </AppLayout> ); }Routes
routes/web.php
//routes/web.php <?php use Illuminate\Support\Facades\Route; use Inertia\Inertia; use App\Http\Controllers\CategoryController; use App\Http\Controllers\PostController; 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::resource('admin/category', CategoryController::class); Route::resource('admin/posts', PostController::class); }); 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