@alfredmouelle/create-stack 0.1.0

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.
Files changed (206) hide show
  1. package/README.md +56 -0
  2. package/_stack/apps/next-base/.dockerignore +10 -0
  3. package/_stack/apps/next-base/Dockerfile +34 -0
  4. package/_stack/apps/next-base/README.md +32 -0
  5. package/_stack/apps/next-base/components.json +25 -0
  6. package/_stack/apps/next-base/drizzle.config.ts +16 -0
  7. package/_stack/apps/next-base/next.config.ts +8 -0
  8. package/_stack/apps/next-base/package.json +70 -0
  9. package/_stack/apps/next-base/postcss.config.mjs +7 -0
  10. package/_stack/apps/next-base/src/app/api/auth/[...all]/route.ts +4 -0
  11. package/_stack/apps/next-base/src/app/api/trpc/[trpc]/route.ts +23 -0
  12. package/_stack/apps/next-base/src/app/auth/_components/forgot-password-form.tsx +92 -0
  13. package/_stack/apps/next-base/src/app/auth/_components/reset-password-form.tsx +105 -0
  14. package/_stack/apps/next-base/src/app/auth/_components/sign-in-form.tsx +126 -0
  15. package/_stack/apps/next-base/src/app/auth/_components/sign-up-form.tsx +139 -0
  16. package/_stack/apps/next-base/src/app/auth/_components/verify-email-actions.tsx +45 -0
  17. package/_stack/apps/next-base/src/app/auth/forgot-password/page.tsx +19 -0
  18. package/_stack/apps/next-base/src/app/auth/layout.tsx +9 -0
  19. package/_stack/apps/next-base/src/app/auth/reset-password/page.tsx +26 -0
  20. package/_stack/apps/next-base/src/app/auth/sign-in/page.tsx +27 -0
  21. package/_stack/apps/next-base/src/app/auth/sign-up/page.tsx +27 -0
  22. package/_stack/apps/next-base/src/app/auth/verify-email/page.tsx +30 -0
  23. package/_stack/apps/next-base/src/app/dashboard/page.tsx +12 -0
  24. package/_stack/apps/next-base/src/app/globals.css +171 -0
  25. package/_stack/apps/next-base/src/app/layout.tsx +23 -0
  26. package/_stack/apps/next-base/src/app/page.tsx +15 -0
  27. package/_stack/apps/next-base/src/components/data-table.tsx +77 -0
  28. package/_stack/apps/next-base/src/components/infinite-data-table.tsx +102 -0
  29. package/_stack/apps/next-base/src/components/sortable-header.tsx +37 -0
  30. package/_stack/apps/next-base/src/components/theme-provider.tsx +8 -0
  31. package/_stack/apps/next-base/src/components/theme-toggle.tsx +37 -0
  32. package/_stack/apps/next-base/src/components/ui/button.tsx +64 -0
  33. package/_stack/apps/next-base/src/components/ui/calendar.tsx +185 -0
  34. package/_stack/apps/next-base/src/components/ui/card.tsx +84 -0
  35. package/_stack/apps/next-base/src/components/ui/date-picker.tsx +85 -0
  36. package/_stack/apps/next-base/src/components/ui/date-range-picker.tsx +138 -0
  37. package/_stack/apps/next-base/src/components/ui/dropdown-menu.tsx +246 -0
  38. package/_stack/apps/next-base/src/components/ui/form.tsx +149 -0
  39. package/_stack/apps/next-base/src/components/ui/input-group.tsx +97 -0
  40. package/_stack/apps/next-base/src/components/ui/input.tsx +18 -0
  41. package/_stack/apps/next-base/src/components/ui/label.tsx +18 -0
  42. package/_stack/apps/next-base/src/components/ui/popover.tsx +76 -0
  43. package/_stack/apps/next-base/src/components/ui/skeleton.tsx +13 -0
  44. package/_stack/apps/next-base/src/components/ui/spinner.tsx +8 -0
  45. package/_stack/apps/next-base/src/components/ui/table.tsx +87 -0
  46. package/_stack/apps/next-base/src/emails/components/components.tsx +199 -0
  47. package/_stack/apps/next-base/src/emails/components/context.tsx +18 -0
  48. package/_stack/apps/next-base/src/emails/components/index.ts +23 -0
  49. package/_stack/apps/next-base/src/emails/components/theme.ts +65 -0
  50. package/_stack/apps/next-base/src/emails/reset-password.tsx +16 -0
  51. package/_stack/apps/next-base/src/emails/verify-email.tsx +15 -0
  52. package/_stack/apps/next-base/src/env.ts +41 -0
  53. package/_stack/apps/next-base/src/features/auth/auth-card.tsx +30 -0
  54. package/_stack/apps/next-base/src/features/auth/form-alert.tsx +27 -0
  55. package/_stack/apps/next-base/src/features/auth/google-button.tsx +66 -0
  56. package/_stack/apps/next-base/src/features/auth/schemas.ts +35 -0
  57. package/_stack/apps/next-base/src/lib/date.ts +4 -0
  58. package/_stack/apps/next-base/src/lib/utils.ts +6 -0
  59. package/_stack/apps/next-base/src/server/api/root.ts +10 -0
  60. package/_stack/apps/next-base/src/server/api/routers/health.router.ts +8 -0
  61. package/_stack/apps/next-base/src/server/api/trpc.ts +56 -0
  62. package/_stack/apps/next-base/src/server/auth/guards.ts +10 -0
  63. package/_stack/apps/next-base/src/server/better-auth/client.ts +9 -0
  64. package/_stack/apps/next-base/src/server/better-auth/config.ts +60 -0
  65. package/_stack/apps/next-base/src/server/better-auth/emails.tsx +25 -0
  66. package/_stack/apps/next-base/src/server/better-auth/index.ts +1 -0
  67. package/_stack/apps/next-base/src/server/better-auth/server.ts +14 -0
  68. package/_stack/apps/next-base/src/server/db/index.ts +6 -0
  69. package/_stack/apps/next-base/src/server/db/keyset.ts +63 -0
  70. package/_stack/apps/next-base/src/server/db/schemas/auth.schema.ts +71 -0
  71. package/_stack/apps/next-base/src/server/db/schemas/index.ts +2 -0
  72. package/_stack/apps/next-base/src/server/db/seed.ts +27 -0
  73. package/_stack/apps/next-base/src/server/email/adapters/resend/config.ts +7 -0
  74. package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +75 -0
  75. package/_stack/apps/next-base/src/server/email/core/address.ts +21 -0
  76. package/_stack/apps/next-base/src/server/email/core/port.ts +89 -0
  77. package/_stack/apps/next-base/src/server/email/core/render.ts +16 -0
  78. package/_stack/apps/next-base/src/server/email/factory.ts +47 -0
  79. package/_stack/apps/next-base/src/server/email/index.ts +36 -0
  80. package/_stack/apps/next-base/src/trpc/query-client.ts +19 -0
  81. package/_stack/apps/next-base/src/trpc/react.tsx +62 -0
  82. package/_stack/apps/next-base/src/trpc/server.ts +23 -0
  83. package/_stack/apps/next-base/tsconfig.json +37 -0
  84. package/_stack/apps/tanstack-base/.dockerignore +13 -0
  85. package/_stack/apps/tanstack-base/Dockerfile +28 -0
  86. package/_stack/apps/tanstack-base/README.md +31 -0
  87. package/_stack/apps/tanstack-base/components.json +25 -0
  88. package/_stack/apps/tanstack-base/drizzle.config.ts +16 -0
  89. package/_stack/apps/tanstack-base/package.json +85 -0
  90. package/_stack/apps/tanstack-base/public/favicon.ico +0 -0
  91. package/_stack/apps/tanstack-base/public/logo192.png +0 -0
  92. package/_stack/apps/tanstack-base/public/logo512.png +0 -0
  93. package/_stack/apps/tanstack-base/public/manifest.json +25 -0
  94. package/_stack/apps/tanstack-base/public/robots.txt +3 -0
  95. package/_stack/apps/tanstack-base/src/components/data-table.tsx +77 -0
  96. package/_stack/apps/tanstack-base/src/components/form/field-error.tsx +18 -0
  97. package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +47 -0
  98. package/_stack/apps/tanstack-base/src/components/infinite-data-table.tsx +102 -0
  99. package/_stack/apps/tanstack-base/src/components/sortable-header.tsx +37 -0
  100. package/_stack/apps/tanstack-base/src/components/theme-provider.tsx +69 -0
  101. package/_stack/apps/tanstack-base/src/components/theme-toggle.tsx +35 -0
  102. package/_stack/apps/tanstack-base/src/components/ui/button.tsx +64 -0
  103. package/_stack/apps/tanstack-base/src/components/ui/calendar.tsx +185 -0
  104. package/_stack/apps/tanstack-base/src/components/ui/card.tsx +84 -0
  105. package/_stack/apps/tanstack-base/src/components/ui/date-picker.tsx +83 -0
  106. package/_stack/apps/tanstack-base/src/components/ui/date-range-picker.tsx +136 -0
  107. package/_stack/apps/tanstack-base/src/components/ui/dropdown-menu.tsx +246 -0
  108. package/_stack/apps/tanstack-base/src/components/ui/input-group.tsx +97 -0
  109. package/_stack/apps/tanstack-base/src/components/ui/input.tsx +18 -0
  110. package/_stack/apps/tanstack-base/src/components/ui/label.tsx +18 -0
  111. package/_stack/apps/tanstack-base/src/components/ui/popover.tsx +74 -0
  112. package/_stack/apps/tanstack-base/src/components/ui/skeleton.tsx +13 -0
  113. package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +8 -0
  114. package/_stack/apps/tanstack-base/src/components/ui/table.tsx +87 -0
  115. package/_stack/apps/tanstack-base/src/emails/components/components.tsx +199 -0
  116. package/_stack/apps/tanstack-base/src/emails/components/context.tsx +18 -0
  117. package/_stack/apps/tanstack-base/src/emails/components/index.ts +23 -0
  118. package/_stack/apps/tanstack-base/src/emails/components/theme.ts +65 -0
  119. package/_stack/apps/tanstack-base/src/emails/reset-password.tsx +16 -0
  120. package/_stack/apps/tanstack-base/src/emails/verify-email.tsx +15 -0
  121. package/_stack/apps/tanstack-base/src/env.ts +41 -0
  122. package/_stack/apps/tanstack-base/src/features/auth/auth-card.tsx +30 -0
  123. package/_stack/apps/tanstack-base/src/features/auth/form-alert.tsx +27 -0
  124. package/_stack/apps/tanstack-base/src/features/auth/google-button.tsx +64 -0
  125. package/_stack/apps/tanstack-base/src/features/auth/schemas.ts +35 -0
  126. package/_stack/apps/tanstack-base/src/lib/date.ts +4 -0
  127. package/_stack/apps/tanstack-base/src/lib/utils.ts +6 -0
  128. package/_stack/apps/tanstack-base/src/router.tsx +40 -0
  129. package/_stack/apps/tanstack-base/src/routes/__root.tsx +73 -0
  130. package/_stack/apps/tanstack-base/src/routes/_authed/dashboard.tsx +12 -0
  131. package/_stack/apps/tanstack-base/src/routes/_authed.tsx +21 -0
  132. package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +14 -0
  133. package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +31 -0
  134. package/_stack/apps/tanstack-base/src/routes/auth/forgot-password.tsx +89 -0
  135. package/_stack/apps/tanstack-base/src/routes/auth/reset-password.tsx +111 -0
  136. package/_stack/apps/tanstack-base/src/routes/auth/sign-in.tsx +117 -0
  137. package/_stack/apps/tanstack-base/src/routes/auth/sign-up.tsx +119 -0
  138. package/_stack/apps/tanstack-base/src/routes/auth/verify-email.tsx +72 -0
  139. package/_stack/apps/tanstack-base/src/routes/auth.tsx +22 -0
  140. package/_stack/apps/tanstack-base/src/routes/index.tsx +18 -0
  141. package/_stack/apps/tanstack-base/src/server/api/root.ts +10 -0
  142. package/_stack/apps/tanstack-base/src/server/api/routers/health.router.ts +8 -0
  143. package/_stack/apps/tanstack-base/src/server/api/trpc.ts +61 -0
  144. package/_stack/apps/tanstack-base/src/server/better-auth/client.ts +9 -0
  145. package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +68 -0
  146. package/_stack/apps/tanstack-base/src/server/better-auth/emails.tsx +25 -0
  147. package/_stack/apps/tanstack-base/src/server/better-auth/index.ts +1 -0
  148. package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +9 -0
  149. package/_stack/apps/tanstack-base/src/server/db/index.ts +6 -0
  150. package/_stack/apps/tanstack-base/src/server/db/keyset.ts +63 -0
  151. package/_stack/apps/tanstack-base/src/server/db/schemas/auth.schema.ts +71 -0
  152. package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +2 -0
  153. package/_stack/apps/tanstack-base/src/server/db/seed.ts +27 -0
  154. package/_stack/apps/tanstack-base/src/server/email/adapters/resend/config.ts +7 -0
  155. package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +75 -0
  156. package/_stack/apps/tanstack-base/src/server/email/core/address.ts +21 -0
  157. package/_stack/apps/tanstack-base/src/server/email/core/port.ts +89 -0
  158. package/_stack/apps/tanstack-base/src/server/email/core/render.ts +16 -0
  159. package/_stack/apps/tanstack-base/src/server/email/factory.ts +47 -0
  160. package/_stack/apps/tanstack-base/src/server/email/index.ts +36 -0
  161. package/_stack/apps/tanstack-base/src/styles.css +171 -0
  162. package/_stack/apps/tanstack-base/src/trpc/devtools.tsx +6 -0
  163. package/_stack/apps/tanstack-base/src/trpc/query-client.ts +19 -0
  164. package/_stack/apps/tanstack-base/src/trpc/react.tsx +49 -0
  165. package/_stack/apps/tanstack-base/src/trpc/server.ts +11 -0
  166. package/_stack/apps/tanstack-base/tsconfig.json +27 -0
  167. package/_stack/apps/tanstack-base/tsr.config.json +3 -0
  168. package/_stack/apps/tanstack-base/vite.config.ts +15 -0
  169. package/_stack/packages/analytics/capability.json +26 -0
  170. package/_stack/packages/cache/capability.json +21 -0
  171. package/_stack/packages/error-tracking/capability.json +21 -0
  172. package/_stack/packages/jobs/capability.json +26 -0
  173. package/_stack/packages/logger/capability.json +21 -0
  174. package/_stack/packages/mailer/capability.json +28 -0
  175. package/_stack/packages/mailer/package.json +37 -0
  176. package/_stack/packages/mailer/src/adapters/brevo/config.ts +7 -0
  177. package/_stack/packages/mailer/src/adapters/brevo/index.ts +90 -0
  178. package/_stack/packages/mailer/src/adapters/resend/config.ts +7 -0
  179. package/_stack/packages/mailer/src/adapters/resend/index.ts +75 -0
  180. package/_stack/packages/mailer/src/adapters/ses/config.ts +13 -0
  181. package/_stack/packages/mailer/src/adapters/ses/index.ts +103 -0
  182. package/_stack/packages/storage/capability.json +32 -0
  183. package/_stack/patterns/README.md +58 -0
  184. package/_stack/patterns/_baseline/README-author.md +10 -0
  185. package/_stack/patterns/_baseline/biome.jsonc +119 -0
  186. package/_stack/patterns/_baseline/env.ts +31 -0
  187. package/_stack/patterns/_baseline/tsconfig.json +27 -0
  188. package/_stack/patterns/better-auth/pattern.json +73 -0
  189. package/_stack/patterns/better-auth-next/pattern.json +76 -0
  190. package/_stack/patterns/data-table/pattern.json +43 -0
  191. package/_stack/patterns/drizzle/pattern.json +61 -0
  192. package/_stack/patterns/trpc/pattern.json +61 -0
  193. package/_stack/patterns/trpc-next/pattern.json +64 -0
  194. package/index.mjs +216 -0
  195. package/lib/build.mjs +64 -0
  196. package/lib/env.mjs +56 -0
  197. package/lib/identity.mjs +33 -0
  198. package/lib/mailer.mjs +95 -0
  199. package/lib/manifests.mjs +61 -0
  200. package/lib/scaffold.mjs +49 -0
  201. package/lib/strip.mjs +132 -0
  202. package/lib/util.mjs +82 -0
  203. package/package.json +51 -0
  204. package/templates/next/layout.no-trpc.tsx +22 -0
  205. package/templates/tanstack/__root.no-trpc.tsx +63 -0
  206. package/templates/tanstack/router.no-trpc.tsx +24 -0
