article

Tuesday, April 29, 2025

Laravel 12 React Starter kit CRUD with File Upload Modal Form

Laravel 12 React Starter kit CRUD with File Upload Modal Form



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

Related Post