article

Wednesday, June 4, 2025

Laravel 12 Simple Inertia React Pagination Components | React Starter Kit

Laravel 12 Simple Inertia React Pagination Components | 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=laravel12dev
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 UsersController change it with the following codes:
app\Http\Controllers\UsersController.php

//app\Http\Controllers\UsersController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Inertia;
use App\Models\User;

class UsersController extends Controller
{
    public function home()
    {
        $query = User::select('id', 'name', 'email', 'created_at')->latest();

        // Handle search
        if (request()->has('search')) {
            $search = request('search');
            $query->where(function ($q) use ($search) {
                $q->where('name', 'like', "%{$search}%")
                    ->orWhere('email', 'like', "%{$search}%");
            });
        }

        $users = $query->paginate(10);

        return Inertia::render('users/index', [
            'users' => $users,
            'filters' => [
                'search' => request('search', ''),
            ]
        ]);
    }
}
Frontend with React and InertiaJS
Add sidebar menu product mainNavItems from reactjs\rescourses\js\components\app-sidebar.tsx
File: pages\users\index.tsx
reactjs\resources\js\pages\users\index.tsx
//reactjs\resources\js\pages\users\index.tsx
import AppLayout from '@/layouts/app-layout';
import { type BreadcrumbItem } from '@/types';
import { Head, router } from '@inertiajs/react';
import { Search } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import Pagination from '@/components/Pagination'; 
import { useState } from 'react';

interface User {
    id: number;
    name: string;
    email: string;
    created_at: string;
}

interface Props {
    users: {
        data: User[];
        current_page: number;
        last_page: number;
        per_page: number;
        total: number;
        from: number;
        to: number;
    };
    filters: {
        search: string;
        filter: string;
    };
}

const breadcrumbs: BreadcrumbItem[] = [
    {
        title: 'Laravel 12 Simple Inertia React Pagination Components | React Starter Kit',
        href: '/admin/users',
    },
];

export default function UsersIndex({ users, filters }: Props) {
    //console.log(users); 
    const [searchTerm, setSearchTerm] = useState(filters.search);

    const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        router.get(route('users.home'), {
            search: searchTerm,
        }, {
            preserveState: true,
            preserveScroll: true,
        });
    };
    return (
        <AppLayout breadcrumbs={breadcrumbs}>
            <Head title="Users" />
            <div className="flex h-full flex-1 flex-col gap-6 rounded-xl p-6 bg-gradient-to-br from-background to-muted/20">
                <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 name or email..."
                            value={searchTerm}
                            onChange={(e) => setSearchTerm(e.target.value)}
                            className="pl-10 bg-gray-200  text-black"
                        />
                    </form>                                     
                </div>
                <div className="rounded-md border">
                    <div className="relative w-full overflow-auto">
                        <table className="w-full caption-bottom text-sm  bg-white rounded-lg">
                            <thead>
                                <tr className="bg-gray-200 text-black">
                                    <th className="py-2 px-4 text-left border-b">ID</th>
                                    <th className="py-2 px-4 text-left border-b">Name</th>
                                    <th className="py-2 px-4 text-left border-b">Email</th>
                                    <th className="py-2 px-4 text-left border-b">Created At</th>                                   
                                    <th className="py-2 px-4 text-right border-b">Actions</th>                                
                                </tr>                            
                            </thead>   
                            <tbody>
                                {users.data.map((user) => (
                                    <tr key={user.id} className="hover:bg-gray-50 text-black">
                                        <td className="p-4 align-middle font-medium border-b">{user.id}</td>
                                        <td className="p-4 align-middle font-medium border-b">{user.name}</td>
                                        <td className="p-4 align-middle font-medium border-b">{user.email}</td>
                                        <td className="p-4 align-middle font-medium border-b">{new Date(user.created_at).toLocaleDateString()}</td>
                                        <td className="p-4 align-middle text-right border-b">
                                            <div className="flex justify-end gap-2">
                                                <Button variant="ghost"
                                                    className="w-full sm:w-auto bg-blue-600 text-white rounded-lg px-4 py-2 hover:bg-blue-700"
                                                >
                                                    Edit
                                                </Button>                                                
                                                <Button variant="ghost"
                                                    className="w-full sm:w-auto bg-red-600 text-white rounded-lg px-4 py-2 hover:bg-red-700 transition transition ml-2"
                                                >
                                                    Delete
                                                </Button>                                            
                                            </div>
                                        </td>
                                    </tr>
                                ))}
                                {users.total === 0 && (
                                    <tr>
                                        <td colSpan={6} className="p-4 text-center text-muted-foreground">
                                            No users Found                                        
                                        </td>
                                    </tr>
                                )}
                            </tbody> 
                        </table>              
                    </div>                
                </div>
                {/* Pagination */}
                <div className="flex items-center justify-between px-2">
                    <div className="text-sm text-muted-foreground">
                        Showing {users.from} to {users.to} of {users.total} results  
                    </div>                    
                    <div className="flex items-center space-x-2">
                    <Pagination links={users.links}/>                  
                    </div>                
                </div> 
            </div>
        </AppLayout>
    );
}
File: js\components\Pagination.tsx
reactjs\resources\js\components\Pagination.tsx
//reactjs\resources\js\components\Pagination.tsx
import { Link } from '@inertiajs/react'

export default function Pagination({ links }) {
    console.log(links);
    return (
        <div className="flex flex-wrap items-center space-x-1 mt-4">
            {links.map((link, index) => (
                <Link 
                    key={index}    
                    href={link.url ?? '#'}
                    dangerouslySetInnerHTML={{ __html: link.label}}
                    className={`text-lg rounded-lg px-4 py-2 border gap-2
                        ${link.active ? 'border-t-blue-600 text-white  bg-blue-600' : 'bg-white text-gray-700'}
                        ${!link.url ? 'opacity-50 pointer-events-none' : 'hover:bg-gray-100'}`}
                />
            ))}
        </div>
    )
}
php artisan tinker
User::factory()->count(1000)->create()
Routes
routes/web.php
//routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Http\Controllers\UsersController;

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('/admin/users-pagination-search', [UsersController::class, 'home'])->name('users.home');
});

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

Friday, May 30, 2025

Laravel 12 React Starter kit Schedule Calendar | Fullcalendar CRUD

