doo-boilerplate 0.2.5 → 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/dist/index.js CHANGED
@@ -112,7 +112,8 @@ var FILE_RENAMES = [
112
112
  ["_gitignore", ".gitignore"],
113
113
  ["_env.example", ".env.example"],
114
114
  ["_prettierrc", ".prettierrc"],
115
- ["_prettierignore", ".prettierignore"]
115
+ ["_prettierignore", ".prettierignore"],
116
+ ["_package.json", "package.json"]
116
117
  ];
117
118
  async function scaffold(options, destDir) {
118
119
  const templateSymlink = path2.join(__dirname2, "..", "templates", "template-vite");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doo-boilerplate",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "CLI to scaffold Pila portal frontend projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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",
@@ -20,7 +20,7 @@ export function ErrorPage({ code, title, description, showBack = true }: ErrorPa
20
20
  <p className='max-w-md text-muted-foreground'>{description}</p>
21
21
  <div className='mt-4 flex gap-3'>
22
22
  {showBack && (
23
- <Button variant='outline' onClick={() => history.back()}>Go Back</Button>
23
+ <Button variant='outline' onClick={() => window.history.back()}>Go Back</Button>
24
24
  )}
25
25
  <Button asChild>
26
26
  <Link to='/dashboard'>Go to Dashboard</Link>
@@ -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
+ }
@@ -35,7 +35,7 @@ export function DataTableFacetedFilter<TData, TValue>({
35
35
  options,
36
36
  }: DataTableFacetedFilterProps<TData, TValue>) {
37
37
  const facets = column?.getFacetedUniqueValues()
38
- const selectedValues = new Set(column?.getFilterValue() as string[])
38
+ const selectedValues = new Set((column?.getFilterValue() as string[] | undefined) ?? [])
39
39
 
40
40
  return (
41
41
  <Popover>
@@ -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 { flexRender, type ColumnDef, type Table as TanstackTable } from '@tanstack/react-table'
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
- <div className='space-y-4'>
16
- <div className='rounded-md border'>
17
- <Table>
18
- <TableHeader>
19
- {table.getHeaderGroups().map((headerGroup) => (
20
- <TableRow key={headerGroup.id}>
21
- {headerGroup.headers.map((header) => (
22
- <TableHead key={header.id} colSpan={header.colSpan}>
23
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
24
- </TableHead>
25
- ))}
26
- </TableRow>
27
- ))}
28
- </TableHeader>
29
- <TableBody>
30
- {isLoading ? (
31
- Array.from({ length: 5 }).map((_, i) => (
32
- <TableRow key={i}>
33
- {columns.map((_, j) => (
34
- <TableCell key={j}>
35
- <Skeleton className='h-4 w-full' />
36
- </TableCell>
37
- ))}
38
- </TableRow>
39
- ))
40
- ) : table.getRowModel().rows.length ? (
41
- table.getRowModel().rows.map((row) => (
42
- <TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
43
- {row.getVisibleCells().map((cell) => (
44
- <TableCell key={cell.id}>
45
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
46
- </TableCell>
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
- <TableRow>
52
- <TableCell colSpan={columns.length} className='h-24 text-center text-muted-foreground'>
53
- No results.
54
- </TableCell>
55
- </TableRow>
56
- )}
57
- </TableBody>
58
- </Table>
105
+ )}
106
+ </TableBody>
107
+ </Table>
108
+ </div>
109
+ <DataTablePagination table={table} />
59
110
  </div>
60
- <DataTablePagination table={table} />
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'>Sign in</h1>
27
- <p className='mt-1 text-sm text-muted-foreground'>
28
- Enter your credentials to access your account · or try <strong>demo@gmail.com / demo</strong>
29
- </p>
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}>
@@ -3,18 +3,18 @@ import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YA
3
3
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
4
4
 
5
5
  const data = [
6
- { month: 'Jan', revenue: 18000, users: 980 },
7
- { month: 'Feb', revenue: 22000, users: 1200 },
8
- { month: 'Mar', revenue: 19500, users: 1050 },
9
- { month: 'Apr', revenue: 28000, users: 1540 },
10
- { month: 'May', revenue: 24000, users: 1320 },
11
- { month: 'Jun', revenue: 32000, users: 1780 },
12
- { month: 'Jul', revenue: 35000, users: 1960 },
13
- { month: 'Aug', revenue: 29000, users: 1590 },
14
- { month: 'Sep', revenue: 38000, users: 2100 },
15
- { month: 'Oct', revenue: 42000, users: 2300 },
16
- { month: 'Nov', revenue: 39000, users: 2150 },
17
- { month: 'Dec', revenue: 45000, users: 2480 },
6
+ { month: 'Jan', revenue: 18000 },
7
+ { month: 'Feb', revenue: 22000 },
8
+ { month: 'Mar', revenue: 19500 },
9
+ { month: 'Apr', revenue: 28000 },
10
+ { month: 'May', revenue: 24000 },
11
+ { month: 'Jun', revenue: 32000 },
12
+ { month: 'Jul', revenue: 35000 },
13
+ { month: 'Aug', revenue: 29000 },
14
+ { month: 'Sep', revenue: 38000 },
15
+ { month: 'Oct', revenue: 42000 },
16
+ { month: 'Nov', revenue: 39000 },
17
+ { month: 'Dec', revenue: 45000 },
18
18
  ]
19
19
 
20
20
  export function OverviewChart() {
@@ -38,7 +38,7 @@ export function getTaskColumns({ onEdit, onDelete }: ActionsCallbacks): ColumnDe
38
38
  id: 'select',
39
39
  header: ({ table }) => (
40
40
  <Checkbox
41
- checked={table.getIsAllPageRowsSelected()}
41
+ checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')}
42
42
  onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
43
43
  aria-label='Select all'
44
44
  />
@@ -1,4 +1,5 @@
1
1
  import axios from 'axios'
2
+ import { AUTH_STORAGE_KEY } from '@/stores/auth-store'
2
3
 
3
4
  // Axios instance configured with base URL from Vite env
4
5
  const apiClient = axios.create({
@@ -10,7 +11,7 @@ const apiClient = axios.create({
10
11
  // Attach Bearer token from auth storage on each request
11
12
  apiClient.interceptors.request.use((config) => {
12
13
  try {
13
- const stored = localStorage.getItem('auth-storage')
14
+ const stored = localStorage.getItem(AUTH_STORAGE_KEY)
14
15
  if (stored) {
15
16
  const { state } = JSON.parse(stored)
16
17
  if (state?.accessToken) {
@@ -28,7 +29,7 @@ apiClient.interceptors.response.use(
28
29
  (response) => response,
29
30
  (error) => {
30
31
  if (error.response?.status === 401) {
31
- localStorage.removeItem('auth-storage')
32
+ localStorage.removeItem(AUTH_STORAGE_KEY)
32
33
  window.location.href = '/sign-in'
33
34
  }
34
35
  return Promise.reject(error)
@@ -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='flex min-h-screen items-center justify-center bg-background p-4'>
19
- <div className='w-full max-w-sm'>
20
- <SignInForm />
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
  )
@@ -1,3 +1,6 @@
1
+ /** Shared storage key used by auth store and api-client interceptor */
2
+ export const AUTH_STORAGE_KEY = 'auth-storage'
3
+
1
4
  import { create } from 'zustand'
2
5
  import { persist } from 'zustand/middleware'
3
6
  import { jwtDecode } from 'jwt-decode'
@@ -51,6 +54,6 @@ export const useAuthStore = create<AuthState>()(
51
54
  hasRole: (role) => get().user?.roles.includes(role) ?? false,
52
55
  hasPermission: (permission) => get().user?.permissions.includes(permission) ?? false,
53
56
  }),
54
- { name: 'auth-storage' }
57
+ { name: AUTH_STORAGE_KEY }
55
58
  )
56
59
  )