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 Modelapp/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