Laravel 12 React Starter kit Schedule Calendar | Fullcalendar CRUD

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 Schedule Model php artisan make:model Schedule -m myapp>php artisan make:model Schedule -m Open new Schedule migrations yourproject/database/migrations laravelproject\database\migrations\_create_schedule_table.php
//laravelproject\database\migrations\_create_schedule_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('schedules', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->dateTime('start');
            $table->dateTime('end');
            $table->string('color')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('schedules');
    }
};
run
myapp>php artisan migrate
update Schedule Model
app/models/Schedule.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Schedule extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'start', 
        'end',
        'color',
    ];
}
Create ScheduleController
php artisan make:controller ScheduleController
change it with the following codes:
app\Http\Controllers\ScheduleController.php

//app\Http\Controllers\ScheduleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Inertia;
use App\Models\Schedule;

class ScheduleController extends Controller
{
    public function index()
    {
        $query = Schedule::select('id', 'title', 'start', 'end', 'color', 'created_at')->get();

        return Inertia::render('schedule/index', [
            'schedules' => $query,
            'flash' => [
                'success' => session('success'),
                'error' => session('error')
            ]
        ]);
    }

    public function store(Request $request)
    {
        Schedule::create([
            'title' => $request->title,
            'start' => $request->startdate,
            'end' => $request->enddate,
            'color' => $request->color,
        ]);

        return redirect()->route('schedule.index')->with('success', 'Schedule created successfully!');
    }

    public function destroy($id)
    {
        Schedule::findOrFail($id)->delete();
        return redirect()->route('schedule.index')->with('success', 'Schedule Deleted successfully!');
    }

    public function update(Request $request, $id)
    {
        $schedule = Schedule::findOrFail($id);
        $schedule->update($request->only('start', 'end'));

        return redirect()->route('schedule.index')->with('success', 'Schedule Updated successfully!');
    }
}
Frontend with React and InertiaJS
Add sidebar menu product mainNavItems from reactjs\rescourses\js\components\app-sidebar.tsx
Index.tsx File: pages\schedule\index.tsx
reactjs\resources\js\pages\schedule\index.tsx
//reactjs\resources\js\pages\schedule\index.tsx
import AppLayout from '@/layouts/app-layout';
import { type BreadcrumbItem } from '@/types';
import { Head, router } from '@inertiajs/react';
import FullCalendar from '@fullcalendar/react'
import dayGridPlugin from '@fullcalendar/daygrid' //npm install --save @fullcalendar/react @fullcalendar/core @fullcalendar/daygrid @fullcalendar/interaction
import interactionPlugin from "@fullcalendar/interaction"; // needed for dayClick
import {
  DateSelectArg,
  formatDate,
} from "@fullcalendar/core"; //   npm install @fullcalendar/interaction https://fullcalendar.io/docs/react
import { useState, useEffect } from 'react'; 
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { DialogDescription } from '@radix-ui/react-dialog';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { CheckCircle2, XCircle } from 'lucide-react';

interface Schedule {
    id: number;
    title: string;
    start: Date;
    end: Date;
    color: string;
}
  
interface Props {
    schedules: Schedule[];
    flash?: {
        success?: string;
        error?: string;
    };
}

const breadcrumbs: BreadcrumbItem[] = [
    {
        title: 'Schedule',
        href: '/admin/schedule',
    },
];

export default function Dashboard({ schedules, flash }: Props) {
    //console.log(schedules);
    const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
    const [title, setTitle] = useState<string>(""); 
    const [color, setColor] = useState<string>("#000000");
    const [startdate, setStart] = useState<DateSelectArg | null>(null);
    const [enddate, setEnd] = useState<DateSelectArg | null>(null);
    const [toastMessage, setToastMessage] = useState('');
    const [toastType, setToastType] = useState<'success' | 'error'>('success');
    const [showToast, setShowToast] = useState(false);
    //console.log(flash);
    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 handleDateClick = (selected: DateSelectArg) => {
        const start = selected.startStr
        const end = selected.endStr
        //console.log(start)
        setStart(start)
        setEnd(end)
        setIsDialogOpen(true);
    };

    function handleSubmit(e: React.FormEvent) {
        e.preventDefault();
        //console.log(color);
        router.post(route('schedule.store'), {
            _method: 'post',
            title,
            startdate,
            enddate,
            color,
        });
        handleCloseDialog();
    };

    const handleCloseDialog = () => {
        setIsDialogOpen(false);
    };

    function formatEvents() {
        return schedules.map(schedule => {
            const {id, title, start, end, color} = schedule
      
            let startTime = new Date(start)
            let endTime = new Date(end)
      
            return {
              id, 
              title, 
              start: startTime,
              end: endTime, 
              color,
              }
        })
    }

    function handleEventDrop(info) {
        if(window.confirm("Are you sure you want to change the event date?")){
            //console.log('change confirmed')
            //console.log(info.event.start)
            const start = (new Date(info.event.start)).toISOString().slice(0, 10)
            const end = (new Date(info.event.end)).toISOString().slice(0, 10)
  
            router.put(`/admin/schedule/${info.event.id}`, {
              _method: 'put',
              start,
              end,
            });
        } else {
            console.log('change aborted')
        }
    }

    function handleEventClick(data) {
        console.log(data);
        console.log(data.event.id);
        if (confirm(`Are you sure you want to delete the event '${data.event.title}'`)) {
            router.delete(`/admin/schedule/${data.event.id}`, {
                onSuccess: () => {
                    router.reload();
                },
                onError: () => {
                    console.error("Failed to delete post.");
                },
            });
            data.event.remove()
        }
    }

    return (
        <AppLayout breadcrumbs={breadcrumbs}>
            <Head title="Schedule" />
            <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>)}

                <h1>Laravel 12 React Starter kit Schedule Calendar | Fullcalendar</h1>
                <FullCalendar
                    plugins={[dayGridPlugin, interactionPlugin]} // Initialize calendar with required plugins.
                    headerToolbar = {{
                        left: 'prev,next' ,
                        center: 'title',
                        right: 'dayGridMonth,timeGridWeek,timeGridDay'
                    }}
                    initialView="dayGridMonth"
                    editable={true}
                    selectable={true} // Allow dates to be selectable.
                    selectMirror={true} // Mirror selections visually.
                    select={handleDateClick} // Handle date selection to create new events.
                    events={formatEvents()}
                    eventDrop={handleEventDrop}
                    eventClick={handleEventClick}
                    //events={[
                    //    {title : 'Event 1', start: '2025-05-19', end: '2025-05-21'},
                    //]}
                />
            </div>
            <div className="flex justify-between items-center">                
                    <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>                     
                        <DialogContent>
                            <DialogHeader>
                                <DialogTitle>Add New Event Details</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"
                                    type="text"
                                    value={title}
                                    onChange={(e) => setTitle(e.target.value)}
                                    />
                                </div>                               
                                <div className="space-y-2">
                                    <Label htmlFor="color">Color</Label>                                    
                                    <Input type="color" id="color" name="color"
                                    value={color}
                                    onChange={(e) => setColor(e.target.value)}
                                    />
                                </div>                               
                                <Button type="submit">
                                    Create
                                </Button>                            
                            </form>                
                        </DialogContent>                    
                    </Dialog>                
            </div>
        </AppLayout>
    );
}
Routes
routes/web.php
//routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Http\Controllers\ScheduleController;

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('/admin/schedule', [ScheduleController::class, 'index'])->name('schedule.index');
    Route::post('/admin/schedule', [ScheduleController::class, 'store'])->name('schedule.store');
    Route::delete('/admin/schedule/{id}', [ScheduleController::class, 'destroy'])->name('schedule.destroy');
    Route::put('/admin/schedule/{id}', [ScheduleController::class, 'update'])->name('users.update');
});

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

