doo-boilerplate 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doo-boilerplate",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "CLI to scaffold Pila portal frontend projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,47 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code when working in this project.
4
+
5
+ ## Tech Stack
6
+ - React 19, TypeScript, Vite 8
7
+ - TanStack Router (file-based routing) — add routes in `src/routes/`
8
+ - TanStack Query — data fetching via `useQuery`/`useMutation`
9
+ - TanStack Table — data tables via `src/components/data-table/`
10
+ - Tailwind CSS 4 + shadcn/ui components in `src/components/ui/`
11
+ - Zustand — global state in `src/stores/`
12
+ - react-hook-form + zod — forms with validation
13
+ - Sentry — error tracking (set `VITE_SENTRY_DSN` in `.env`)
14
+
15
+ ## Project Structure
16
+ - `src/routes/` — file-based routes (TanStack Router auto-generates `routeTree.gen.ts`)
17
+ - `src/features/` — feature modules (each has components/, hooks/, types/, schemas/, data/)
18
+ - `src/components/ui/` — shadcn/ui components
19
+ - `src/components/data-table/` — reusable DataTable components
20
+ - `src/components/layout/` — Sidebar, Header, PageLayout
21
+ - `src/lib/` — utilities (api-client, query-client, sentry, utils)
22
+ - `src/stores/` — Zustand stores
23
+
24
+ ## Development Commands
25
+ ```bash
26
+ pnpm dev # start dev server
27
+ pnpm build # build for production
28
+ pnpm type-check # TypeScript check
29
+ pnpm lint # ESLint
30
+ ```
31
+
32
+ ## Adding a New Feature Module
33
+ 1. Create `src/features/{name}/` with: types/, schemas/, components/, data/
34
+ 2. Add route at `src/routes/_authenticated/{name}.tsx`
35
+ 3. Add nav item in `src/components/layout/sidebar.tsx`
36
+ 4. Use `DataTable` from `src/components/data-table/data-table.tsx` for list views
37
+
38
+ ## Adding New Routes
39
+ Routes are auto-discovered. Create a file in `src/routes/` and run `pnpm dev` to regenerate `routeTree.gen.ts`.
40
+
41
+ ## API Integration
42
+ Replace mock data in `src/features/*/data/` with real API calls using `src/lib/api-client.ts` (axios).
43
+ Use TanStack Query hooks for data fetching.
44
+
45
+ ## Environment Variables
46
+ Copy `.env.example` to `.env` and fill in values.
47
+ `VITE_SENTRY_DSN` — Sentry DSN for error tracking (optional)
@@ -1,9 +1,10 @@
1
1
  import { useState } from 'react'
2
2
  import { Link } from '@tanstack/react-router'
3
- import { LayoutDashboard, Settings, User, Users, ChevronLeft, ChevronRight } from 'lucide-react'
3
+ import { Bell, LayoutDashboard, LayoutTemplate, Settings, Table2, User, Users, ChevronLeft, ChevronRight } from 'lucide-react'
4
4
 
5
5
  import { cn } from '@/lib/utils'
6
6
  import { Button } from '@/components/ui/button'
7
+ import { Separator } from '@/components/ui/separator'
7
8
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
8
9
  import { siteConfig } from '@/config/site'
9
10
 
@@ -14,6 +15,12 @@ const navItems = [
14
15
  { to: '/settings', label: 'Settings', icon: Settings },
15
16
  ] as const
16
17
 
