@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,40 @@
1
+ import { createRouter as createTanStackRouter } from '@tanstack/react-router'
2
+ import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
3
+ import type { ReactNode } from 'react'
4
+ import { ThemeProvider } from './components/theme-provider'
5
+ import { routeTree } from './routeTree.gen'
6
+ import { createQueryClient } from './trpc/query-client'
7
+ import { createServerHelpers, TRPCReactProvider } from './trpc/react'
8
+
9
+ export function getRouter() {
10
+ const queryClient = createQueryClient()
11
+ const trpc = createServerHelpers(queryClient)
12
+
13
+ const router = createTanStackRouter({
14
+ routeTree,
15
+ context: { queryClient, trpc },
16
+ scrollRestoration: true,
17
+ defaultPreload: 'intent',
18
+ defaultPreloadStaleTime: 0,
19
+
20
+ Wrap: (props: { children: ReactNode }) => (
21
+ <ThemeProvider defaultTheme="system">
22
+ <TRPCReactProvider queryClient={queryClient}>{props.children}</TRPCReactProvider>
23
+ </ThemeProvider>
24
+ ),
25
+ })
26
+
27
+ setupRouterSsrQueryIntegration({
28
+ router,
29
+ queryClient,
30
+ wrapQueryClient: false,
31
+ })
32
+
33
+ return router
34
+ }
35
+
36
+ declare module '@tanstack/react-router' {
37
+ interface Register {
38
+ router: ReturnType<typeof getRouter>
39
+ }
40
+ }
@@ -0,0 +1,73 @@
1
+ import { TanStackDevtools } from '@tanstack/react-devtools'
2
+ import type { QueryClient } from '@tanstack/react-query'
3
+ import { createRootRouteWithContext, HeadContent, Scripts } from '@tanstack/react-router'
4
+ import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
5
+ import type { TRPCOptionsProxy } from '@trpc/tanstack-react-query'
6
+ import type { AppRouter } from '~/server/api/root'
7
+ import TanStackQueryDevtools from '~/trpc/devtools'
8
+
9
+ import appCss from '../styles.css?url'
10
+
11
+ // Runs before hydration to set the theme class and avoid a flash of wrong theme.
12
+ const themeScript = `(function(){try{var t=localStorage.getItem('theme')||'system';var m=window.matchMedia('(prefers-color-scheme:dark)').matches;document.documentElement.classList.toggle('dark',t==='dark'||(t==='system'&&m));}catch(e){}})();`
13
+
14
+ interface RouterContext {
15
+ queryClient: QueryClient
16
+ trpc: TRPCOptionsProxy<AppRouter>
17
+ }
18
+
19
+ export const Route = createRootRouteWithContext<RouterContext>()({
20
+ head: () => ({
21
+ meta: [
22
+ {
23
+ charSet: 'utf-8',
24
+ },
25
+ {
26
+ name: 'viewport',
27
+ content: 'width=device-width, initial-scale=1',
28
+ },
29
+ {
30
+ name: 'author',
31
+ content: 'Alfred MOUELLE',
32
+ },
33
+ {
34
+ title: 'App',
35
+ },
36
+ ],
37
+ links: [
38
+ {
39
+ rel: 'stylesheet',
40
+ href: appCss,
41
+ },
42
+ ],
43
+ }),
44
+ shellComponent: RootDocument,
45
+ })
46
+
47
+ function RootDocument({ children }: { children: React.ReactNode }) {
48
+ return (
49
+ <html lang="en">
50
+ <head>
51
+ {/** biome-ignore lint/security/noDangerouslySetInnerHtml: anti-FOUC theme script */}
52
+ <script dangerouslySetInnerHTML={{ __html: themeScript }} />
53
+ <HeadContent />
54
+ </head>
55
+ <body>
56
+ {children}
57
+ <TanStackDevtools
58
+ config={{
59
+ position: 'bottom-right',
60
+ }}
61
+ plugins={[
62
+ {
63
+ name: 'Tanstack Router',
64
+ render: <TanStackRouterDevtoolsPanel />,
65
+ },
66
+ TanStackQueryDevtools,
67
+ ]}
68
+ />
69
+ <Scripts />
70
+ </body>
71
+ </html>
72
+ )
73
+ }
@@ -0,0 +1,12 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+
3
+ export const Route = createFileRoute('/_authed/dashboard')({ component: Dashboard })
4
+
5
+ function Dashboard() {
6
+ return (
7
+ <div className="p-8">
8
+ <h1 className="font-heading font-medium text-2xl">Dashboard</h1>
9
+ <p className="mt-2 text-muted-foreground">Protected route.</p>
10
+ </div>
11
+ )
12
+ }
@@ -0,0 +1,21 @@
1
+ import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
2
+ import { getServerSession } from '~/server/better-auth/session'
3
+
4
+ /**
5
+ * Layout group guarding every route under it: redirects to sign-in when there's
6
+ * no session, and exposes the session to child loaders via `beforeLoad`.
7
+ */
8
+ export const Route = createFileRoute('/_authed')({
9
+ beforeLoad: async () => {
10
+ const session = await getServerSession()
11
+ if (!session) {
12
+ throw redirect({ to: '/auth/sign-in' })
13
+ }
14
+ return { session }
15
+ },
16
+ component: AuthedLayout,
17
+ })
18
+
19
+ function AuthedLayout() {
20
+ return <Outlet />
21
+ }
@@ -0,0 +1,14 @@
1
+ // Loads the TanStack Start augmentation that adds `server` to route options.
2
+
3
+ import { createFileRoute } from '@tanstack/react-router'
4
+ import type {} from '@tanstack/react-start'
5
+ import { auth } from '~/server/better-auth'
6
+
7
+ export const Route = createFileRoute('/api/auth/$')({
8
+ server: {
9
+ handlers: {
10
+ GET: ({ request }) => auth.handler(request),
11
+ POST: ({ request }) => auth.handler(request),
12
+ },
13
+ },
14
+ })
@@ -0,0 +1,31 @@
1
+ // Loads the TanStack Start augmentation that adds `server` to route options
2
+ // (so this server route typechecks even without other react-start imports).
3
+
4
+ import { createFileRoute } from '@tanstack/react-router'
5
+ import type {} from '@tanstack/react-start'
6
+ import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
7
+ import { appRouter } from '~/server/api/root'
8
+ import { createTRPCContext } from '~/server/api/trpc'
9
+
10
+ function handler({ request }: { request: Request }) {
11
+ return fetchRequestHandler({
12
+ req: request,
13
+ router: appRouter,
14
+ endpoint: '/api/trpc',
15
+ createContext: () => createTRPCContext({ headers: request.headers }),
16
+ onError: import.meta.env.DEV
17
+ ? ({ path, error }) => {
18
+ console.error(`❌ tRPC failed on ${path ?? '<no-path>'}: ${error.message}`)
19
+ }
20
+ : undefined,
21
+ })
22
+ }
23
+
24
+ export const Route = createFileRoute('/api/trpc/$')({
25
+ server: {
26
+ handlers: {
27
+ GET: handler,
28
+ POST: handler,
29
+ },
30
+ },
31
+ })
@@ -0,0 +1,89 @@
1
+ import { useForm } from '@tanstack/react-form'
2
+ import { createFileRoute, Link } from '@tanstack/react-router'
3
+ import { Mail } from 'lucide-react'
4
+ import { useState } from 'react'
5
+ import { TextField } from '~/components/form/text-field'
6
+ import { Button } from '~/components/ui/button'
7
+ import { Spinner } from '~/components/ui/spinner'
8
+ import { AuthCard } from '~/features/auth/auth-card'
9
+ import { FormAlert } from '~/features/auth/form-alert'
10
+ import { ForgotPasswordSchema } from '~/features/auth/schemas'
11
+ import { authClient } from '~/server/better-auth/client'
12
+
13
+ export const Route = createFileRoute('/auth/forgot-password')({
14
+ component: ForgotPassword,
15
+ })
16
+
17
+ function ForgotPassword() {
18
+ const [formError, setFormError] = useState<string | null>(null)
19
+ const [sent, setSent] = useState(false)
20
+
21
+ const form = useForm({
22
+ defaultValues: { email: '' },
23
+ validators: { onBlur: ForgotPasswordSchema },
24
+ onSubmit: async ({ value }) => {
25
+ setFormError(null)
26
+ const { error } = await authClient.requestPasswordReset({
27
+ email: value.email,
28
+ redirectTo: '/auth/reset-password',
29
+ })
30
+
31
+ if (error) {
32
+ setFormError(error.message ?? 'Could not send. Try again.')
33
+ return
34
+ }
35
+ setSent(true)
36
+ },
37
+ })
38
+
39
+ return (
40
+ <AuthCard
41
+ description="Enter your email to receive a reset link."
42
+ footer={
43
+ <Link className="text-foreground hover:underline" to="/auth/sign-in">
44
+ Back to sign in
45
+ </Link>
46
+ }
47
+ title="Forgot password"
48
+ >
49
+ {sent ? (
50
+ <FormAlert variant="success">
51
+ If an account exists for this address, a reset email has just been sent. Check your spam
52
+ folder.
53
+ </FormAlert>
54
+ ) : (
55
+ <form
56
+ className="grid gap-4"
57
+ onSubmit={(e) => {
58
+ e.preventDefault()
59
+ form.handleSubmit()
60
+ }}
61
+ >
62
+ {formError ? <FormAlert>{formError}</FormAlert> : null}
63
+
64
+ <form.Field name="email">
65
+ {(field) => (
66
+ <TextField
67
+ autoComplete="email"
68
+ field={field}
69
+ icon={<Mail />}
70
+ label="Email"
71
+ placeholder="you@example.com"
72
+ type="email"
73
+ />
74
+ )}
75
+ </form.Field>
76
+
77
+ <form.Subscribe selector={(s) => [s.canSubmit, s.isSubmitting]}>
78
+ {([canSubmit, isSubmitting]) => (
79
+ <Button className="w-full cursor-pointer" disabled={!canSubmit} type="submit">
80
+ {isSubmitting ? <Spinner /> : null}
81
+ {isSubmitting ? 'Sending…' : 'Send link'}
82
+ </Button>
83
+ )}
84
+ </form.Subscribe>
85
+ </form>
86
+ )}
87
+ </AuthCard>
88
+ )
89
+ }
@@ -0,0 +1,111 @@
1
+ import { useForm } from '@tanstack/react-form'
2
+ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
3
+ import { Lock } from 'lucide-react'
4
+ import { useState } from 'react'
5
+ import { TextField } from '~/components/form/text-field'
6
+ import { Button } from '~/components/ui/button'
7
+ import { Spinner } from '~/components/ui/spinner'
8
+ import { AuthCard } from '~/features/auth/auth-card'
9
+ import { FormAlert } from '~/features/auth/form-alert'
10
+ import { ResetPasswordSchema } from '~/features/auth/schemas'
11
+ import { authClient } from '~/server/better-auth/client'
12
+
13
+ interface ResetPasswordSearch {
14
+ token?: string
15
+ error?: string
16
+ }
17
+
18
+ export const Route = createFileRoute('/auth/reset-password')({
19
+ validateSearch: (search: Record<string, unknown>): ResetPasswordSearch => ({
20
+ token: typeof search.token === 'string' ? search.token : undefined,
21
+ error: typeof search.error === 'string' ? search.error : undefined,
22
+ }),
23
+ component: ResetPassword,
24
+ })
25
+
26
+ function ResetPassword() {
27
+ const navigate = useNavigate()
28
+ const { token, error: tokenError } = Route.useSearch()
29
+ const [formError, setFormError] = useState<string | null>(null)
30
+
31
+ const invalidToken = !token || tokenError === 'INVALID_TOKEN'
32
+
33
+ const form = useForm({
34
+ defaultValues: { password: '', confirmPassword: '' },
35
+ validators: { onBlur: ResetPasswordSchema },
36
+ onSubmit: async ({ value }) => {
37
+ if (!token) return
38
+ setFormError(null)
39
+ const { error } = await authClient.resetPassword({
40
+ newPassword: value.password,
41
+ token,
42
+ })
43
+
44
+ if (error) {
45
+ setFormError(error.message ?? 'Could not reset password.')
46
+ return
47
+ }
48
+ await navigate({ to: '/auth/sign-in' })
49
+ },
50
+ })
51
+
52
+ return (
53
+ <AuthCard
54
+ description="Choose a new password for your account."
55
+ footer={
56
+ <Link className="text-foreground hover:underline" to="/auth/forgot-password">
57
+ Request a new link
58
+ </Link>
59
+ }
60
+ title="New password"
61
+ >
62
+ {invalidToken ? (
63
+ <FormAlert>This reset link is invalid or expired. Please request a new one.</FormAlert>
64
+ ) : (
65
+ <form
66
+ className="grid gap-4"
67
+ onSubmit={(e) => {
68
+ e.preventDefault()
69
+ form.handleSubmit()
70
+ }}
71
+ >
72
+ {formError ? <FormAlert>{formError}</FormAlert> : null}
73
+
74
+ <form.Field name="password">
75
+ {(field) => (
76
+ <TextField
77
+ autoComplete="new-password"
78
+ field={field}
79
+ icon={<Lock />}
80
+ label="New password"
81
+ placeholder="At least 8 characters"
82
+ type="password"
83
+ />
84
+ )}
85
+ </form.Field>
86
+
87
+ <form.Field name="confirmPassword">
88
+ {(field) => (
89
+ <TextField
90
+ autoComplete="new-password"
91
+ field={field}
92
+ icon={<Lock />}
93
+ label="Confirm password"
94
+ type="password"
95
+ />
96
+ )}
97
+ </form.Field>
98
+
99
+ <form.Subscribe selector={(s) => [s.canSubmit, s.isSubmitting]}>
100
+ {([canSubmit, isSubmitting]) => (
101
+ <Button className="w-full cursor-pointer" disabled={!canSubmit} type="submit">
102
+ {isSubmitting ? <Spinner /> : null}
103
+ {isSubmitting ? 'Saving…' : 'Reset password'}
104
+ </Button>
105
+ )}
106
+ </form.Subscribe>
107
+ </form>
108
+ )}
109
+ </AuthCard>
110
+ )
111
+ }
@@ -0,0 +1,117 @@
1
+ import { useForm } from '@tanstack/react-form'
2
+ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
3
+ import { Lock, Mail } from 'lucide-react'
4
+ import { useState } from 'react'
5
+ import { TextField } from '~/components/form/text-field'
6
+ import { Button } from '~/components/ui/button'
7
+ import { Spinner } from '~/components/ui/spinner'
8
+ import { AuthCard } from '~/features/auth/auth-card'
9
+ import { FormAlert } from '~/features/auth/form-alert'
10
+ import { AuthDivider, GoogleButton } from '~/features/auth/google-button'
11
+ import { SignInSchema } from '~/features/auth/schemas'
12
+ import { authClient } from '~/server/better-auth/client'
13
+
14
+ export const Route = createFileRoute('/auth/sign-in')({ component: SignIn })
15
+
16
+ function SignIn() {
17
+ const navigate = useNavigate()
18
+ const [formError, setFormError] = useState<string | null>(null)
19
+
20
+ const form = useForm({
21
+ defaultValues: { email: '', password: '' },
22
+ validators: { onBlur: SignInSchema },
23
+ onSubmit: async ({ value }) => {
24
+ setFormError(null)
25
+ const { error } = await authClient.signIn.email({
26
+ email: value.email,
27
+ password: value.password,
28
+ })
29
+
30
+ if (!error) {
31
+ await navigate({ to: '/' })
32
+ return
33
+ }
34
+
35
+ if (error.code === 'EMAIL_NOT_VERIFIED') {
36
+ await navigate({ to: '/auth/verify-email', search: { email: value.email } })
37
+ return
38
+ }
39
+
40
+ setFormError(
41
+ error.code === 'INVALID_EMAIL_OR_PASSWORD'
42
+ ? 'Incorrect email or password.'
43
+ : (error.message ?? 'Could not sign in. Try again.'),
44
+ )
45
+ },
46
+ })
47
+
48
+ return (
49
+ <AuthCard
50
+ description="Sign in to your account."
51
+ footer={
52
+ <>
53
+ No account yet?{' '}
54
+ <Link className="ml-1 text-foreground hover:underline" to="/auth/sign-up">
55
+ Create an account
56
+ </Link>
57
+ </>
58
+ }
59
+ title="Sign in"
60
+ >
61
+ <form
62
+ className="grid gap-4"
63
+ onSubmit={(e) => {
64
+ e.preventDefault()
65
+ form.handleSubmit()
66
+ }}
67
+ >
68
+ <GoogleButton label="Continue with Google" />
69
+ <AuthDivider />
70
+
71
+ {formError ? <FormAlert>{formError}</FormAlert> : null}
72
+
73
+ <form.Field name="email">
74
+ {(field) => (
75
+ <TextField
76
+ autoComplete="email"
77
+ field={field}
78
+ icon={<Mail />}
79
+ label="Email"
80
+ placeholder="you@example.com"
81
+ type="email"
82
+ />
83
+ )}
84
+ </form.Field>
85
+
86
+ <form.Field name="password">
87
+ {(field) => (
88
+ <div className="grid gap-2">
89
+ <TextField
90
+ autoComplete="current-password"
91
+ field={field}
92
+ icon={<Lock />}
93
+ label="Password"
94
+ type="password"
95
+ />
96
+ <Link
97
+ className="justify-self-end text-muted-foreground text-xs hover:underline"
98
+ to="/auth/forgot-password"
99
+ >
100
+ Forgot password?
101
+ </Link>
102
+ </div>
103
+ )}
104
+ </form.Field>
105
+
106
+ <form.Subscribe selector={(s) => [s.canSubmit, s.isSubmitting]}>
107
+ {([canSubmit, isSubmitting]) => (
108
+ <Button className="w-full cursor-pointer" disabled={!canSubmit} type="submit">
109
+ {isSubmitting ? <Spinner /> : null}
110
+ {isSubmitting ? 'Signing in…' : 'Sign in'}
111
+ </Button>
112
+ )}
113
+ </form.Subscribe>
114
+ </form>
115
+ </AuthCard>
116
+ )
117
+ }
@@ -0,0 +1,119 @@
1
+ import { useForm } from '@tanstack/react-form'
2
+ import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
3
+ import { Lock, Mail, User } from 'lucide-react'
4
+ import { useState } from 'react'
5
+ import { TextField } from '~/components/form/text-field'
6
+ import { Button } from '~/components/ui/button'
7
+ import { Spinner } from '~/components/ui/spinner'
8
+ import { AuthCard } from '~/features/auth/auth-card'
9
+ import { FormAlert } from '~/features/auth/form-alert'
10
+ import { AuthDivider, GoogleButton } from '~/features/auth/google-button'
11
+ import { SignUpSchema } from '~/features/auth/schemas'
12
+ import { authClient } from '~/server/better-auth/client'
13
+
14
+ export const Route = createFileRoute('/auth/sign-up')({ component: SignUp })
15
+
16
+ function SignUp() {
17
+ const navigate = useNavigate()
18
+ const [formError, setFormError] = useState<string | null>(null)
19
+
20
+ const form = useForm({
21
+ defaultValues: { name: '', email: '', password: '' },
22
+ validators: { onBlur: SignUpSchema },
23
+ onSubmit: async ({ value }) => {
24
+ setFormError(null)
25
+ const { error } = await authClient.signUp.email({
26
+ name: value.name,
27
+ email: value.email,
28
+ password: value.password,
29
+ callbackURL: '/',
30
+ })
31
+
32
+ if (!error) {
33
+ await navigate({ to: '/auth/verify-email', search: { email: value.email } })
34
+ return
35
+ }
36
+
37
+ setFormError(
38
+ error.code === 'USER_ALREADY_EXISTS'
39
+ ? 'An account already exists with this email.'
40
+ : (error.message ?? 'Could not sign up. Try again.'),
41
+ )
42
+ },
43
+ })
44
+
45
+ return (
46
+ <AuthCard
47
+ description="Create your account."
48
+ footer={
49
+ <>
50
+ Already have an account?{' '}
51
+ <Link className="ml-1 text-foreground hover:underline" to="/auth/sign-in">
52
+ Sign in
53
+ </Link>
54
+ </>
55
+ }
56
+ title="Create an account"
57
+ >
58
+ <form
59
+ className="grid gap-4"
60
+ onSubmit={(e) => {
61
+ e.preventDefault()
62
+ form.handleSubmit()
63
+ }}
64
+ >
65
+ <GoogleButton label="Sign up with Google" />
66
+ <AuthDivider />
67
+
68
+ {formError ? <FormAlert>{formError}</FormAlert> : null}
69
+
70
+ <form.Field name="name">
71
+ {(field) => (
72
+ <TextField
73
+ autoComplete="name"
74
+ field={field}
75
+ icon={<User />}
76
+ label="Name"
77
+ placeholder="Your name"
78
+ />
79
+ )}
80
+ </form.Field>
81
+
82
+ <form.Field name="email">
83
+ {(field) => (
84
+ <TextField
85
+ autoComplete="email"
86
+ field={field}
87
+ icon={<Mail />}
88
+ label="Email"
89
+ placeholder="you@example.com"
90
+ type="email"
91
+ />
92
+ )}
93
+ </form.Field>
94
+
95
+ <form.Field name="password">
96
+ {(field) => (
97
+ <TextField
98
+ autoComplete="new-password"
99
+ field={field}
100
+ icon={<Lock />}
101
+ label="Password"
102
+ placeholder="At least 8 characters"
103
+ type="password"
104
+ />
105
+ )}
106
+ </form.Field>
107
+
108
+ <form.Subscribe selector={(s) => [s.canSubmit, s.isSubmitting]}>
109
+ {([canSubmit, isSubmitting]) => (
110
+ <Button className="w-full cursor-pointer" disabled={!canSubmit} type="submit">
111
+ {isSubmitting ? <Spinner /> : null}
112
+ {isSubmitting ? 'Creating…' : 'Create account'}
113
+ </Button>
114
+ )}
115
+ </form.Subscribe>
116
+ </form>
117
+ </AuthCard>
118
+ )
119
+ }