Wednesday, May 28, 2025

Laravel 12 Schedule Calendar | Fullcalendar

Laravel 12 Schedule Calendar | Fullcalendar

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 Schedule Model php artisan make:model Schedule -m myapp>php artisan make:model Schedule -m Open new Schedule migrations yourproject/database/migrations laravelproject\database\migrations\_create_schedule_table.php
//laravelproject\database\migrations\_create_schedule_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('schedules', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->dateTime('start');
            $table->dateTime('end');
            $table->string('color')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('schedules');
    }
};
run
myapp>php artisan migrate
update Schedule Model
app/models/Schedule.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Schedule extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'start', 
        'end',
        'color',
    ];
}
Create ScheduleController
php artisan make:controller ScheduleController
change it with the following codes:
app\Http\Controllers\ScheduleController.php

//app\Http\Controllers\ScheduleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Schedule;
use Carbon\Carbon;

class ScheduleController extends Controller
{
    public function index()
    {
        return view('schedule.index');
    }

    public function store(Request $request)
    {
        $item = new Schedule();
        $item->title = $request->title;
        $item->start = $request->start;
        $item->end = $request->end;
        $item->color = $request->color;
        $item->save();

        return redirect()->route("schedule.index");
    }

    public function getEvents()
    {
        $schedules = Schedule::all();
        return response()->json($schedules);
    }

    public function update(Request $request, $id)
    {
        $schedule = Schedule::findOrFail($id);

        $schedule->update([
            'start' => Carbon::parse($request->input('start_date'))->setTimezone('UTC'),
            'end' => Carbon::parse($request->input('end_date'))->setTimezone('UTC'),
        ]);

        return response()->json(['message' => 'Event moved successfully']);
    }

    public function deleteEvent($id)
    {
        $schedule = Schedule::findOrFail($id);
        $schedule->delete();

        return response()->json(['message' => 'Event deleted successfully']);
    }

    public function search(Request $request)
    {
        $searchKeywords = $request->input('title');
        $matchingEvents = Schedule::where('title', 'like', '%' . $searchKeywords . '%')->get();

        return response()->json($matchingEvents);
    }
}
Create AuthController
php artisan make:controller AuthController
change it with the following codes:
app\Http\Controllers\AuthController.php

//app\Http\Controllers\AuthController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function login()
    {
        $pass = Hash::make("123456789");
        return view("login")->with('password', $pass);
    }

    public function loginAction(Request $request)
    {
        $validated = $request->validate([
            'email' => 'required|email',
            'password' => 'required'
        ]);

        if (!Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) {
            throw ValidationException::withMessages([
                'email' => trans('auth.failed')
            ]);
        }

        $request->session()->regenerate();
        return redirect()->route("products.index")->with("success", "Login successfully");
    }

    public function logout(Request $request)
    {
        Auth::guard('web')->logout();

        $request->session()->invalidate();

        return redirect('/login');
    }
}
Create View File resources/views/layouts/app.blade.php
//resources/views/layouts/app.blade.php
<!DOCTYPE html>
<html>

<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
    <title>Schedule Fullcalendar</title>
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
</head>

<body>
    <div class="layout-wrapper layout-content-navbar">
        <div class="layout-container">
            <div class="layout-page">
                <div class="content-wrapper">
                    @yield('content')
                </div>
            </div>
        </div>
    </div>
</body>

</html>
Home resources/views/schedule/index.blade.php
//resources/views/schedule/index.blade.php
@extends('layouts.app')
@section('content')
<div class="container mt-5">
    <div class="row">
        <h1>Laravel 12 Schedule Calendar | Fullcalendar</h1>
        <div class="col-md-6">
            <div class="input-group mb-3">
                <input type="text" id="searchInput" class="form-control" placeholder="Search events">
                <div class="input-group-append">
                    <button id="searchButton" class="btn btn-primary">Search</button>
                </div>
            </div>
        </div>
        <div class="col-md-6">
            Color : <input type="color" id="myColor" class='form-control' name="colorpicker" onchange="myFunction()" />
            <p id="demo"></p>
        </div>
    </div>

    @if (Session::has('success'))
    <span class="alert alert-success p-2">{{ Session::get('success')}}</span>
    @endif
    @if (Session::has('error'))
    <span>{{ Session::get('error')}}</span>
    @endif

    <div class="card">
        <div class="card-body">
            <div id="calendar" style="width: 100%;height:100vh"></div>
        </div>
    </div>
</div>
<script
    src="https://code.jquery.com/jquery-3.7.1.js"
    integrity="sha256-eKhayi8LEQwp4NKxN+CfCh+3qOVUtJn3QNZ0TciWLP4="
    crossorigin="anonymous"></script>
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@6.1.17/index.global.min.js'></script> <!--fullcalendar.io/docs/initialize-globals-->

