doo-boilerplate 0.2.6 → 0.2.7
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/_package.json +5 -1
- package/templates/template-vite/src/components/data-table/data-table-drag-handle.tsx +21 -0
- package/templates/template-vite/src/components/data-table/data-table-sortable-header.tsx +35 -0
- package/templates/template-vite/src/components/data-table/data-table.tsx +97 -47
- package/templates/template-vite/src/features/auth/components/sign-in-form.tsx +18 -4
- package/templates/template-vite/src/routes/(auth)/sign-in.tsx +19 -3
package/package.json
CHANGED
|
@@ -58,7 +58,11 @@
|
|
|
58
58
|
"@radix-ui/react-tabs": "^1.1.3",
|
|
59
59
|
"@radix-ui/react-tooltip": "^1.1.8",
|
|
60
60
|
"cmdk": "^1.1.1",
|
|
61
|
-
"recharts": "^2.15.0"
|
|
61
|
+
"recharts": "^2.15.0",
|
|
62
|
+
"@dnd-kit/core": "^6.3.1",
|
|
63
|
+
"@dnd-kit/sortable": "^9.0.0",
|
|
64
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
65
|
+
"@dnd-kit/modifiers": "^9.0.0"
|
|
62
66
|
},
|
|
63
67
|
"devDependencies": {
|
|
64
68
|
"@types/react": "^19",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { GripVertical } from 'lucide-react'
|
|
2
|
+
import { useSortable } from '@dnd-kit/sortable'
|
|
3
|
+
|
|
4
|
+
interface DragHandleProps {
|
|
5
|
+
id: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Drag handle icon for sortable column headers */
|
|
9
|
+
export function ColumnDragHandle({ id }: DragHandleProps) {
|
|
10
|
+
const { attributes, listeners } = useSortable({ id })
|
|
11
|
+
return (
|
|
12
|
+
<button
|
|
13
|
+
{...attributes}
|
|
14
|
+
{...listeners}
|
|
15
|
+
className='cursor-grab opacity-40 hover:opacity-100 active:cursor-grabbing'
|
|
16
|
+
aria-label='Drag to reorder column'
|
|
17
|
+
>
|
|
18
|
+
<GripVertical className='h-4 w-4' />
|
|
19
|
+
</button>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useSortable } from '@dnd-kit/sortable'
|
|
2
|
+
import { CSS } from '@dnd-kit/utilities'
|
|
3
|
+
import { type Header, flexRender } from '@tanstack/react-table'
|
|
4
|
+
|
|
5
|
+
import { TableHead } from '@/components/ui/table'
|
|
6
|
+
|
|
7
|
+
import { ColumnDragHandle } from './data-table-drag-handle'
|
|
8
|
+
|
|
9
|
+
interface SortableHeaderProps<TData, TValue> {
|
|
10
|
+
header: Header<TData, TValue>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Table header cell with DnD sortable context for column reordering */
|
|
14
|
+
export function SortableHeader<TData, TValue>({ header }: SortableHeaderProps<TData, TValue>) {
|
|
15
|
+
const { transform, transition, isDragging } = useSortable({ id: header.column.id })
|
|
16
|
+
|
|
17
|
+
const style: React.CSSProperties = {
|
|
18
|
+
transform: CSS.Translate.toString(transform),
|
|
19
|
+
transition,
|
|
20
|
+
opacity: isDragging ? 0.5 : 1,
|
|
21
|
+
zIndex: isDragging ? 1 : 0,
|
|
22
|
+
position: 'relative',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<TableHead key={header.id} colSpan={header.colSpan} style={style}>
|
|
27
|
+
{header.isPlaceholder ? null : (
|
|
28
|
+
<div className='flex items-center gap-1'>
|
|
29
|
+
{header.column.getCanPin() !== false && <ColumnDragHandle id={header.column.id} />}
|
|
30
|
+
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
</TableHead>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DndContext,
|
|
5
|
+
type DragEndEvent,
|
|
6
|
+
KeyboardSensor,
|
|
7
|
+
MouseSensor,
|
|
8
|
+
TouchSensor,
|
|
9
|
+
closestCenter,
|
|
10
|
+
useSensor,
|
|
11
|
+
useSensors,
|
|
12
|
+
} from '@dnd-kit/core'
|
|
13
|
+
import { restrictToHorizontalAxis } from '@dnd-kit/modifiers'
|
|
14
|
+
import { SortableContext, arrayMove, horizontalListSortingStrategy } from '@dnd-kit/sortable'
|
|
15
|
+
import { type ColumnDef, type Table as TanstackTable, flexRender } from '@tanstack/react-table'
|
|
2
16
|
|
|
3
|
-
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
4
17
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
18
|
+
import { Table, TableBody, TableCell, TableHeader, TableRow } from '@/components/ui/table'
|
|
19
|
+
|
|
5
20
|
import { DataTablePagination } from './data-table-pagination'
|
|
21
|
+
import { SortableHeader } from './data-table-sortable-header'
|
|
6
22
|
|
|
7
23
|
interface DataTableProps<TData> {
|
|
8
24
|
table: TanstackTable<TData>
|
|
@@ -11,53 +27,87 @@ interface DataTableProps<TData> {
|
|
|
11
27
|
}
|
|
12
28
|
|
|
13
29
|
export function DataTable<TData>({ table, columns, isLoading }: DataTableProps<TData>) {
|
|
30
|
+
const [columnOrder, setColumnOrder] = useState<string[]>(() =>
|
|
31
|
+
table.getAllLeafColumns().map((col) => col.id)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const sensors = useSensors(
|
|
35
|
+
useSensor(MouseSensor, {}),
|
|
36
|
+
useSensor(TouchSensor, {}),
|
|
37
|
+
useSensor(KeyboardSensor, {})
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
function handleDragEnd(event: DragEndEvent) {
|
|
41
|
+
const { active, over } = event
|
|
42
|
+
if (active && over && active.id !== over.id) {
|
|
43
|
+
const newOrder = arrayMove(
|
|
44
|
+
columnOrder,
|
|
45
|
+
columnOrder.indexOf(String(active.id)),
|
|
46
|
+
columnOrder.indexOf(String(over.id))
|
|
47
|
+
)
|
|
48
|
+
setColumnOrder(newOrder)
|
|
49
|
+
table.setColumnOrder(newOrder)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
14
53
|
return (
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
<DndContext
|
|
55
|
+
sensors={sensors}
|
|
56
|
+
collisionDetection={closestCenter}
|
|
57
|
+
onDragEnd={handleDragEnd}
|
|
58
|
+
modifiers={[restrictToHorizontalAxis]}
|
|
59
|
+
>
|
|
60
|
+
<div className='space-y-4'>
|
|
61
|
+
<div className='rounded-md border'>
|
|
62
|
+
<Table>
|
|
63
|
+
<TableHeader>
|
|
64
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
65
|
+
<SortableContext
|
|
66
|
+
key={headerGroup.id}
|
|
67
|
+
items={columnOrder}
|
|
68
|
+
strategy={horizontalListSortingStrategy}
|
|
69
|
+
>
|
|
70
|
+
<tr>
|
|
71
|
+
{headerGroup.headers.map((header) => (
|
|
72
|
+
<SortableHeader key={header.id} header={header} />
|
|
73
|
+
))}
|
|
74
|
+
</tr>
|
|
75
|
+
</SortableContext>
|
|
76
|
+
))}
|
|
77
|
+
</TableHeader>
|
|
78
|
+
<TableBody>
|
|
79
|
+
{isLoading ? (
|
|
80
|
+
Array.from({ length: 5 }).map((_, i) => (
|
|
81
|
+
<TableRow key={i}>
|
|
82
|
+
{columns.map((_, j) => (
|
|
83
|
+
<TableCell key={j}>
|
|
84
|
+
<Skeleton className='h-4 w-full' />
|
|
85
|
+
</TableCell>
|
|
86
|
+
))}
|
|
87
|
+
</TableRow>
|
|
88
|
+
))
|
|
89
|
+
) : table.getRowModel().rows.length ? (
|
|
90
|
+
table.getRowModel().rows.map((row) => (
|
|
91
|
+
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
|
92
|
+
{row.getVisibleCells().map((cell) => (
|
|
93
|
+
<TableCell key={cell.id}>
|
|
94
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
95
|
+
</TableCell>
|
|
96
|
+
))}
|
|
97
|
+
</TableRow>
|
|
98
|
+
))
|
|
99
|
+
) : (
|
|
100
|
+
<TableRow>
|
|
101
|
+
<TableCell colSpan={columns.length} className='h-24 text-center text-muted-foreground'>
|
|
102
|
+
No results.
|
|
103
|
+
</TableCell>
|
|
48
104
|
</TableRow>
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
</TableCell>
|
|
55
|
-
</TableRow>
|
|
56
|
-
)}
|
|
57
|
-
</TableBody>
|
|
58
|
-
</Table>
|
|
105
|
+
)}
|
|
106
|
+
</TableBody>
|
|
107
|
+
</Table>
|
|
108
|
+
</div>
|
|
109
|
+
<DataTablePagination table={table} />
|
|
59
110
|
</div>
|
|
60
|
-
|
|
61
|
-
</div>
|
|
111
|
+
</DndContext>
|
|
62
112
|
)
|
|
63
113
|
}
|
|
@@ -23,10 +23,24 @@ export function SignInForm() {
|
|
|
23
23
|
return (
|
|
24
24
|
<div className='space-y-6'>
|
|
25
25
|
<div className='text-center'>
|
|
26
|
-
<h1 className='text-2xl font-bold tracking-tight'>
|
|
27
|
-
<p className='mt-1 text-sm text-muted-foreground'>
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
<h1 className='text-2xl font-bold tracking-tight'>Welcome back</h1>
|
|
27
|
+
<p className='mt-1 text-sm text-muted-foreground'>Sign in to your account to continue</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
{/* Demo shortcut */}
|
|
31
|
+
<div className='rounded-md border border-dashed p-3 text-center'>
|
|
32
|
+
<p className='text-xs text-muted-foreground mb-2'>No account? Try the demo</p>
|
|
33
|
+
<Button
|
|
34
|
+
type='button'
|
|
35
|
+
variant='outline'
|
|
36
|
+
size='sm'
|
|
37
|
+
onClick={() => {
|
|
38
|
+
form.setValue('email', 'demo@gmail.com')
|
|
39
|
+
form.setValue('password', 'demo')
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
Use demo account
|
|
43
|
+
</Button>
|
|
30
44
|
</div>
|
|
31
45
|
|
|
32
46
|
<Form {...form}>
|
|
@@ -2,6 +2,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
|
2
2
|
|
|
3
3
|
import { SignInForm } from '@/features/auth/components/sign-in-form'
|
|
4
4
|
import { useAuthStore } from '@/stores/auth-store'
|
|
5
|
+
import { siteConfig } from '@/config/site'
|
|
5
6
|
|
|
6
7
|
export const Route = createFileRoute('/(auth)/sign-in')({
|
|
7
8
|
beforeLoad: () => {
|
|
@@ -15,9 +16,24 @@ export const Route = createFileRoute('/(auth)/sign-in')({
|
|
|
15
16
|
|
|
16
17
|
function SignInPage() {
|
|
17
18
|
return (
|
|
18
|
-
<div className='
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
<div className='grid min-h-screen lg:grid-cols-2'>
|
|
20
|
+
{/* Left: branding panel (hidden on mobile) */}
|
|
21
|
+
<div className='hidden lg:flex flex-col items-start justify-between bg-muted p-10'>
|
|
22
|
+
<div className='flex items-center gap-2 text-lg font-semibold'>
|
|
23
|
+
<div className='h-8 w-8 rounded-md bg-primary' />
|
|
24
|
+
{siteConfig.name}
|
|
25
|
+
</div>
|
|
26
|
+
<blockquote className='space-y-2'>
|
|
27
|
+
<p className='text-lg'>"This boilerplate saved us weeks of setup time. Everything just works."</p>
|
|
28
|
+
<footer className='text-sm text-muted-foreground'>— A Happy Developer</footer>
|
|
29
|
+
</blockquote>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{/* Right: form panel */}
|
|
33
|
+
<div className='flex items-center justify-center p-8'>
|
|
34
|
+
<div className='w-full max-w-sm'>
|
|
35
|
+
<SignInForm />
|
|
36
|
+
</div>
|
|
21
37
|
</div>
|
|
22
38
|
</div>
|
|
23
39
|
)
|