New CPM with Laravel 12 and React
This commit is contained in:
45
resources/js/pages/roles/index/columns.tsx
Normal file
45
resources/js/pages/roles/index/columns.tsx
Normal 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} />;
|
||||
},
|
||||
},
|
||||
];
|
38
resources/js/pages/roles/index/page.tsx
Normal file
38
resources/js/pages/roles/index/page.tsx
Normal 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;
|
||||
|
||||
|
46
resources/js/pages/roles/index/row-actions.tsx
Normal file
46
resources/js/pages/roles/index/row-actions.tsx
Normal 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;
|
43
resources/js/pages/roles/role-context.tsx
Normal file
43
resources/js/pages/roles/role-context.tsx
Normal 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;
|
||||
};
|
145
resources/js/pages/roles/role-form.tsx
Normal file
145
resources/js/pages/roles/role-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user