<script type="text/javascript">
    $.ajaxSetup({
        headers: {
            'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
        }
    });

    var calendarEl = document.getElementById('calendar');
    var events = [];
    var calendar = new FullCalendar.Calendar(calendarEl, {
        headerToolbar: {
            left: 'prev,next today',
            center: 'title',
            right: 'dayGridMonth,timeGridWeek,timeGridDay'
        },
        initialView: 'dayGridMonth',
        events: '/events',
        editable: true,
        selectable: true,
        select: function(info) {
            var title = prompt('Event Title:');
            var color = prompt("Color", "#0d6efd");
            if (title && color) {
                //alert('selected ' + info.startStr + ' to ' + info.endStr);
                $.ajax({
                    url: "/create-schedule",
                    data: 'title=' + title + '&start=' + info.startStr + '&end=' + info.endStr + '&color=' + color + '&_token=' + "{{ csrf_token() }}",
                    type: "post",
                    success: function(data) {
                        alert("Added Successfully");
                        calendar.refetchEvents(); // Refresh events
                    }
                });

            }
        },
        // Drag And Drop
        eventDrop: function(info) {
            var eventId = info.event.id;
            var newStartDate = info.event.start;
            var newEndDate = info.event.end || newStartDate;
            var newStartDateUTC = newStartDate.toISOString().slice(0, 10);
            var newEndDateUTC = newEndDate.toISOString().slice(0, 10);

            $.ajax({
                method: 'post',
                url: `/schedule/${eventId}`,
                data: {
                    '_token': "{{ csrf_token() }}",
                    start_date: newStartDateUTC,
                    end_date: newEndDateUTC,
                },
                success: function(response) {
                    alert(response.message);
                    console.log('Event moved successfully.');
                },
                error: function(error) {
                    console.error('Error moving event:', error);
                }
            });
        },
        // Deleting The Event
        eventContent: function(info) {
            var eventTitle = info.event.title;
            var eventElement = document.createElement('div');
            eventElement.innerHTML = '<span style="cursor: pointer;">❌</span> ' + eventTitle;

            eventElement.querySelector('span').addEventListener('click', function() {
                if (confirm("Are you sure you want to delete this event?")) {
                    var eventId = info.event.id;
                    $.ajax({
                        method: 'get',
                        url: '/schedule/delete/' + eventId,
                        headers: {
                            'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
                        },
                        success: function(response) {
                            alert(response.message);
                            console.log('Event deleted successfully.');
                            calendar.refetchEvents(); // Refresh events after deletion
                        },
                        error: function(error) {
                            console.error('Error deleting event:', error);
                        }
                    });
                }
            });
            return {
                domNodes: [eventElement]
            };
        },
    });
    calendar.render();

    document.getElementById('searchButton').addEventListener('click', function() {
        var searchKeywords = document.getElementById('searchInput').value.toLowerCase();
        filterAndDisplayEvents(searchKeywords);
    });

    function filterAndDisplayEvents(searchKeywords) {
        $.ajax({
            method: 'GET',
            url: `/events/search?title=${searchKeywords}`,
            success: function(response) {
                calendar.removeAllEvents();
                calendar.addEventSource(response);
            },
            error: function(error) {
                console.error('Error searching events:', error);
            }
        });
    }

    function myFunction() {
        var x = document.getElementById("myColor").value;
        document.getElementById("demo").innerHTML = x;
    }
</script>
@endsection
resources/views/login.blade.php
//resources/views/login.blade.php
@extends('layouts.app')
@section('content')
<div class="container mt-5">
    <div class="card">
        <div class="card-header">
            Login
        </div>
        <div class="card-body">
            <form action="{{ route('login.action') }}" method="POST" class="user">
                @csrf
                @if ($errors->any())
                <div class="alert alert-danger">
                    <ul>
                        @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                        @endforeach
                    </ul>
                </div>
                @endif
                <div class="mb-3">
                    <label class="form-label">Email</label>
                    <input name="email" type="email" class="form-control form-control-user" id="exampleInputEmail" aria-describedby="emailHelp" placeholder="Enter Email Address...">
                </div>
                <div class="mb-3">
                    <label class="form-label">Password</label>
                    <input name="password" type="password" class="form-control form-control-user" id="exampleInputPassword" placeholder="Password">
                    {{ $password }}
                </div>
                <div class="mb-3">
                    <div class="custom-control custom-checkbox small">
                        <input name="remember" type="checkbox" class="custom-control-input" id="customCheck">
                        <label class="custom-control-label" for="customCheck">Remember
                            Me</label>
                    </div>
                </div>
                <button type="submit" class="btn btn-primary btn-block btn-user">Login</button>
            </form>
        </div>
    </div>

</div>

@endsection
Routes
routes/web.php
//routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ScheduleController; //php artisan make:controller ScheduleController
use App\Http\Controllers\AuthController;

Route::get('/', function () {
    return view('welcome');
});

Route::middleware('auth')->group(function () {
    Route::controller(ScheduleController::class)->group(function () {
        Route::get('/fullcalender', 'index')->name('schedule.index');
        Route::post('/create-schedule', 'store')->name('schedule.store');
        Route::get('/events', 'getEvents')->name('schedule.events');
        Route::post('/schedule/{id}', 'update')->name('schedule.update');
        Route::get('/schedule/delete/{id}', 'deleteEvent')->name('schedule.deleteEvent');
        Route::get('/events/search', 'search')->name('schedule.search');
    });
});

Route::controller(AuthController::class)->group(function () {
    Route::get('login', 'login')->name('login');
    Route::post('login', 'loginAction')->name('login.action');
    Route::get('logout', 'logout')->middleware('auth')->name('logout');
});

Wednesday, May 14, 2025

Laravel 12 React Pagination and Search | React Starter kit

Laravel 12 React Pagination and Search | 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=laravel12dev
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 UsersController change it with the following codes:
app\Http\Controllers\UsersController.php

//app\Http\Controllers\UsersController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Inertia\Inertia;
use App\Models\User;

class UsersController extends Controller
{
    public function home()
    {
        $query = User::select('id', 'name', 'email', 'created_at')->latest();

        // Handle search
        if (request()->has('search')) {
            $search = request('search');
            $query->where(function ($q) use ($search) {
                $q->where('name', 'like', "%{$search}%")
                    ->orWhere('email', 'like', "%{$search}%");
            });
        }

        $users = $query->paginate(10);

        return Inertia::render('users/index', [
            'users' => $users,
            'filters' => [
                'search' => request('search', ''),
            ]
        ]);
    }
}
Frontend with React and InertiaJS
Add sidebar menu product mainNavItems from reactjs\rescourses\js\components\app-sidebar.tsx
File: pages\users\index.tsx
reactjs\resources\js\pages\users\index.tsx
//reactjs\resources\js\pages\users\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 { Search, ChevronLeft, ChevronRight } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { useState } from 'react';