@@ -0,0 +1,102 @@
1
+ import { flexRender, type Table as ReactTable } from '@tanstack/react-table'
2
+ import { Skeleton } from '~/components/ui/skeleton'
3
+ import {
4
+ Table,
5
+ TableBody,
6
+ TableCell,
7
+ TableHead,
8
+ TableHeader,
9
+ TableRow,
10
+ } from '~/components/ui/table'
11
+ import { cn } from '~/lib/utils'
12
+
13
+ interface InfiniteDataTableProps<TData> {
14
+ table: ReactTable<TData>
15
+ columnCount: number
16
+ isLoading: boolean
17
+ isRefetching: boolean
18
+ isFetchingNextPage: boolean
19
+ hasFilters: boolean
20
+ emptyLabel: string
21
+ emptyFilteredLabel: string
22
+ sentinelRef: (node: HTMLDivElement | null) => void
23
+ }
24
+
25
+ export function InfiniteDataTable<TData>({
26
+ table,
27
+ columnCount,
28
+ isLoading,
29
+ isRefetching,
30
+ isFetchingNextPage,
31
+ hasFilters,
32
+ emptyLabel,
33
+ emptyFilteredLabel,
34
+ sentinelRef,
35
+ }: InfiniteDataTableProps<TData>) {
36
+ const dataRows = table.getRowModel().rows
37
+
38
+ return (
39
+ <div
40
+ className={cn(
41
+ 'overflow-hidden rounded-lg border transition-opacity',
42
+ isRefetching && 'opacity-60',
43
+ )}
44
+ >
45
+ <Table>
46
+ <TableHeader>
47
+ {table.getHeaderGroups().map((headerGroup) => (
48
+ <TableRow key={headerGroup.id}>
49
+ {headerGroup.headers.map((header) => (
50
+ <TableHead key={header.id}>
51
+ {header.isPlaceholder
52
+ ? null
53
+ : flexRender(header.column.columnDef.header, header.getContext())}
54
+ </TableHead>
55
+ ))}
56
+ </TableRow>
57
+ ))}
58
+ </TableHeader>
59
+ <TableBody>
60
+ {isLoading ? <SkeletonRows columns={columnCount} /> : null}
61
+
62
+ {!isLoading && dataRows.length === 0 ? (
63
+ <TableRow>
64
+ <TableCell className="h-24 text-center text-muted-foreground" colSpan={columnCount}>
65
+ {hasFilters ? emptyFilteredLabel : emptyLabel}
66
+ </TableCell>
67
+ </TableRow>
68
+ ) : null}
69
+
70
+ {dataRows.map((row) => (
71
+ <TableRow className="group" key={row.id}>
72
+ {row.getVisibleCells().map((cell) => (
73
+ <TableCell key={cell.id}>
74
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
75
+ </TableCell>
76
+ ))}
77
+ </TableRow>
78
+ ))}
79
+
80
+ {isFetchingNextPage ? <SkeletonRows columns={columnCount} /> : null}
81
+ </TableBody>
82
+ </Table>
83
+ <div ref={sentinelRef} />
84
+ </div>
85
+ )
86
+ }
87
+
88
+ function SkeletonRows({ columns }: { columns: number }) {
89
+ return (
90
+ <>
91
+ {Array.from({ length: 5 }, (_, rowIndex) => (
92
+ <TableRow key={rowIndex}>
93
+ {Array.from({ length: columns }, (_, cellIndex) => (
94
+ <TableCell key={cellIndex}>
95
+ <Skeleton className="h-6 w-full" />
96
+ </TableCell>
97
+ ))}
98
+ </TableRow>
99
+ ))}
100
+ </>
101
+ )
102
+ }
@@ -0,0 +1,37 @@
1
+ import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react'
2
+ import { Button } from '~/components/ui/button'
3
+
4
+ export interface SortState<Field extends string> {
5
+ field: Field
6
+ direction: 'asc' | 'desc'
7
+ }
8
+
9
+ interface SortableHeaderProps<Field extends string> {
10
+ label: string
11
+ field: Field
12
+ sort: SortState<Field>
13
+ onSort: (field: Field) => void
14
+ }
15
+
16
+ export function SortableHeader<Field extends string>({
17
+ label,
18
+ field,
19
+ sort,
20
+ onSort,
21
+ }: SortableHeaderProps<Field>) {
22
+ const active = sort.field === field
23
+ const Icon = active ? (sort.direction === 'asc' ? ArrowUp : ArrowDown) : ArrowUpDown
24
+
25
+ return (
26
+ <Button
27
+ className="-ml-2 h-8 cursor-pointer text-muted-foreground data-[active=true]:text-foreground"
28
+ data-active={active}
29
+ onClick={() => onSort(field)}
30
+ size="sm"
31
+ variant="ghost"
32
+ >
33
+ {label}
34
+ <Icon className="size-3.5" />
35
+ </Button>
36
+ )
37
+ }
@@ -0,0 +1,8 @@
1
+ 'use client'
2
+
3
+ import { ThemeProvider as NextThemesProvider } from 'next-themes'
4
+ import type { ComponentProps } from 'react'
5
+
6
+ export function ThemeProvider({ children, ...props }: ComponentProps<typeof NextThemesProvider>) {
7
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>
8
+ }
@@ -0,0 +1,37 @@
1
+ 'use client'
2
+
3
+ import { Monitor, Moon, Sun } from 'lucide-react'
4
+ import { useTheme } from 'next-themes'
5
+ import { Button } from '~/components/ui/button'
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuTrigger,
11
+ } from '~/components/ui/dropdown-menu'
12
+
13
+ export function ThemeToggle() {
14
+ const { setTheme } = useTheme()
15
+
16
+ return (
17
+ <DropdownMenu>
18
+ <DropdownMenuTrigger asChild>
19
+ <Button aria-label="Toggle theme" size="icon" type="button" variant="ghost">
20
+ <Sun className="dark:hidden" />
21
+ <Moon className="hidden dark:block" />
22
+ </Button>
23
+ </DropdownMenuTrigger>
24
+ <DropdownMenuContent align="end">
25
+ <DropdownMenuItem onClick={() => setTheme('light')}>
26
+ <Sun /> Light
27
+ </DropdownMenuItem>
28
+ <DropdownMenuItem onClick={() => setTheme('dark')}>
29
+ <Moon /> Dark
30
+ </DropdownMenuItem>
31
+ <DropdownMenuItem onClick={() => setTheme('system')}>
32
+ <Monitor /> System
33
+ </DropdownMenuItem>
34
+ </DropdownMenuContent>
35
+ </DropdownMenu>
36
+ )
37
+ }
@@ -0,0 +1,64 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority'
2
+ import { Slot } from 'radix-ui'
3
+ import type { ComponentProps } from 'react'
4
+ import { cn } from '~/lib/utils'
5
+
6
+ const buttonVariants = cva(
7
+ "group/button inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-4xl border border-transparent bg-clip-padding font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
12
+ outline:
13
+ 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent dark:hover:bg-input/30',
14
+ secondary:
15
+ 'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
16
+ ghost:
17
+ 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
18
+ destructive:
19
+ 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 dark:hover:bg-destructive/30',
20
+ link: 'text-primary underline-offset-4 hover:underline',
21
+ },
22
+ size: {
23
+ default:
24
+ 'h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5',
25
+ xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
26
+ sm: 'h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
27
+ lg: 'h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',
28
+ icon: 'size-9',
29
+ 'icon-xs': "size-6 [&_svg:not([class*='size-'])]:size-3",
30
+ 'icon-sm': 'size-8',
31
+ 'icon-lg': 'size-10',
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ variant: 'default',
36
+ size: 'default',
37
+ },
38
+ },
39
+ )
40
+
41
+ function Button({
42
+ className,
43
+ variant = 'default',
44
+ size = 'default',
45
+ asChild = false,
46
+ ...props
47
+ }: ComponentProps<'button'> &
48
+ VariantProps<typeof buttonVariants> & {
49
+ asChild?: boolean
50
+ }) {
51
+ const Comp = asChild ? Slot.Root : 'button'
52
+
53
+ return (
54
+ <Comp
55
+ className={cn(buttonVariants({ variant, size, className }))}
56
+ data-size={size}
57
+ data-slot="button"
58
+ data-variant={variant}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ export { Button, buttonVariants }
@@ -0,0 +1,185 @@
1
+ 'use client'
2
+
3
+ import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
4
+ import * as React from 'react'
5
+ import { type DayButton, DayPicker, getDefaultClassNames, type Locale } from 'react-day-picker'
6
+ import { Button, buttonVariants } from '~/components/ui/button'
7
+ import { cn } from '~/lib/utils'
8
+
9
+ function Calendar({
10
+ className,
11
+ classNames,
12
+ showOutsideDays = true,
13
+ captionLayout = 'label',
14
+ buttonVariant = 'ghost',
15
+ locale,
16
+ formatters,
17
+ components,
18
+ ...props
19
+ }: React.ComponentProps<typeof DayPicker> & {
20
+ buttonVariant?: React.ComponentProps<typeof Button>['variant']
21
+ }) {
22
+ const defaultClassNames = getDefaultClassNames()
23
+
24
+ return (
25
+ <DayPicker
26
+ captionLayout={captionLayout}
27
+ className={cn(
28
+ 'group/calendar bg-background in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent p-3 [--cell-radius:var(--radius-4xl)] [--cell-size:--spacing(8)]',
29
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
30
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
31
+ className,
32
+ )}
33
+ classNames={{
34
+ root: cn('w-fit', defaultClassNames.root),
35
+ months: cn('relative flex flex-col gap-4 md:flex-row', defaultClassNames.months),
36
+ month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
37
+ nav: cn(
38
+ 'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1',
39
+ defaultClassNames.nav,
40
+ ),
41
+ button_previous: cn(
42
+ buttonVariants({ variant: buttonVariant }),
43
+ 'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
44
+ defaultClassNames.button_previous,
45
+ ),
46
+ button_next: cn(
47
+ buttonVariants({ variant: buttonVariant }),
48
+ 'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
49
+ defaultClassNames.button_next,
50
+ ),
51
+ month_caption: cn(
52
+ 'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
53
+ defaultClassNames.month_caption,
54
+ ),
55
+ dropdowns: cn(
56
+ 'flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm',
57
+ defaultClassNames.dropdowns,
58
+ ),
59
+ dropdown_root: cn('relative rounded-(--cell-radius)', defaultClassNames.dropdown_root),
60
+ dropdown: cn('absolute inset-0 bg-popover opacity-0', defaultClassNames.dropdown),
61
+ caption_label: cn(
62
+ 'select-none font-medium',
63
+ captionLayout === 'label'
64
+ ? 'text-sm'
65
+ : 'flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
66
+ defaultClassNames.caption_label,
67
+ ),
68
+ month_grid: cn('w-full border-collapse', defaultClassNames.month_grid),
69
+ weekdays: cn('flex', defaultClassNames.weekdays),
70
+ weekday: cn(
71
+ 'flex-1 select-none rounded-(--cell-radius) font-normal text-[0.8rem] text-muted-foreground',
72
+ defaultClassNames.weekday,
73
+ ),
74
+ week: cn('mt-2 flex w-full', defaultClassNames.week),
75
+ week_number_header: cn('w-(--cell-size) select-none', defaultClassNames.week_number_header),
76
+ week_number: cn(
77
+ 'select-none text-[0.8rem] text-muted-foreground',
78
+ defaultClassNames.week_number,
79
+ ),
80
+ day: cn(
81
+ 'group/day relative aspect-square h-full w-full select-none rounded-(--cell-radius) p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)',
82
+ props.showWeekNumber
83
+ ? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)'
84
+ : '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)',
85
+ defaultClassNames.day,
86
+ ),
87
+ range_start: cn(
88
+ 'relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted',
89
+ defaultClassNames.range_start,
90
+ ),
91
+ range_middle: cn('rounded-none', defaultClassNames.range_middle),
92
+ range_end: cn(
93
+ 'relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted',
94
+ defaultClassNames.range_end,
95
+ ),
96
+ today: cn(
97
+ 'rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none',
98
+ defaultClassNames.today,
99
+ ),
100
+ outside: cn(
101
+ 'text-muted-foreground aria-selected:text-muted-foreground',
102
+ defaultClassNames.outside,
103
+ ),
104
+ disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
105
+ hidden: cn('invisible', defaultClassNames.hidden),
106
+ ...classNames,
107
+ }}
108
+ components={{
109
+ Root: ({ className, rootRef, ...props }) => {
110
+ return <div className={cn(className)} data-slot="calendar" ref={rootRef} {...props} />
111
+ },
112
+ Chevron: ({ className, orientation, ...props }) => {
113
+ if (orientation === 'left') {
114
+ return <ChevronLeftIcon className={cn('size-4', className)} {...props} />
115
+ }
116
+
117
+ if (orientation === 'right') {
118
+ return <ChevronRightIcon className={cn('size-4', className)} {...props} />
119
+ }
120
+
121
+ return <ChevronDownIcon className={cn('size-4', className)} {...props} />
122
+ },
123
+ DayButton: ({ ...props }) => <CalendarDayButton locale={locale} {...props} />,
124
+ WeekNumber: ({ children, ...props }) => {
125
+ return (
126
+ <td {...props}>
127
+ <div className="flex size-(--cell-size) items-center justify-center text-center">
128
+ {children}
129
+ </div>
130
+ </td>
131
+ )
132
+ },
133
+ ...components,
134
+ }}
135
+ formatters={{
136
+ formatMonthDropdown: (date) => date.toLocaleString(locale?.code, { month: 'short' }),
137
+ ...formatters,
138
+ }}
139
+ locale={locale}
140
+ showOutsideDays={showOutsideDays}
141
+ {...props}
142
+ />
143
+ )
144
+ }
145
+
146
+ function CalendarDayButton({
147
+ className,
148
+ day,
149
+ modifiers,
150
+ locale,
151
+ ...props
152
+ }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
153
+ const defaultClassNames = getDefaultClassNames()
154
+
155
+ const ref = React.useRef<HTMLButtonElement>(null)
156
+ React.useEffect(() => {
157
+ if (modifiers.focused) ref.current?.focus()
158
+ }, [modifiers.focused])
159
+
160
+ return (
161
+ <Button
162
+ className={cn(
163
+ 'relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 font-normal leading-none data-[range-end=true]:rounded-(--cell-radius) data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-end=true]:bg-primary data-[range-middle=true]:bg-muted data-[range-start=true]:bg-primary data-[selected-single=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:text-foreground data-[range-start=true]:text-primary-foreground data-[selected-single=true]:text-primary-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70',
164
+ defaultClassNames.day,
165
+ className,
166
+ )}
167
+ data-day={day.date.toLocaleDateString(locale?.code)}
168
+ data-range-end={modifiers.range_end}
169
+ data-range-middle={modifiers.range_middle}
170
+ data-range-start={modifiers.range_start}
171
+ data-selected-single={
172
+ modifiers.selected &&
173
+ !modifiers.range_start &&
174
+ !modifiers.range_end &&
175
+ !modifiers.range_middle
176
+ }
177
+ ref={ref}
178
+ size="icon"
179
+ variant="ghost"
180
+ {...props}
181
+ />
182
+ )
183
+ }
184
+
185
+ export { Calendar, CalendarDayButton }
@@ -0,0 +1,84 @@
1
+ import type { ComponentProps } from 'react'
2
+ import { cn } from '~/lib/utils'
3
+
4
+ function Card({
5
+ className,
6
+ size = 'default',
7
+ ...props
8
+ }: ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
9
+ return (
10
+ <div
11
+ className={cn(
12
+ 'group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-4xl bg-card py-(--card-spacing) text-card-foreground text-sm shadow-md ring-1 ring-foreground/5 [--card-spacing:--spacing(6)] has-[>img:first-child]:pt-0 dark:ring-foreground/10 data-[size=sm]:[--card-spacing:--spacing(4)] *:[img:first-child]:rounded-t-4xl *:[img:last-child]:rounded-b-4xl',
13
+ className,
14
+ )}
15
+ data-size={size}
16
+ data-slot="card"
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function CardHeader({ className, ...props }: ComponentProps<'div'>) {
23
+ return (
24
+ <div
25
+ className={cn(
26
+ 'group/card-header @container/card-header grid auto-rows-min items-start gap-1.5 rounded-t-4xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)',
27
+ className,
28
+ )}
29
+ data-slot="card-header"
30
+ {...props}
31
+ />
32
+ )
33
+ }
34
+
35
+ function CardTitle({ className, ...props }: ComponentProps<'div'>) {
36
+ return (
37
+ <div
38
+ className={cn('font-heading font-medium text-base', className)}
39
+ data-slot="card-title"
40
+ {...props}
41
+ />
42
+ )
43
+ }
44
+
45
+ function CardDescription({ className, ...props }: ComponentProps<'div'>) {
46
+ return (
47
+ <div
48
+ className={cn('text-muted-foreground text-sm', className)}
49
+ data-slot="card-description"
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+
55
+ function CardAction({ className, ...props }: ComponentProps<'div'>) {
56
+ return (
57
+ <div
58
+ className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
59
+ data-slot="card-action"
60
+ {...props}
61
+ />
62
+ )
63
+ }
64
+
65
+ function CardContent({ className, ...props }: ComponentProps<'div'>) {
66
+ return (
67
+ <div className={cn('px-(--card-spacing)', className)} data-slot="card-content" {...props} />
68
+ )
69
+ }
70
+
71
+ function CardFooter({ className, ...props }: ComponentProps<'div'>) {
72
+ return (
73
+ <div
74
+ className={cn(
75
+ 'flex items-center rounded-b-4xl px-(--card-spacing) [.border-t]:pt-(--card-spacing)',
76
+ className,
77
+ )}
78
+ data-slot="card-footer"
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
@@ -0,0 +1,85 @@
1
+ 'use client'
2
+
3
+ import { format, isValid, parseISO } from 'date-fns'
4
+ import { CalendarIcon, X } from 'lucide-react'
5
+ import { useState } from 'react'
6
+ import { Button } from '~/components/ui/button'
7
+ import { Calendar } from '~/components/ui/calendar'
8
+ import { Popover, PopoverContent, PopoverTrigger } from '~/components/ui/popover'
9
+ import { toISODate } from '~/lib/date'
10
+ import { cn } from '~/lib/utils'
11
+
12
+ interface DatePickerProps {
13
+ value: string
14
+ onChange: (value: string) => void
15
+ placeholder?: string
16
+ disabled?: boolean
17
+ className?: string
18
+ }
19
+
20
+ export function DatePicker({
21
+ value,
22
+ onChange,
23
+ placeholder = 'Pick a date',
24
+ disabled,
25
+ className,
26
+ }: DatePickerProps) {
27
+ const [open, setOpen] = useState(false)
28
+ const parsed = value ? parseISO(value) : undefined
29
+ const date = parsed && isValid(parsed) ? parsed : undefined
30
+
31
+ const clear = () => {
32
+ onChange('')
33
+ setOpen(false)
34
+ }
35
+
36
+ return (
37
+ <Popover onOpenChange={setOpen} open={open}>
38
+ <div className={cn('group relative w-full', className)}>
39
+ <PopoverTrigger asChild>
40
+ <Button
41
+ className={cn(
42
+ 'w-full cursor-pointer justify-start px-3 text-left font-normal',
43
+ !date && 'text-muted-foreground',
44
+ )}
45
+ disabled={disabled}
46
+ variant="outline"
47
+ >
48
+ <CalendarIcon
49
+ className={cn(
50
+ 'mr-2 size-4 opacity-50 transition-opacity',
51
+ date && !disabled && 'group-hover:opacity-0',
52
+ )}
53
+ />
54
+ {date ? format(date, 'PPP') : <span>{placeholder}</span>}
55
+ </Button>
56
+ </PopoverTrigger>
57
+
58
+ {date && !disabled && (
59
+ <button
60
+ aria-label="Clear date"
61
+ className="absolute top-1/2 left-3 z-10 flex size-4 -translate-y-1/2 cursor-pointer items-center justify-center text-muted-foreground opacity-0 transition-opacity hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100"
62
+ onClick={clear}
63
+ type="button"
64
+ >
65
+ <X className="size-4" />
66
+ </button>
67
+ )}
68
+ </div>
69
+
70
+ <PopoverContent align="start" className="w-auto p-0">
71
+ <Calendar
72
+ autoFocus
73
+ mode="single"
74
+ onSelect={(selected) => {
75
+ if (selected) {
76
+ onChange(toISODate(selected))
77
+ setOpen(false)
78
+ }
79
+ }}
80
+ selected={date}
81
+ />
82
+ </PopoverContent>
83
+ </Popover>
84
+ )
85
+ }