18
+ const exampleItems = [
19
+ { to: '/examples/toasts', label: 'Toasts', icon: Bell },
20
+ { to: '/examples/modals', label: 'Modals', icon: LayoutTemplate },
21
+ { to: '/examples/crud', label: 'CRUD', icon: Table2 },
22
+ ] as const
23
+
17
24
  export function Sidebar() {
18
25
  const [collapsed, setCollapsed] = useState(false)
19
26
 
@@ -47,27 +54,58 @@ export function Sidebar() {
47
54
  </div>
48
55
 
49
56
  {/* Navigation */}
50
- <nav className='flex-1 space-y-1 p-2'>
57
+ <nav className='flex-1 overflow-y-auto p-2'>
51
58
  <TooltipProvider delayDuration={0}>
52
- {navItems.map(({ to, label, icon: Icon }) => (
53
- <Tooltip key={to} disableHoverableContent={!collapsed}>
54
- <TooltipTrigger asChild>
55
- <Link
56
- to={to}
57
- className={cn(
58
- 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-sidebar-foreground',
59
- 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
60
- '[&.active]:bg-sidebar-accent [&.active]:text-sidebar-accent-foreground',
61
- collapsed && 'justify-center px-2'
62
- )}
63
- >
64
- <Icon className='h-4 w-4 shrink-0' />
65
- {!collapsed && <span>{label}</span>}
66
- </Link>
67
- </TooltipTrigger>
68
- {collapsed && <TooltipContent side='right'>{label}</TooltipContent>}
69
- </Tooltip>
70
- ))}
59
+ <div className='space-y-1'>
60
+ {navItems.map(({ to, label, icon: Icon }) => (
61
+ <Tooltip key={to} disableHoverableContent={!collapsed}>
62
+ <TooltipTrigger asChild>
63
+ <Link
64
+ to={to}
65
+ className={cn(
66
+ 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-sidebar-foreground',
67
+ 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
68
+ '[&.active]:bg-sidebar-accent [&.active]:text-sidebar-accent-foreground',
69
+ collapsed && 'justify-center px-2'
70
+ )}
71
+ >
72
+ <Icon className='h-4 w-4 shrink-0' />
73
+ {!collapsed && <span>{label}</span>}
74
+ </Link>
75
+ </TooltipTrigger>
76
+ {collapsed && <TooltipContent side='right'>{label}</TooltipContent>}
77
+ </Tooltip>
78
+ ))}
79
+ </div>
80
+
81
+ {/* Examples group */}
82
+ <Separator className='my-3' />
83
+ {!collapsed && (
84
+ <p className='mb-1 px-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground'>
85
+ Examples
86
+ </p>
87
+ )}
88
+ <div className='space-y-1'>
89
+ {exampleItems.map(({ to, label, icon: Icon }) => (
90
+ <Tooltip key={to} disableHoverableContent={!collapsed}>
91
+ <TooltipTrigger asChild>
92
+ <Link
93
+ to={to}
94
+ className={cn(
95
+ 'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-sidebar-foreground',
96
+ 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
97
+ '[&.active]:bg-sidebar-accent [&.active]:text-sidebar-accent-foreground',
98
+ collapsed && 'justify-center px-2'
99
+ )}
100
+ >
101
+ <Icon className='h-4 w-4 shrink-0' />
102
+ {!collapsed && <span>{label}</span>}
103
+ </Link>
104
+ </TooltipTrigger>
105
+ {collapsed && <TooltipContent side='right'>{label}</TooltipContent>}
106
+ </Tooltip>
107
+ ))}
108
+ </div>
71
109
  </TooltipProvider>
72
110
  </nav>
73
111
  </aside>
@@ -0,0 +1,98 @@
1
+ import { useEffect } from 'react'
2
+ import { useForm } from 'react-hook-form'
3
+ import { zodResolver } from '@hookform/resolvers/zod'
4
+ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
5
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
6
+ import { Input } from '@/components/ui/input'
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
8
+ import { Button } from '@/components/ui/button'
9
+ import { taskFormSchema, type TaskFormValues } from '../schemas/task-form-schema'
10
+ import type { Task } from '../types/task'
11
+
12
+ interface TaskFormDialogProps {
13
+ open: boolean
14
+ onOpenChange: (open: boolean) => void
15
+ /** Existing task to edit; undefined = create mode */
16
+ task?: Task
17
+ onSubmit: (values: TaskFormValues) => void
18
+ }
19
+
20
+ /** Dialog for creating or editing a task */
21
+ export function TaskFormDialog({ open, onOpenChange, task, onSubmit }: TaskFormDialogProps) {
22
+ const form = useForm<TaskFormValues>({
23
+ resolver: zodResolver(taskFormSchema),
24
+ defaultValues: { title: '', status: 'todo', priority: 'medium' },
25
+ })
26
+
27
+ // Populate form when editing
28
+ useEffect(() => {
29
+ if (task) {
30
+ form.reset({ title: task.title, status: task.status, priority: task.priority })
31
+ } else {
32
+ form.reset({ title: '', status: 'todo', priority: 'medium' })
33
+ }
34
+ }, [task, form])
35
+
36
+ const handleSubmit = (values: TaskFormValues) => {
37
+ onSubmit(values)
38
+ onOpenChange(false)
39
+ }
40
+
41
+ return (
42
+ <Dialog open={open} onOpenChange={onOpenChange}>
43
+ <DialogContent>
44
+ <DialogHeader>
45
+ <DialogTitle>{task ? 'Edit Task' : 'Create Task'}</DialogTitle>
46
+ </DialogHeader>
47
+ <Form {...form}>
48
+ <form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-4'>
49
+ <FormField control={form.control} name='title' render={({ field }) => (
50
+ <FormItem>
51
+ <FormLabel>Title</FormLabel>
52
+ <FormControl><Input placeholder='Task title...' {...field} /></FormControl>
53
+ <FormMessage />
54
+ </FormItem>
55
+ )} />
56
+ <FormField control={form.control} name='status' render={({ field }) => (
57
+ <FormItem>
58
+ <FormLabel>Status</FormLabel>
59
+ <Select onValueChange={field.onChange} value={field.value}>
60
+ <FormControl>
61
+ <SelectTrigger><SelectValue /></SelectTrigger>
62
+ </FormControl>
63
+ <SelectContent>
64
+ <SelectItem value='todo'>Todo</SelectItem>
65
+ <SelectItem value='in-progress'>In Progress</SelectItem>
66
+ <SelectItem value='done'>Done</SelectItem>
67
+ <SelectItem value='cancelled'>Cancelled</SelectItem>
68
+ </SelectContent>
69
+ </Select>
70
+ <FormMessage />
71
+ </FormItem>
72
+ )} />
73
+ <FormField control={form.control} name='priority' render={({ field }) => (
74
+ <FormItem>
75
+ <FormLabel>Priority</FormLabel>
76
+ <Select onValueChange={field.onChange} value={field.value}>
77
+ <FormControl>
78
+ <SelectTrigger><SelectValue /></SelectTrigger>
79
+ </FormControl>
80
+ <SelectContent>
81
+ <SelectItem value='low'>Low</SelectItem>
82
+ <SelectItem value='medium'>Medium</SelectItem>
83
+ <SelectItem value='high'>High</SelectItem>
84
+ </SelectContent>
85
+ </Select>
86
+ <FormMessage />
87
+ </FormItem>
88
+ )} />
89
+ <DialogFooter>
90
+ <Button type='button' variant='outline' onClick={() => onOpenChange(false)}>Cancel</Button>
91
+ <Button type='submit'>{task ? 'Save' : 'Create'}</Button>
92
+ </DialogFooter>
93
+ </form>
94
+ </Form>
95
+ </DialogContent>
96
+ </Dialog>
97
+ )
98
+ }
@@ -0,0 +1,117 @@
1
+ import { useState } from 'react'
2
+ import {
3
+ useReactTable,
4
+ getCoreRowModel,
5
+ getPaginationRowModel,
6
+ getSortedRowModel,
7
+ getFilteredRowModel,
8
+ type SortingState,
9
+ type ColumnFiltersState,
10
+ type VisibilityState,
11
+ } from '@tanstack/react-table'
12
+ import { Plus } from 'lucide-react'
13
+ import { toast } from 'sonner'
14
+ import { Button } from '@/components/ui/button'
15
+ import { DataTable } from '@/components/data-table/data-table'
16
+ import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'
17
+ import { tasksMockData } from '../data/tasks-mock-data'
18
+ import { getTaskColumns } from './tasks-table-columns'
19
+ import { TaskFormDialog } from './task-form-dialog'
20
+ import type { Task } from '../types/task'
21
+ import type { TaskFormValues } from '../schemas/task-form-schema'
22
+
23
+ const STATUS_FILTER_OPTIONS = [
24
+ { label: 'Todo', value: 'todo' },
25
+ { label: 'In Progress', value: 'in-progress' },
26
+ { label: 'Done', value: 'done' },
27
+ { label: 'Cancelled', value: 'cancelled' },
28
+ ]
29
+
30
+ const PRIORITY_FILTER_OPTIONS = [
31
+ { label: 'Low', value: 'low' },
32
+ { label: 'Medium', value: 'medium' },
33
+ { label: 'High', value: 'high' },
34
+ ]
35
+
36
+ /** Full CRUD data table for tasks using local state */
37
+ export function TasksCrudTable() {
38
+ const [tasks, setTasks] = useState<Task[]>(tasksMockData)
39
+ const [sorting, setSorting] = useState<SortingState>([])
40
+ const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
41
+ const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
42
+ const [rowSelection, setRowSelection] = useState({})
43
+ const [dialogOpen, setDialogOpen] = useState(false)
44
+ const [editingTask, setEditingTask] = useState<Task | undefined>()
45
+
46
+ const handleEdit = (task: Task) => {
47
+ setEditingTask(task)
48
+ setDialogOpen(true)
49
+ }
50
+
51
+ const handleDelete = (id: string) => {
52
+ setTasks((prev) => prev.filter((t) => t.id !== id))
53
+ toast.success('Task deleted')
54
+ }
55
+
56
+ const handleSubmit = (values: TaskFormValues) => {
57
+ if (editingTask) {
58
+ setTasks((prev) => prev.map((t) => t.id === editingTask.id ? { ...t, ...values } : t))
59
+ toast.success('Task updated')
60
+ } else {
61
+ const newTask: Task = {
62
+ id: String(Date.now()),
63
+ ...values,
64
+ createdAt: new Date().toISOString().slice(0, 10),
65
+ }
66
+ setTasks((prev) => [newTask, ...prev])
67
+ toast.success('Task created')
68
+ }
69
+ setEditingTask(undefined)
70
+ }
71
+
72
+ const columns = getTaskColumns({ onEdit: handleEdit, onDelete: handleDelete })
73
+
74
+ const table = useReactTable({
75
+ data: tasks,
76
+ columns,
77
+ state: { sorting, columnFilters, columnVisibility, rowSelection },
78
+ onSortingChange: setSorting,
79
+ onColumnFiltersChange: setColumnFilters,
80
+ onColumnVisibilityChange: setColumnVisibility,
81
+ onRowSelectionChange: setRowSelection,
82
+ getCoreRowModel: getCoreRowModel(),
83
+ getPaginationRowModel: getPaginationRowModel(),
84
+ getSortedRowModel: getSortedRowModel(),
85
+ getFilteredRowModel: getFilteredRowModel(),
86
+ })
87
+
88
+ return (
89
+ <div className='space-y-4'>
90
+ <div className='flex items-center justify-between'>
91
+ <DataTableToolbar
92
+ table={table}
93
+ searchColumn='title'
94
+ searchPlaceholder='Search tasks...'
95
+ filters={[
96
+ { columnId: 'status', title: 'Status', options: STATUS_FILTER_OPTIONS },
97
+ { columnId: 'priority', title: 'Priority', options: PRIORITY_FILTER_OPTIONS },
98
+ ]}
99
+ />
100
+ <Button
101
+ size='sm'
102
+ className='ml-2'
103
+ onClick={() => { setEditingTask(undefined); setDialogOpen(true) }}
104
+ >
105
+ <Plus className='mr-1 h-4 w-4' /> Add Task
106
+ </Button>
107
+ </div>
108
+ <DataTable table={table} columns={columns} />
109
+ <TaskFormDialog
110
+ open={dialogOpen}
111
+ onOpenChange={setDialogOpen}
112
+ task={editingTask}
113
+ onSubmit={handleSubmit}
114
+ />
115
+ </div>
116
+ )
117
+ }
@@ -0,0 +1,108 @@
1
+ import { type ColumnDef } from '@tanstack/react-table'
2
+ import { MoreHorizontal } from 'lucide-react'
3
+ import { Badge } from '@/components/ui/badge'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Checkbox } from '@/components/ui/checkbox'
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuTrigger,
11
+ } from '@/components/ui/dropdown-menu'
12
+ import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header'
13
+ import type { Task } from '../types/task'
14
+
15
+ /** Maps task status to badge variant */
16
+ const statusVariant: Record<Task['status'], 'success' | 'warning' | 'secondary' | 'destructive'> = {
17
+ done: 'success',
18
+ 'in-progress': 'warning',
19
+ todo: 'secondary',
20
+ cancelled: 'destructive',
21
+ }
22
+
23
+ /** Maps task priority to badge variant */
24
+ const priorityVariant: Record<Task['priority'], 'destructive' | 'warning' | 'secondary'> = {
25
+ high: 'destructive',
26
+ medium: 'warning',
27
+ low: 'secondary',
28
+ }
29
+
30
+ interface ActionsCallbacks {
31
+ onEdit: (task: Task) => void
32
+ onDelete: (id: string) => void
33
+ }
34
+
35
+ export function getTaskColumns({ onEdit, onDelete }: ActionsCallbacks): ColumnDef<Task>[] {
36
+ return [
37
+ {
38
+ id: 'select',
39
+ header: ({ table }) => (
40
+ <Checkbox
41
+ checked={table.getIsAllPageRowsSelected()}
42
+ onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
43
+ aria-label='Select all'
44
+ />
45
+ ),
46
+ cell: ({ row }) => (
47
+ <Checkbox
48
+ checked={row.getIsSelected()}
49
+ onCheckedChange={(v) => row.toggleSelected(!!v)}
50
+ aria-label='Select row'
51
+ />
52
+ ),
53
+ enableSorting: false,
54
+ enableHiding: false,
55
+ },
56
+ {
57
+ accessorKey: 'title',
58
+ header: ({ column }) => <DataTableColumnHeader column={column} title='Title' />,
59
+ filterFn: 'includesString',
60
+ },
61
+ {
62
+ accessorKey: 'status',
63
+ header: ({ column }) => <DataTableColumnHeader column={column} title='Status' />,
64
+ cell: ({ row }) => {
65
+ const status = row.getValue<Task['status']>('status')
66
+ return <Badge variant={statusVariant[status]}>{status}</Badge>
67
+ },
68
+ filterFn: (row, id, filterValues: string[]) =>
69
+ filterValues.includes(row.getValue(id)),
70
+ },
71
+ {
72
+ accessorKey: 'priority',
73
+ header: ({ column }) => <DataTableColumnHeader column={column} title='Priority' />,
74
+ cell: ({ row }) => {
75
+ const priority = row.getValue<Task['priority']>('priority')
76
+ return <Badge variant={priorityVariant[priority]}>{priority}</Badge>
77
+ },
78
+ filterFn: (row, id, filterValues: string[]) =>
79
+ filterValues.includes(row.getValue(id)),
80
+ },
81
+ {
82
+ accessorKey: 'createdAt',
83
+ header: ({ column }) => <DataTableColumnHeader column={column} title='Created' />,
84
+ },
85
+ {
86
+ id: 'actions',
87
+ cell: ({ row }) => (
88
+ <DropdownMenu>
89
+ <DropdownMenuTrigger asChild>
90
+ <Button variant='ghost' className='h-8 w-8 p-0'>
91
+ <span className='sr-only'>Open menu</span>
92
+ <MoreHorizontal className='h-4 w-4' />
93
+ </Button>
94
+ </DropdownMenuTrigger>
95
+ <DropdownMenuContent align='end'>
96
+ <DropdownMenuItem onClick={() => onEdit(row.original)}>Edit</DropdownMenuItem>
97
+ <DropdownMenuItem
98
+ onClick={() => onDelete(row.original.id)}
99
+ className='text-destructive'
100
+ >
101
+ Delete
102
+ </DropdownMenuItem>
103
+ </DropdownMenuContent>
104
+ </DropdownMenu>
105
+ ),
106
+ },
107
+ ]
108
+ }
@@ -0,0 +1,24 @@
1
+ import type { Task } from '../types/task'
2
+
3
+ export const tasksMockData: Task[] = [
4
+ { id: '1', title: 'Set up project structure', status: 'done', priority: 'high', createdAt: '2024-01-01' },
5
+ { id: '2', title: 'Design database schema', status: 'done', priority: 'high', createdAt: '2024-01-02' },
6
+ { id: '3', title: 'Implement authentication', status: 'done', priority: 'high', createdAt: '2024-01-03' },
7
+ { id: '4', title: 'Build user profile page', status: 'in-progress', priority: 'medium', createdAt: '2024-01-04' },
8
+ { id: '5', title: 'Add email notifications', status: 'in-progress', priority: 'medium', createdAt: '2024-01-05' },
9
+ { id: '6', title: 'Write unit tests', status: 'todo', priority: 'high', createdAt: '2024-01-06' },
10
+ { id: '7', title: 'Set up CI/CD pipeline', status: 'todo', priority: 'medium', createdAt: '2024-01-07' },
11
+ { id: '8', title: 'Optimize database queries', status: 'todo', priority: 'low', createdAt: '2024-01-08' },
12
+ { id: '9', title: 'Add dark mode support', status: 'todo', priority: 'low', createdAt: '2024-01-09' },
13
+ { id: '10', title: 'Implement search functionality', status: 'in-progress', priority: 'high', createdAt: '2024-01-10' },
14
+ { id: '11', title: 'Create onboarding flow', status: 'todo', priority: 'medium', createdAt: '2024-01-11' },
15
+ { id: '12', title: 'Add export to CSV feature', status: 'cancelled', priority: 'low', createdAt: '2024-01-12' },
16
+ { id: '13', title: 'Implement role-based access', status: 'in-progress', priority: 'high', createdAt: '2024-01-13' },
17
+ { id: '14', title: 'Write API documentation', status: 'todo', priority: 'medium', createdAt: '2024-01-14' },
18
+ { id: '15', title: 'Add analytics dashboard', status: 'todo', priority: 'medium', createdAt: '2024-01-15' },
19
+ { id: '16', title: 'Fix pagination bug', status: 'done', priority: 'high', createdAt: '2024-01-16' },
20
+ { id: '17', title: 'Improve error messages', status: 'todo', priority: 'low', createdAt: '2024-01-17' },
21
+ { id: '18', title: 'Add keyboard shortcuts', status: 'cancelled', priority: 'low', createdAt: '2024-01-18' },
22
+ { id: '19', title: 'Performance audit', status: 'todo', priority: 'medium', createdAt: '2024-01-19' },
23
+ { id: '20', title: 'Mobile responsive fixes', status: 'in-progress', priority: 'high', createdAt: '2024-01-20' },
24
+ ]
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod'
2
+
3
+ export const taskFormSchema = z.object({
4
+ title: z.string().min(2, 'Title must be at least 2 characters'),
5
+ status: z.enum(['todo', 'in-progress', 'done', 'cancelled']),
6
+ priority: z.enum(['low', 'medium', 'high']),
7
+ })
8
+
9
+ export type TaskFormValues = z.infer<typeof taskFormSchema>
@@ -0,0 +1,10 @@
1
+ export type TaskStatus = 'todo' | 'in-progress' | 'done' | 'cancelled'
2
+ export type TaskPriority = 'low' | 'medium' | 'high'
3
+
4
+ export interface Task {
5
+ id: string
6
+ title: string
7
+ status: TaskStatus
8
+ priority: TaskPriority
9
+ createdAt: string
10
+ }
@@ -0,0 +1,15 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { PageLayout } from '@/components/layout/page-layout'
3
+ import { TasksCrudTable } from '@/features/tasks/components/tasks-crud-table'
4
+
5
+ export const Route = createFileRoute('/_authenticated/examples/crud')({
6
+ component: CrudPage,
7
+ })
8
+
9
+ function CrudPage() {
10
+ return (
11
+ <PageLayout title='CRUD Example' description='Full Create, Read, Update, Delete with DataTable'>
12
+ <TasksCrudTable />
13
+ </PageLayout>
14
+ )
15
+ }
@@ -0,0 +1,123 @@
1
+ import { useState } from 'react'
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import { toast } from 'sonner'
4
+ import { useForm } from 'react-hook-form'
5
+ import { zodResolver } from '@hookform/resolvers/zod'
6
+ import { z } from 'zod'
7
+ import { PageLayout } from '@/components/layout/page-layout'
8
+ import { Button } from '@/components/ui/button'
9
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
10
+ import {
11
+ AlertDialog,
12
+ AlertDialogAction,
13
+ AlertDialogCancel,
14
+ AlertDialogContent,
15
+ AlertDialogDescription,
16
+ AlertDialogFooter,
17
+ AlertDialogHeader,
18
+ AlertDialogTitle,
19
+ } from '@/components/ui/alert-dialog'
20
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
21
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
22
+ import { Input } from '@/components/ui/input'
23
+
24
+ export const Route = createFileRoute('/_authenticated/examples/modals')({
25
+ component: ModalsPage,
26
+ })
27
+
28
+ const formSchema = z.object({
29
+ name: z.string().min(2, 'Name must be at least 2 characters'),
30
+ email: z.string().email('Invalid email address'),
31
+ })
32
+ type FormValues = z.infer<typeof formSchema>
33
+
34
+ function ModalsPage() {
35
+ const [confirmOpen, setConfirmOpen] = useState(false)
36
+ const [formOpen, setFormOpen] = useState(false)
37
+
38
+ const form = useForm<FormValues>({
39
+ resolver: zodResolver(formSchema),
40
+ defaultValues: { name: '', email: '' },
41
+ })
42
+
43
+ const onSubmit = (values: FormValues) => {
44
+ toast.success(`Submitted: ${values.name} (${values.email})`)
45
+ form.reset()
46
+ setFormOpen(false)
47
+ }
48
+
49
+ return (
50
+ <PageLayout title='Modals & Dialogs' description='Alert dialogs and form modal examples'>
51
+ <div className='grid gap-6 md:grid-cols-2'>
52
+ <Card>
53
+ <CardHeader>
54
+ <CardTitle>Confirm Dialog</CardTitle>
55
+ <CardDescription>Destructive action with confirmation</CardDescription>
56
+ </CardHeader>
57
+ <CardContent>
58
+ <Button variant='destructive' onClick={() => setConfirmOpen(true)}>Delete Item</Button>
59
+ </CardContent>
60
+ </Card>
61
+
62
+ <Card>
63
+ <CardHeader>
64
+ <CardTitle>Form Dialog</CardTitle>
65
+ <CardDescription>Modal with react-hook-form + zod validation</CardDescription>
66
+ </CardHeader>
67
+ <CardContent>
68
+ <Button onClick={() => setFormOpen(true)}>Open Form</Button>
69
+ </CardContent>
70
+ </Card>
71
+ </div>
72
+
73
+ <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
74
+ <AlertDialogContent>
75
+ <AlertDialogHeader>
76
+ <AlertDialogTitle>Are you sure?</AlertDialogTitle>
77
+ <AlertDialogDescription>This action cannot be undone. This will permanently delete the item.</AlertDialogDescription>
78
+ </AlertDialogHeader>
79
+ <AlertDialogFooter>
80
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
81
+ <AlertDialogAction
82
+ onClick={() => { setConfirmOpen(false); toast.success('Item deleted') }}
83
+ className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
84
+ >
85
+ Delete
86
+ </AlertDialogAction>
87
+ </AlertDialogFooter>
88
+ </AlertDialogContent>
89
+ </AlertDialog>
90
+
91
+ <Dialog open={formOpen} onOpenChange={setFormOpen}>
92
+ <DialogContent>
93
+ <DialogHeader>
94
+ <DialogTitle>Create Item</DialogTitle>
95
+ <DialogDescription>Fill in the details below to create a new item.</DialogDescription>
96
+ </DialogHeader>
97
+ <Form {...form}>
98
+ <form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
99
+ <FormField control={form.control} name='name' render={({ field }) => (
100
+ <FormItem>
101
+ <FormLabel>Name</FormLabel>
102
+ <FormControl><Input placeholder='John Doe' {...field} /></FormControl>
103
+ <FormMessage />
104
+ </FormItem>
105
+ )} />
106
+ <FormField control={form.control} name='email' render={({ field }) => (
107
+ <FormItem>
108
+ <FormLabel>Email</FormLabel>
109
+ <FormControl><Input placeholder='john@example.com' type='email' {...field} /></FormControl>
110
+ <FormMessage />
111
+ </FormItem>
112
+ )} />
113
+ <DialogFooter>
114
+ <Button type='button' variant='outline' onClick={() => setFormOpen(false)}>Cancel</Button>
115
+ <Button type='submit'>Submit</Button>
116
+ </DialogFooter>
117
+ </form>
118
+ </Form>
119
+ </DialogContent>
120
+ </Dialog>
121
+ </PageLayout>
122
+ )
123
+ }
@@ -0,0 +1,47 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { toast } from 'sonner'
3
+ import { PageLayout } from '@/components/layout/page-layout'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
6
+
7
+ export const Route = createFileRoute('/_authenticated/examples/toasts')({
8
+ component: ToastsPage,
9
+ })
10
+
11
+ function ToastsPage() {
12
+ const showPromise = () => {
13
+ toast.promise(new Promise((resolve) => setTimeout(resolve, 2000)), {
14
+ loading: 'Loading...',
15
+ success: 'Promise resolved!',
16
+ error: 'Promise rejected',
17
+ })
18
+ }
19
+
20
+ return (
21
+ <PageLayout title='Toast Notifications' description='Interactive toast notification examples using Sonner'>
22
+ <Card>
23
+ <CardHeader>
24
+ <CardTitle>Toast Types</CardTitle>
25
+ <CardDescription>Click buttons to trigger different toast variants</CardDescription>
26
+ </CardHeader>
27
+ <CardContent className='flex flex-wrap gap-3'>
28
+ <Button onClick={() => toast.success('Operation completed successfully!')}>Success</Button>
29
+ <Button variant='destructive' onClick={() => toast.error('Something went wrong!')}>Error</Button>
30
+ <Button variant='outline' onClick={() => toast.warning('Proceed with caution')}>Warning</Button>
31
+ <Button variant='outline' onClick={() => toast.info('Here is some information')}>Info</Button>
32
+ <Button variant='outline' onClick={showPromise}>Promise</Button>
33
+ <Button
34
+ variant='secondary'
35
+ onClick={() =>
36
+ toast('Event created', {
37
+ action: { label: 'Undo', onClick: () => toast.info('Undone!') },
38
+ })
39
+ }
40
+ >
41
+ With Action
42
+ </Button>
43
+ </CardContent>
44
+ </Card>
45
+ </PageLayout>
46
+ )
47
+ }