interface User {
    id: number;
    name: string;
    email: string;
    created_at: string;
}

interface Props {
    users: {
        data: User[];
        current_page: number;
        last_page: number;
        per_page: number;
        total: number;
        from: number;
        to: number;
    };
    filters: {
        search: string;
        filter: string;
    };
}

const breadcrumbs: BreadcrumbItem[] = [
    {
        title: 'Users',
        href: '/admin/users',
    },
];

export const generatePaginationnumber = (currentPage, totalPages) => {

    if (totalPages <= 7) {
       return Array.from({ length: totalPages }, (_, i) => i + 1); 
    }
      
    if (currentPage <= 3) {
        return [1, 2, 3 , "...", totalPages - 1, totalPages];
    }
     
    if (currentPage >= totalPages - 3) {
       console.log("2");
       return [1, 2, 3, "...", totalPages - 2, totalPages - 1, totalPages];
    }
     
    return [
      1,
      "...",
      currentPage - 1,
      currentPage,
      currentPage + 1,
      "...",
      totalPages,
    ];
};

export default function UsersIndex({ users, filters }: Props) {
    //console.log(users); 

    const params = new URLSearchParams(window.location.search);
    const currentPage = Number(params.get("page")) || 1;
    //console.log(currentPage); 

    const totalPages = Math.ceil(Number(users.total) / Number(users.per_page));
    //const totalPages = users.total;
    console.log(totalPages);

    const allPages = generatePaginationnumber(currentPage, totalPages);

    const [searchTerm, setSearchTerm] = useState(filters.search);

    const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        router.get(route('users.home'), {
            search: searchTerm,
        }, {
            preserveState: true,
            preserveScroll: true,
        });
    };
    
    const handlePageChange = (page: number) => {
        router.get(route('users.home'), {
            page,
        }, {
            preserveState: true,
            preserveScroll: true,
        });
    };

    return (
        <AppLayout breadcrumbs={breadcrumbs}>
            <Head title="Users" />
            <div className="flex h-full flex-1 flex-col gap-6 rounded-xl p-6 bg-gradient-to-br from-background to-muted/20">
                <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 name or email..."
                            value={searchTerm}
                            onChange={(e) => setSearchTerm(e.target.value)}
                            className="pl-10 bg-gray-200  text-black"
                        />
                    </form>                                    
                </div>
                <div className="rounded-md border">
                    <div className="relative w-full overflow-auto">
                        <table className="w-full caption-bottom text-sm  bg-white rounded-lg">
                            <thead>
                                <tr className="bg-gray-200 text-black">
                                    <th className="py-2 px-4 text-left border-b">ID</th>
                                    <th className="py-2 px-4 text-left border-b">Name</th>
                                    <th className="py-2 px-4 text-left border-b">Email</th>
                                    <th className="py-2 px-4 text-left border-b">Created At</th>                                   
                                    <th className="py-2 px-4 text-right border-b">Actions</th>                                
                                </tr>                            
                            </thead>     
                            <tbody>
                                {users.data.map((user) => (
                                    <tr key={user.id} className="hover:bg-gray-50 text-black">
                                        <td className="p-4 align-middle font-medium border-b">{user.id}</td>
                                        <td className="p-4 align-middle font-medium border-b">{user.name}</td>
                                        <td className="p-4 align-middle font-medium border-b">{user.email}</td>
                                        <td className="p-4 align-middle font-medium border-b">{new Date(user.created_at).toLocaleDateString()}</td>
                                        <td className="p-4 align-middle text-right border-b">
                                            <div className="flex justify-end gap-2">
                                                <Button variant="ghost"
                                                    className="w-full sm:w-auto bg-blue-600 text-white rounded-lg px-4 py-2 hover:bg-blue-700"
                                                >
                                                    Edit
                                                </Button>                                                
                                                <Button variant="ghost"
                                                    className="w-full sm:w-auto bg-red-600 text-white rounded-lg px-4 py-2 hover:bg-red-700 transition transition ml-2"
                                                >
                                                    Delete
                                                </Button>                                            
                                            </div>
                                        </td>
                                    </tr>
                                ))}
                                {users.total === 0 && (
                                    <tr>
                                        <td colSpan={6} className="p-4 text-center text-muted-foreground">
                                            No users Found                                        
                                        </td>
                                    </tr>
                                )}
                            </tbody> 
                        </table>                    
                    </div>                
                </div>

                {/* Pagination */}
                <div className="flex items-center justify-between px-2">
                    <div className="text-sm text-muted-foreground">
                        Showing {users.from} to {users.to} of {users.total} results  
                    </div>                    
                    <div className="flex items-center space-x-2">
                        <Button variant="outline"
                            size="icon"
                            onClick={() => handlePageChange(users.current_page - 1)}
                            disabled={users.current_page === 1}
                        >
                            <ChevronLeft className="h-4 w-4" />
                        </Button>   
                        <div className="flex items-center space-x-1">
                        {allPages.map((page, index) => (

                            <Button key={index}
                            variant={page === users.current_page ? "default" : "outline"}
                            size="icon"
                            onClick={() => handlePageChange(page)}
                            >
                            {page}
                            </Button> 
                           
                        ))}
                        </div>                        
                        <Button variant="outline"
                            size="icon"
                            onClick={() => handlePageChange(users.current_page + 1)}
                            disabled={users.current_page === users.last_page}
                        >
                            <ChevronRight className="h-4 w-4" />
                        </Button>                    
                    </div>                
                </div> 
            </div>
        </AppLayout>
    );
}
Routes
routes/web.php
//routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Http\Controllers\UsersController;

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('/admin/users-pagination-search', [UsersController::class, 'home'])->name('users.home');
});

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

Sunday, May 11, 2025

Laravel 12 CRUD Image Upload, Search, Soft Deletes Bootstrap and Vite

