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 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 public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('content'); $table->string('picture')->nullable(); // Add picture column (nullable) $table->timestamps(); }); }run
myapp>php artisan migrate
update Product Model
app/models/Product.php
//app/models/Product.php <?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Product extends Model { protected $fillable = ['title', 'content', 'picture']; }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 App\Models\Post; use Inertia\Inertia; use Inertia\Response; class PostController extends Controller { public function index(): Response { return Inertia::render('Posts', [ 'posts' => Post::all(), ]); } public function store(Request $request) { $request->validate([ 'title' => 'required|string|max:255', 'content' => 'required|string', 'picture' => 'nullable|image|max:2048', // Validate that picture is an image ]); $data = $request->only(['title', 'content']); if ($request->hasFile('picture')) { $file = $request->file('picture'); $filename = time() . '_' . $file->getClientOriginalName(); // Store the file in the "public/uploads" directory $path = $file->storeAs('uploads', $filename, 'public'); $data['picture'] = '/storage/' . $path; //php artisan storage:link } Post::create($data); return redirect()->route('posts.index')->with('success', 'Post created successfully.'); } public function update(Request $request, Post $post) { $request->validate([ 'title' => 'required|string|max:255', 'content' => 'required|string', 'picture' => 'nullable|image|max:2048', ]); $data = $request->only(['title', 'content']); if ($request->hasFile('picture')) { $file = $request->file('picture'); $filename = time() . '_' . $file->getClientOriginalName(); $path = $file->storeAs('uploads', $filename, 'public'); $data['picture'] = '/storage/' . $path; } $post->update($data); return redirect()->route('posts.index')->with('success', 'Post updated successfully.'); } public function destroy(Post $post) { $post->delete(); return redirect()->route('posts.index')->with('success', 'Post deleted successfully.'); } }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 App\Models\User; use Inertia\Response; use Inertia\Inertia; class UsersController extends Controller { public function index(): Response { $users = User::select('id', 'name', 'email', 'created_at')->latest()->paginate(10); return Inertia::render('users', [ 'users' => $users, ]); } public function store(Request $request) { $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users,email', ]); User::create($request->only('name', 'email')); return redirect()->route('users.index'); } public function update(Request $request, $id) { $user = User::findOrFail($id); $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users,email,' . $id, ]); $user->update($request->only('name', 'email')); return redirect()->route('users.index'); } public function destroy($id) { User::findOrFail($id)->delete(); return redirect()->route('users.index'); } }Frontend with React and InertiaJS
Add sidebar menu product mainNavItems from reactjs\rescourses\js\components\app-sidebar.tsx
File: pages\Posts.tsx
reactjs\resources\js\pages\Posts.tsx
//reactjs\resources\js\pages\Posts.tsx import AppLayout from '@/layouts/app-layout'; import { Head, router, usePage } from "@inertiajs/react"; import { useState } from "react"; import PostFormModal from "@/components/PostFormModal"; export default function Posts() { const { posts } = usePage<{ posts: { id: number; title: string; content: string; picture?: string }[] }>().props; const [isModalOpen, setIsModalOpen] = useState(false); const [selectedPost, setSelectedPost] = useState(null); const openModal = (post = null) => { setSelectedPost(post); setIsModalOpen(true); }; const handleDelete = (id: number) => { router.delete(`/admin/posts/${id}`, { onSuccess: () => { router.reload(); }, onError: () => { console.error("Failed to delete post."); }, }); }; return ( <AppLayout> <Head title="Posts" /> <div className="container mx-auto p-4"> <div className="flex justify-between items-center mb-4"> <h1 className="text-2xl font-bold">Posts</h1> <button onClick={() => openModal()} className="bg-green-600 text-white rounded px-3 py-1 text-sm hover:bg-green-700 transition"> Add Post </button> </div> <div className="overflow-x-auto"> <table className="min-w-full bg-white shadow 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">Photo</th> <th className="py-2 px-4 text-left border-b">Title</th> <th className="py-2 px-4 text-left border-b">Content</th> <th className="py-2 px-4 text-left border-b">Actions</th> </tr> </thead> <tbody> {posts.length ? ( posts.map((post) => ( <tr key={post.id} className="hover:bg-gray-50 text-black"> <td className="py-2 px-4 border-b">{post.id}</td> <td className="py-2 px-4 border-b"> {post.picture ? <img src={post.picture} alt="Post" className="w-16 h-16 object-cover rounded-full" /> : "No Picture"} </td> <td className="py-2 px-4 border-b">{post.title}</td> <td className="py-2 px-4 border-b">{post.content}</td> <td className="py-2 px-4 border-b"> <button onClick={() => openModal(post)} className="bg-blue-500 text-sm text-white px-3 py-2 rounded">Edit</button> <button onClick={() => handleDelete(post.id)} className="bg-red-500 text-sm text-white px-3 py-2 rounded">Delete</button> </td> </tr> )) ) : ( <tr><td colSpan={4} className="text-center p-4 text-gray-600">No posts found.</td></tr> )} </tbody> </table> </div> </div> <PostFormModal isOpen={isModalOpen} closeModal={() => setIsModalOpen(false)} post={selectedPost} /> </AppLayout> ); }File: pages/users.tsx reactjs\resources\js\pages\users.tsx
//reactjs\resources\js\pages\users.tsx import { useState } from "react"; import AppLayout from "@/layouts/app-layout"; import { Head, router, usePage } from "@inertiajs/react"; import UserFormModal from "@/components/UserFormModal"; export default function Users() { const { users } = usePage<{ users: { data: { id: number; name: string; email: string; created_at: string }[] } }>().props; //console.log(users.data); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const openModal = (user = null) => { setSelectedUser(user); setIsModalOpen(true); }; const handleDelete = (id: number) => { if (confirm("Are you sure you want to delete this user?")) { router.delete(`/admin/users/${id}`); } }; return ( <AppLayout> <Head title="Users" /> <div className="container mx-auto p-4"> <div className="flex justify-between items-center mb-4"> <h1 className="text-2xl font-bold">Users</h1> <button onClick={() => openModal()} className="mb-4 sm:w-auto bg-green-600 text-white rounded-lg px-4 py-2 hover:bg-green-700 transition"> Add User </button> </div> <div className="overflow-x-auto"> <table className="min-w-full bg-white shadow 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-left border-b">Actions</th> </tr> </thead> <tbody> {users.data.length ? ( users.data.map(({ id, name, email, created_at }) => ( <tr key={id} className="hover:bg-gray-50 text-black"> {[id, name, email, new Date(created_at).toLocaleDateString()].map((value, i) => ( <td key={i} className="border p-3 text-gray-700 text-xs sm:text-sm"> {value} </td> ))} <td className="py-2 px-4 border-b"> <button onClick={() => openModal({ id, name, email })} className="w-full sm:w-auto bg-blue-600 text-white rounded-lg px-4 py-2 hover:bg-blue-700" > Edit </button> <button onClick={() => handleDelete(id)} 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> </td> </tr> )) ) : ( <tr> <td colSpan={5} className="text-center p-4 text-gray-600"> No users found. </td> </tr> )} </tbody> </table> </div> </div> <UserFormModal isOpen={isModalOpen} closeModal={() => setIsModalOpen(false)} user={selectedUser} /> </AppLayout> ); }File: components/PostFormModal.tsx reactjs\resources\js\components\PostFormModal.tsx
//reactjs\resources\js\components\PostFormModal.tsx import { Input } from "@headlessui/react"; import { useState, useEffect } from "react"; import { router, usePage } from "@inertiajs/react"; import InputError from '@/components/input-error'; interface Post { id?: number; title: string; content: string; picture?: string; } interface Props { isOpen: boolean; closeModal: () => void; post?: Post | null; } export default function PostFormModal({ isOpen, closeModal, post }: Props) { const [formData, setFormData] = useState<Post>({ title: "", content: "", picture: "" }); const [selectedFile, setSelectedFile] = useState<File | null>(null); const [preview, setPreview] = useState<string>(""); const { errors } = usePage().props; useEffect(() => { if (post) { setFormData({ title: post.title, content: post.content, picture: post.picture || "" }); setPreview(post.picture || ""); setSelectedFile(null); } else { setFormData({ title: "", content: "", picture: "" }); setPreview(""); setSelectedFile(null); } }, [post]); const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { setFormData({ ...formData, [e.target.name]: e.target.value }); }; const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.files && e.target.files[0]) { const file = e.target.files[0]; setSelectedFile(file); setPreview(URL.createObjectURL(file)); } }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const data = new FormData(); data.append("title", formData.title); data.append("content", formData.content); if (selectedFile) { data.append("picture", selectedFile); } if (post?.id) { data.append("_method", "PUT"); router.post(`/admin/posts/${post.id}`, data, { onSuccess: () => { closeModal(); router.reload(); }, onError: (errors) => { console.error(errors.message || "Failed to submit post."); }, }); } else { router.post("/admin/posts", data, { onSuccess: () => { closeModal(); router.reload(); }, onError: (errors) => { console.error(errors.message || "Failed to submit post."); }, }); } }; if (!isOpen) return null; return ( <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"> <div className="bg-white p-6 rounded-lg shadow-lg w-full max-w-xl text-black"> <h2 className="text-lg font-semibold mb-4">Add Post</h2> <form onSubmit={handleSubmit} encType="multipart/form-data"> <div className="mb-3"> <label className="block text-sm font-medium">Title</label> <Input type="text" name="title" value={formData.title} onChange={handleChange} className="w-full border rounded p-2" /> <InputError message={errors.title} /> </div> <div className="mb-3"> <label className="block text-sm font-medium">Content</label> <textarea name="content" value={formData.content} onChange={handleChange} className="w-full border rounded p-2" ></textarea> <InputError message={errors.content} /> </div> <div className="mb-3"> <label className="block text-sm font-medium">Picture (optional)</label> <Input type="file" name="picture" onChange={handleFileChange} className="w-full" accept="image/*" /> <InputError message={errors.picture} /> </div> {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> )} <div className="flex justify-end gap-2"> <button type="button" onClick={closeModal} className="px-4 py-2 bg-gray-500 text-white rounded">Cancel</button> <button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded">{post ? "Update" : "Create"}</button> </div> </form> </div> </div> ); }File: components/UserFormModal.tsx reactjs\resources\js\components\PostFormModal.tsx
//reactjs\resources\js\components\UserFormModal.tsx import { Input } from "@headlessui/react"; import { useState, useEffect } from "react"; import { router, usePage } from "@inertiajs/react"; import InputError from "./input-error"; interface User { id?: number; name: string; email: string; } interface Props { isOpen: boolean; closeModal: () => void; user?: User | null; } export default function UserFormModal({ isOpen, closeModal, user }: Props) { const [formData, setFormData] = useState<User>({ name: "", email: "" }); const { errors } = usePage().props; useEffect(() => { if (user) { setFormData({ name: user.name, email: user.email }); } else { setFormData({ name: "", email: "" }); } }, [user]); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setFormData({ ...formData, [e.target.name]: e.target.value }); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (user?.id) { router.put(`/admin/users/${user.id}`, formData, { onSuccess: closeModal }); } else { router.post("/admin/users", formData, { onSuccess: closeModal }); } }; if (!isOpen) return null; return ( <div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50"> <div className="bg-white p-6 rounded-lg shadow-lg w-full max-w-xl text-black"> <h2 className="text-lg font-semibold mb-4">Add User</h2> <form onSubmit={handleSubmit}> <div className="mb-3"> <label className="block text-sm font-medium">Name</label> <Input type="text" name="name" value={formData.name} onChange={handleChange} className="w-full border rounded p-2" /> <InputError message={errors.name} /> </div> <div className="mb-3"> <label className="block text-sm font-medium">Email</label> <Input type="email" name="email" value={formData.email} onChange={handleChange} className="w-full border rounded p-2" /> <InputError message={errors.email} /> </div> <div className="flex justify-end gap-2"> <button type="button" onClick={closeModal} className="px-4 py-2 bg-gray-500 text-white rounded" > Cancel </button> <button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded"> {user ? "Update" : "Create"} </button> </div> </form> </div> </div> ); }Routes
routes/web.php
//routes/web.php <?php use Illuminate\Support\Facades\Route; use Inertia\Inertia; use App\Http\Controllers\PostController; 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::resource('admin/posts', PostController::class); Route::get('/admin/users', [UsersController::class, 'index'])->name('users.index'); Route::post('/admin/users', [UsersController::class, 'store'])->name('users.store'); Route::put('/admin/users/{id}', [UsersController::class, 'update'])->name('users.update'); Route::delete('/admin/users/{id}', [UsersController::class, 'destroy'])->name('users.destroy'); }); 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