doo-boilerplate 0.2.2 → 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 +1 -1
- package/templates/template-vite/CLAUDE.md +47 -0
- package/templates/template-vite/src/components/layout/sidebar.tsx +59 -21
- package/templates/template-vite/src/features/tasks/components/task-form-dialog.tsx +98 -0
- package/templates/template-vite/src/features/tasks/components/tasks-crud-table.tsx +117 -0
- package/templates/template-vite/src/features/tasks/components/tasks-table-columns.tsx +108 -0
- package/templates/template-vite/src/features/tasks/data/tasks-mock-data.ts +24 -0
- package/templates/template-vite/src/features/tasks/schemas/task-form-schema.ts +9 -0
- package/templates/template-vite/src/features/tasks/types/task.ts +10 -0
- package/templates/template-vite/src/routes/_authenticated/examples/crud.tsx +15 -0
- package/templates/template-vite/src/routes/_authenticated/examples/modals.tsx +123 -0
- package/templates/template-vite/src/routes/_authenticated/examples/toasts.tsx +47 -0
package/package.json
CHANGED
|
@@ -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
|
|
57
|
+
<nav className='flex-1 overflow-y-auto p-2'>
|
|
51
58
|
<TooltipProvider delayDuration={0}>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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,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
|
+
}
|