New CPM with Laravel 12 and React

This commit is contained in:
Subash
2025-04-02 17:49:09 +05:45
commit ecc3d97909
190 changed files with 27148 additions and 0 deletions

View File

@ -0,0 +1,45 @@
import { DataTableColumnHeaderSimple } from '@/components/data-table-column-header-simple-sort';
import { DataTableColumnHeader } from '@/components/data-table-column-header-sort';
import { Checkbox } from '@/components/ui/checkbox';
import { User } from '@/types';
import { ColumnDef } from '@tanstack/react-table';
import RowActions from './row-actions';
export const columns: ColumnDef<User>[] = [
{
id: 'select',
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox checked={row.getIsSelected()} onCheckedChange={(value) => row.toggleSelected(!!value)} aria-label="Select row" />
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'name',
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="Name" />,
},
{
accessorKey: 'email',
header: ({ column }) => {
return <DataTableColumnHeader column={column} title="Email" />;
},
},
{
accessorKey: 'status',
header: 'Status',
},
{
id: 'actions',
cell: ({ row }) => {
const user = row.original;
return <RowActions user={user} />;
},
},
];

View File

@ -0,0 +1,38 @@
import { DataTable } from '@/components/data-table';
import AppLayout from '@/layouts/app-layout';
import { BreadcrumbItem, User } from '@/types';
import { Head } from '@inertiajs/react';
import { RoleProvider } from '../role-context';
import RoleForm from '../role-form';
import { columns } from './columns';
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Dashboard',
href: '/dashboard',
},
{
title: 'Roles',
href: '/roles',
},
];
const Page = ({ users }: { users: User[] }) => {
return (
<RoleProvider>
<AppLayout breadcrumbs={breadcrumbs}>
<Head title="Roles" />
<div className="flex h-full flex-col gap-4 rounded-xl p-4">
<div className="flex justify-end">
<RoleForm />
</div>
<DataTable columns={columns} data={roles} />
</div>
</AppLayout>
</RoleProvider>
);
};
export default Page;

View File

@ -0,0 +1,46 @@
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { User } from '@/types';
import { MoreHorizontal, PenLine, Trash2, UserX } from 'lucide-react';
import { useUser } from '../user-context';
function RowActions({ user }: { user: User }) {
const { setOpen } = useUser();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem onClick={() => setOpen(true, user)}>
<PenLine className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<UserX className="mr-2 h-4 w-4" />
Deactivate
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
export default RowActions;

View File

@ -0,0 +1,43 @@
import { Role } from '@/types';
import { createContext, ReactNode, use, useState, useTransition } from 'react';
type RoleContextType = {
open: boolean;
isPending: boolean;
setOpen: (state: boolean | false, role: Role | null) => void;
};
const RoleContext = createContext<RoleContextType>({
open: false,
isPending: false,
setOpen: () => {},
});
export const RoleProvider = ({ children }: { children: ReactNode }) => {
const [open, setOpen] = useState(false);
const [isPending, startTransition] = useTransition();
const handleOpen = (state: boolean, role: Role | null = null) => {
startTransition(() => {
setOpen(state);
});
};
return (
<RoleContext.Provider
value={{
open,
isPending,
setOpen: handleOpen,
}}
>
{children}
</RoleContext.Provider>
);
};
export const useRole = () => {
const context = use(RoleContext);
if (!context) throw new Error('useRole must be used within a RoleProvider');
return context;
};

View File

@ -0,0 +1,145 @@
import { useForm } from '@inertiajs/react';
import { LoaderCircle, Plus } from 'lucide-react';
import { FormEventHandler, useEffect } from 'react' ;
import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { toast } from 'sonner';
import { useRole } from './role-context';
type RoleForm = {
name: string;
email: string;
password?: string;
};
interface RoleFormProps {
status?: string;
}
export default function RoleForm({ status }: RoleFormProps) {
const { open, setOpen } = useRole();
const { data, setData, post, put, processing, errors, reset } = useForm<Required<RoleForm>>({
name: '',
email: '',
password: '',
});
useEffect(() => {
if (open && selectedUser) {
setData({
name: selectedUser?.name || '',
email: selectedUser?.email || '',
password: '',
});
} else {
reset('name', 'email', 'password');
}
}, [open, selectedUser, setData, reset]);
const submit: FormEventHandler = (e) => {
e.preventDefault();
const method = selectedUser ? put : post;
const routeName = selectedUser ? 'user.update' : 'user.store';
method(route(routeName, selectedUser?.id), {
preserveScroll: true,
onSuccess: () => {
reset('name', 'email', 'password');
setOpen(false, null);
toast.success(`User successfully ${selectedUser ? 'updated' : 'created'}.`, {
description: new Date().toLocaleDateString(undefined, {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
}),
});
},
});
};
return (
<Dialog open={open} onOpenChange={() => setOpen(!open, null)}>
<DialogTrigger asChild>
<Button>
Create
<Plus className="ml-2 h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{selectedUser ? 'Edit User' : 'Create User'}</DialogTitle>
<DialogDescription>{selectedUser ? 'Update the user details here.' : 'Create a new user here.'}</DialogDescription>
</DialogHeader>
<form className="flex flex-col gap-6" onSubmit={submit}>
<div className="grid gap-6">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="name"
required
autoFocus
tabIndex={1}
autoComplete="name"
value={data.name}
onChange={(e) => setData('name', e.target.value)}
placeholder="Full Name"
/>
<InputError message={errors.name} />
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email address</Label>
<Input
id="email"
type="email"
required
autoFocus
tabIndex={1}
autoComplete="new-email"
value={data.email}
onChange={(e) => setData('email', e.target.value)}
placeholder="Email Address"
/>
<InputError message={errors.email} />
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
required={!selectedUser}
tabIndex={2}
autoComplete="new-password"
value={data.password}
onChange={(e) => setData('password', e.target.value)}
placeholder="Password"
/>
<InputError message={errors.password} />
</div>
<div className="mt-4 flex justify-end space-x-2">
<Button type="button" variant="outline" onClick={() => setOpen(false, null)}>
Cancel
</Button>
<Button type="submit" tabIndex={4} disabled={processing}>
{processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
{selectedUser ? 'Update' : 'Create'}
</Button>
</div>
</div>
</form>
{status && <div className="mb-4 text-center text-sm font-medium text-green-600">{status}</div>}
</DialogContent>
</Dialog>
);
}