Laravel 12 CRUD Image Upload, Search, Soft Deletes Bootstrap and Vite e

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 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('description')->nullable();
            $table->integer('quantity');
            $table->float('price');
            $table->string('image')->nullable();
            $table->enum('status', ['active', 'in-active'])->default('active');
            $table->softDeletes();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};
run
myapp>php artisan migrate
update Product Model
app/models/Product.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;

class Product extends Model
{
    use SoftDeletes, HasFactory;

    protected $fillable = [
        "name",
        "description",
        "quantity",
        "price",
        "status",
        "image",
    ];
}
Create PostController
php artisan make:controller PostController
change it with the following codes:
app\Http\Controllers\PostController.php

//app\Http\Controllers\ProductController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Product;
use Illuminate\Support\Facades\Storage;

class ProductController extends Controller
{
    public function index(Request $request)
    {
        // $products = Product::all();
        $query = Product::query();
        if (request()->has("search") && $request->search) {
            $query = $query->where("name", "like", "%" . $request->search . "%")
                ->orWhere('description', 'like', "%" . $request->search . "%");
        }
        $products = $query->latest()->paginate(3);

        return view("product.index", compact("products"));
    }

    public function create()
    {
        return view("product.create");
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            "name" => "required|string",
            "description" => "nullable|string",
            "price" => "required|numeric",
            "quantity" => "required|numeric",
            "status" => "required",
            "image" => "nullable|image|mimes:jpg,png",
        ]);

        if ($request->hasFile("image")) { //php artisan storage:link
            $validated["image"] = $request->file("image")->store("products", "public");
        }
        Product::create($validated);

        return redirect()->route("products.index")->with("success", "product added successfully");
    }

    public function show($id)
    {
        $product = Product::find($id);
        return view("product.show", compact("product"));
    }

    public function edit($id)
    {
        $product = Product::find($id);
        return view("product.edit", compact("product"));
    }

    public function update(Request $request, $id)
    {
        $validated = $request->validate([
            "name" => "required|string",
            "description" => "nullable|string",
            "price" => "required|numeric",
            "quantity" => "required|numeric",
            "status" => "required",
            "image" => "nullable|image|mimes:jpg,png",
        ]);

        if ($request->hasFile("image")) {
            if ($request->image && Storage::disk("public")->exists($request->image)) {
                Storage::disk("public")->delete($request->image);
            }
            $validated["image"] = $request->file("image")->store("products", "public");
        }

        Product::find($id)->update($validated);

        return redirect()->route("products.index")->with("success", "product updated successfully!");
    }

    public function destroy($id)
    {
        Product::find($id)->delete();
        return redirect()->route("products.index")->with("success", "product deleted successfully!");
    }

    public function trashedProducts(Request $request)
    {
        $query = Product::query()->onlyTrashed();
        $products = $query->paginate(3);
        return view("product.deleted-products", compact("products"));
    }

    public function showTrashed($id)
    {
        $product = Product::onlyTrashed()->findOrFail($id);
        return view("product.show", compact("product"));
    }

    public function restoreProduct($id)
    {
        $product = Product::onlyTrashed()->findOrFail($id);
        $product->restore();
        return redirect()->route("products.index")->with("success", "product restored successfully");
    }

    public function destroyProduct($id)
    {
        $product = Product::onlyTrashed()->findOrFail($id);
        if ($product->image && Storage::exists($product->image)) {
            Storage::delete($product->image);
        }
        $product->forceDelete();

        return redirect()->route("products.index")->with("success", "product was force deleted successfully!");
    }
}
Create AuthController
php artisan make:controller AuthController
change it with the following codes:
app\Http\Controllers\AuthController.php

//app\Http\Controllers\AuthController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function login()
    {
        $pass = Hash::make("123456789");
        return view("login")->with('password', $pass);
    }

    public function loginAction(Request $request)
    {
        $validated = $request->validate([
            'email' => 'required|email',
            'password' => 'required'
        ]);

        if (!Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) {
            throw ValidationException::withMessages([
                'email' => trans('auth.failed')
            ]);
        }

        $request->session()->regenerate();
        return redirect()->route("products.index")->with("success", "Login successfully");
    }

    public function logout(Request $request)
    {
        Auth::guard('web')->logout();

        $request->session()->invalidate();

        return redirect('/login');
    }
}
Create View File resources/views/layouts/app.blade.php
//resources/views/layouts/app.blade.php
<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Laravel 12 CRUD</title>
    @vite(['resources/sass/app.scss', 'resources/js/app.js'])
</head>

<body>
    <nav class="py-2 bg-body-tertiary border-bottom">
        <div class="container d-flex flex-wrap">
            <ul class="nav me-auto">
                <li class="nav-item"><a href="#" class="nav-link link-body-emphasis px-2 active" aria-current="page">Home</a></li>
                <li class="nav-item"><a href="#" class="nav-link link-body-emphasis px-2">Features</a></li>
                <li class="nav-item"><a href="#" class="nav-link link-body-emphasis px-2">Pricing</a></li>
                <li class="nav-item"><a href="#" class="nav-link link-body-emphasis px-2">FAQs</a></li>
                <li class="nav-item"><a href="#" class="nav-link link-body-emphasis px-2">About</a></li>
            </ul>
            <ul class="nav">
                <li class="nav-item"><a href="#" class="nav-link link-body-emphasis px-2">Login</a></li>
                <li class="nav-item"><a href="#" class="nav-link link-body-emphasis px-2">Sign up</a></li>
            </ul>
        </div>
    </nav>
    <header class="py-3 mb-4 border-bottom">
        <div class="container d-flex flex-wrap justify-content-center"> <a href="/" class="d-flex align-items-center mb-3 mb-lg-0 me-lg-auto link-body-emphasis text-decoration-none">
                <svg class="bi me-2" width="40" height="32" aria-hidden="true">
                    <use xlink:href="#bootstrap"></use>
                </svg> <span class="fs-4">Cairocoders</span> </a>
        </div>
    </header>
    @yield('content')
</body>

