article

Thursday, May 8, 2025

Laravel 12 Blog | CRUD Category Pagination Search and Upload picture | React Starter kit ShadCN/UI

Laravel 12 Blog | CRUD Category Pagination Search and Upload picture | React Starter kit 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 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

Related Post