</html>
Home resources/views/product/index.blade.php
//resources/views/product/index.blade.php
@extends('layouts.layout')
@section('content')
<div class="container">
    <div class="card">
        <div class="card-header">
            <div class="row">
                <div class="col">
                    <h2>Product List</h2>
                </div>
                <div class="col">
                    <div class="row">
                        <div class="col-md-12">
                            <div class="row">
                                <div class="col">
                                    <div><a href="{{ route('products.trashed')}}" class="float-end btn btn-danger" style="margin-left:10px;">Deleted</a></div>
                                    <div><a href="{{ route('products.create')}}" class="float-end btn btn-primary">Add New</a></div>
                                </div>
                            </div>
                        </div>
                        <div class="col-md-12" style="padding-top:20px;">
                            <form class="d-flex" role="search" action="{{ route('products.index')}}" method="GET">
                                @csrf
                                <input class="form-control me-2" name="search" type="search" placeholder="Search"
                                    aria-label="Search">
                                <button class="btn btn-outline-success" type="submit">Search</button>
                            </form>
                        </div>
                    </div>

                </div>
            </div>
        </div>
        @if (Session::has('success'))
        <span class="alert alert-success p-2">{{ Session::get('success')}}</span>
        @endif
        @if (Session::has('error'))
        <span>{{ Session::get('error')}}</span>
        @endif
        <div class="card-body">
            <table class="table table-striped">
                <thead>
                    <tr>
                        <th scope="col">#</th>
                        <th scope="col">Photo</th>
                        <th scope="col">Product Name</th>
                        <th scope="col">Quantity</th>
                        <th scope="col">Price</th>
                        <th scope="col">Status</th>
                        <th scope="col">Description</th>
                        <th scope="col">Action</th>
                    </tr>
                </thead>
                <tbody>
                    @if (count($products) > 0)
                    @foreach ($products as $product)
                    <tr>
                        <th scope="row">{{ $loop->iteration }}</th>
                        <td><img src="{{ asset('storage/' . $product->image) }}" width="100"></td>
                        <td>{{ str($product->name)->words(2)}}</td>
                        <td>{{ $product->quantity}}</td>
                        <td>{{ $product->price}}</td>
                        <td>{{ $product->status}}</td>
                        <td>{{ str($product->description)->words(5)}}</td>
                        <td>
                            <a href="{{ route('products.show', $product->id) }} " class="btn btn-success btn">show</a>
                            <a href="{{ route('products.edit', $product->id) }} " class="btn btn-primary btn">edit</a>
                            <form action="{{ route('products.destroy', $product->id) }}" method="POST"
                                style="display:inline-block">
                                @csrf @method('DELETE')
                                <button onclick="return confirm('Are you sure?')"
                                    class="btn btn btn-danger">Delete</button>
                            </form>
                        </td>
                    </tr>
                    @endforeach
                    @else
                    <tr>
                        <td colspan="8" class="text-center">No Data Found!</td>
                    </tr>
                    @endif

                </tbody>
            </table>
            {{ $products->links('pagination::bootstrap-5') }}
        </div>
    </div>
</div>
@endsection
resources/views/product/create.blade.php
//resources/views/product/create.blade.php
@extends('layouts.layout')
@section('content')
<div class="container">
    <div class="card">
        <div class="card-header">
            Add New Product
        </div>
        <div class="card-body">
            <form action="{{ route('products.store')}}" method="post" enctype="multipart/form-data">
                @csrf
                @include('product.form')

                <button type="submit" class="btn btn-primary">Save</button>
                <a href="{{ route('products.index')}}" class="btn btn-secondary">Cancel</a>
            </form>
        </div>
    </div>

</div>

@endsection
resources/views/product/form.blade.php
//resources/views/product/form.blade.php
<div class="mb-3">
    <label class="form-label">Product Name *</label>
    <input type="text" name="name" class="form-control" value="{{ old('name', $product->name ?? '') }}">
    @error('name')
    <span class="text-danger">{{ $message }}</span>
    @enderror
</div>

<div class="mb-3">
    <label class="form-label">Description</label>
    <textarea name="description" class="form-control">{{ old('description', $product->description ?? '') }}</textarea>
    @error('description')
    <span class="text-danger">{{ $message }}</span>
    @enderror
</div>

<div class="mb-3">
    <label class="form-label">Price *</label>
    <input type="number" name="price" step="0.01" class="form-control"
        value="{{ old('price', $product->price ?? '') }}">
    @error('price')
    <span class="text-danger">{{ $message }}</span>
    @enderror
</div>

<div class="mb-3">
    <label class="form-label">Quantity *</label>
    <input type="number" name="quantity" class="form-control" value="{{ old('quantity', $product->quantity ?? '') }}">
    @error('quantity')
    <span class="text-danger">{{ $message }}</span>
    @enderror
</div>

<div class="mb-3">
    <label class="form-label">Status *</label>
    <select name="status" class="form-select">
        <option value="active" {{ (old('status', $product->status ?? '') == 'active') ? 'selected' : '' }}>Active</option>
        <option value="in-active" {{ (old('status', $product->status ?? '') == 'in-active') ? 'selected' : '' }}>Inactive
        </option>
    </select>
    @error('status')
    <span class="text-danger">{{ $message }}</span>
    @enderror
</div>

<div class="mb-3">
    <label class="form-label">Product Image</label>
    <input type="file" name="image" class="form-control">
    @error('image')
    <span class="text-danger">{{ $message }}</span>
    @enderror
    @if (!empty($product->image))
    <img src="{{ asset('storage/' . $product->image) }}" class="mt-2" width="300">
    @endif
</div>
resources/views/product/edit.blade.php
//resources/views/product/edit.blade.php
@extends('layouts.layout')
@section('content')
<div class="container">
    <div class="card">
        <div class="card-header">
            Edit Product
        </div>
        <div class="card-body">
            <form action="{{ route('products.update', $product->id) }}" method="POST" enctype="multipart/form-data">
                @csrf
                @method('PUT')
                @include('product.form')

                <button type="submit" class="btn btn-primary">Update</button>
                <a href="{{ route('products.index')}}" class="btn btn-secondary">Cancel</a>
            </form>
        </div>
    </div>

</div>

@endsection
resources/views/product/show.blade.php
//resources/views/product/show.blade.php
@extends('layouts.layout')
@section('content')
<div class="container">
    <h2>Product Details</h2>

    <div class="card">
        <div class="card-body">
            @if($product->image)
            <img src="{{ asset('storage/' . $product->image) }}" width="250" class="mb-3">
            @endif

            <p><strong>Name:</strong> {{ $product->name }}</p>
            <p><strong>Description:</strong> {{ $product->description }}</p>
            <p><strong>Price:</strong> ${{ number_format($product->price, 2) }}</p>
            <p><strong>Quantity:</strong> {{ $product->quantity }}</p>
            <p><strong>Status:</strong>
                <span class="badge bg-{{ $product->status === 'active' ? 'success' : 'secondary' }}">
                    {{ ucfirst($product->status) }}
                </span>
            </p>

            <a href="{{ route('products.edit', $product->id) }}" class="btn btn-warning">Edit</a>
            <a href="{{ route('products.index') }}" class="btn btn-secondary">Back</a>
        </div>
    </div>
</div>

@endsection
resources/views/product/deleted-products.blade.php
//resources/views/product/deleted-products.blade.php
@extends('layouts.layout')
@section('content')
<div class="container">
    <div class="card">
        <div class="card-header">
            <div class="row">
                <div class="col">
                    <h2>Product List</h2>
                </div>
                <div class="col">
                    <div class="row">
                        <div class="col-md-12">
                            <a href="{{ route('products.index')}}" class="float-end btn btn-warning">View All
                                Products</a>
                        </div>
                        <div class="col-md-12" style="padding-top:20px;">
                            <form class="d-flex" role="search" action="{{ route('products.trashed')}}" method="GET">
                                @csrf
                                <input class="form-control me-2" name="search" type="search" placeholder="Search"
                                    aria-label="Search">
                                <button class="btn btn-outline-success" type="submit">Search</button>
                            </form>
                        </div>
                    </div>

                </div>
            </div>
        </div>
        @if (Session::has('success'))
        <span class="alert alert-success p-2">{{ Session::get('success')}}</span>
        @endif
        @if (Session::has('error'))
        <span>{{ Session::get('error')}}</span>
        @endif
        <div class="card-body">
            <table class="table table-striped">
                <thead>
                    <tr>
                        <th scope="col">#</th>
                        <th scope="col">Photo</th>
                        <th scope="col">Product Name</th>
                        <th scope="col">Quantity</th>
                        <th scope="col">Price</th>
                        <th scope="col">Status</th>
                        <th scope="col">Description</th>
                        <th scope="col">Action</th>
                    </tr>
                </thead>
                <tbody>
                    @if (count($products) > 0)
                    @foreach ($products as $product)
                    <tr>
                        <th scope="row">{{ $loop->iteration }}</th>
                        <td><img src="{{ asset('storage/' . $product->image) }}" width="100"></td>
                        <td>{{ $product->name}}</td>
                        <td>{{ $product->quantity}}</td>
                        <td>{{ $product->price}}</td>
                        <td>{{ $product->status}}</td>
                        <td>{{ $product->description}}</td>
                        <td>
                            <a href="{{ route('trashed.show', $product->id) }}" class="btn btn-success btn">show</a>
                            <form action="{{ route('trashed.restore', $product->id) }}" method="POST"
                                style="display:inline-block">
                                @csrf
                                @method('PUT')
                                <button onclick="return confirm('Are you sure?')"
                                    class="btn btn btn-info">Restore</button>
                            </form>
                            <form action="{{ route('trashed.delete', $product->id) }}" method="POST"
                                style="display:inline-block">
                                @csrf
                                @method('DELETE')
                                <button onclick="return confirm('Are you sure?')"
                                    class="btn btn btn-danger">Delete</button>
                            </form>
                        </td>
                    </tr>
                    @endforeach
                    @else
                    <tr>
                        <td colspan="8" class="text-center">No Data Found!</td>
                    </tr>
                    @endif

                </tbody>
            </table>
            {{ $products->links() }}
        </div>
    </div>
</div>
@endsection
resources/views/login.blade.php
//resources/views/login.blade.php
@extends('layouts.layout')
@section('content')
<div class="container">
    <div class="card">
        <div class="card-header">
            Login
        </div>
        <div class="card-body">
            <form action="{{ route('login.action') }}" method="POST" class="user">
                @csrf
                @if ($errors->any())
                <div class="alert alert-danger">
                    <ul>
                        @foreach ($errors->all() as $error)
                        <li>{{ $error }}</li>
                        @endforeach
                    </ul>
                </div>
                @endif
                <div class="mb-3">
                    <label class="form-label">Email</label>
                    <input name="email" type="email" class="form-control form-control-user" id="exampleInputEmail" aria-describedby="emailHelp" placeholder="Enter Email Address...">
                </div>
                <div class="mb-3">
                    <label class="form-label">Password</label>
                    <input name="password" type="password" class="form-control form-control-user" id="exampleInputPassword" placeholder="Password">
                    {{ $password }}
                </div>
                <div class="mb-3">
                    <div class="custom-control custom-checkbox small">
                        <input name="remember" type="checkbox" class="custom-control-input" id="customCheck">
                        <label class="custom-control-label" for="customCheck">Remember
                            Me</label>
                    </div>
                </div>
                <button type="submit" class="btn btn-primary btn-block btn-user">Login</button>
            </form>
        </div>
    </div>

</div>

@endsection
Routes
routes/web.php
//routes/web.php
<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductController;
use App\Http\Controllers\AuthController;

Route::get('/', function () {
    return view('welcome');
});

Route::middleware('auth')->group(function () {
    //Route::get('dashboard', function () {
    //    return view('dashboard');
    //})->name('dashboard');

    Route::controller(ProductController::class)->group(function () {
        Route::get('/admin/products', 'index')->name('products.index');
        Route::get('/admin/products/create', 'create')->name('products.create');
        Route::post('/admin/products', 'store')->name('products.store');
        Route::get('/admin/products/show/{product}', 'show')->name('products.show');
        Route::get('/admin/products/{product}/edit', 'edit')->name('products.edit');
        Route::put('/admin/products/{product}', 'update')->name('products.update');
        Route::delete('/admin/products/{product}', 'destroy')->name('products.destroy');

        Route::get('/admin/deleted-products', 'trashedProducts')->name('products.trashed');
        Route::get('/admin/show-trashed-product/{id}', 'showTrashed')->name('trashed.show');
        Route::put('/admin/restore-product/{id}', 'restoreProduct')->name('trashed.restore');
        Route::delete('/admin/delete-product/{id}', 'destroyProduct')->name('trashed.delete');
    });
});

Route::controller(AuthController::class)->group(function () {
    Route::get('login', 'login')->name('login');
    Route::post('login', 'loginAction')->name('login.action');
    Route::get('logout', 'logout')->middleware('auth')->name('logout');
});

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