create-nexst-monorepo 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 (303) hide show
  1. package/dist/index.js +1727 -0
  2. package/package.json +37 -0
  3. package/template/.agents/plugins/marketplace.json +21 -0
  4. package/template/.claude/settings.json +6 -0
  5. package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
  6. package/template/.claude/skills/frontend-design/SKILL.md +154 -0
  7. package/template/.claude/skills/theme-factory/SKILL.md +95 -0
  8. package/template/.claude/skills/theme-factory/references/schema.md +104 -0
  9. package/template/.claude/skills/theme-factory/scripts/theme.mjs +261 -0
  10. package/template/.dockerignore +43 -0
  11. package/template/.editorconfig +10 -0
  12. package/template/.github/workflows/ci.yaml +289 -0
  13. package/template/.github/workflows/publish-app.yaml +66 -0
  14. package/template/.github/workflows/publish.yaml +148 -0
  15. package/template/.github/workflows/snyk.yaml +34 -0
  16. package/template/.vscode/settings.json +8 -0
  17. package/template/AGENTS.md +42 -0
  18. package/template/Dockerfile.base +9 -0
  19. package/template/README.md +54 -0
  20. package/template/_gitignore +10 -0
  21. package/template/apps/api/.env.example +32 -0
  22. package/template/apps/api/AGENTS.md +44 -0
  23. package/template/apps/api/Dockerfile +69 -0
  24. package/template/apps/api/_gitignore +8 -0
  25. package/template/apps/api/knip.config.ts +22 -0
  26. package/template/apps/api/kustomize/base/application.properties +16 -0
  27. package/template/apps/api/kustomize/base/deployment.yaml +94 -0
  28. package/template/apps/api/kustomize/base/kustomization.yaml +17 -0
  29. package/template/apps/api/kustomize/base/service.yaml +10 -0
  30. package/template/apps/api/kustomize/overlays/prod/kustomization.yaml +17 -0
  31. package/template/apps/api/kustomize/overlays/staging/application.properties +1 -0
  32. package/template/apps/api/kustomize/overlays/staging/kustomization.yaml +21 -0
  33. package/template/apps/api/kustomize/secret.keys +14 -0
  34. package/template/apps/api/oxfmt.config.ts +8 -0
  35. package/template/apps/api/oxlint.config.ts +107 -0
  36. package/template/apps/api/package.json +79 -0
  37. package/template/apps/api/prisma/migrations/20260927000000_init/migration.sql +113 -0
  38. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  39. package/template/apps/api/prisma/schema.prisma +113 -0
  40. package/template/apps/api/prisma.config.ts +7 -0
  41. package/template/apps/api/scripts/auth.cli.ts +19 -0
  42. package/template/apps/api/scripts/seal-secret.sh +171 -0
  43. package/template/apps/api/sonar-project.properties +9 -0
  44. package/template/apps/api/src/app-info/app-info.dto.ts +10 -0
  45. package/template/apps/api/src/app-info/app-info.module.ts +7 -0
  46. package/template/apps/api/src/app-info/app-info.resolver.spec.ts +14 -0
  47. package/template/apps/api/src/app-info/app-info.resolver.ts +21 -0
  48. package/template/apps/api/src/app.module.spec.ts +93 -0
  49. package/template/apps/api/src/app.module.ts +77 -0
  50. package/template/apps/api/src/auth/auth.guard.spec.ts +90 -0
  51. package/template/apps/api/src/auth/auth.guard.ts +97 -0
  52. package/template/apps/api/src/auth/auth.ts +129 -0
  53. package/template/apps/api/src/auth/decorator/allow-anonymous.decorator.ts +17 -0
  54. package/template/apps/api/src/auth/decorator/is-email-verified.decorator.spec.ts +41 -0
  55. package/template/apps/api/src/auth/decorator/is-email-verified.decorator.ts +20 -0
  56. package/template/apps/api/src/auth/decorator/roles.decorator.ts +27 -0
  57. package/template/apps/api/src/auth/testing/make-session.ts +31 -0
  58. package/template/apps/api/src/auth/types/auth.types.ts +27 -0
  59. package/template/apps/api/src/bootstrap/register-graphql-enums.ts +12 -0
  60. package/template/apps/api/src/bootstrap/security.ts +24 -0
  61. package/template/apps/api/src/bootstrap/swagger.ts +28 -0
  62. package/template/apps/api/src/config/app.config.spec.ts +22 -0
  63. package/template/apps/api/src/config/app.config.ts +122 -0
  64. package/template/apps/api/src/config/graphql-depth-limit.ts +103 -0
  65. package/template/apps/api/src/config/graphql.config.ts +77 -0
  66. package/template/apps/api/src/config/resources/app.yaml +29 -0
  67. package/template/apps/api/src/errors/app-parse-uuid.pipe.ts +27 -0
  68. package/template/apps/api/src/errors/app.exception.ts +47 -0
  69. package/template/apps/api/src/errors/error-code.enum.ts +54 -0
  70. package/template/apps/api/src/errors/http-status-fallback-code.util.ts +27 -0
  71. package/template/apps/api/src/errors/validation-exception.factory.ts +43 -0
  72. package/template/apps/api/src/health/health.controller.spec.ts +82 -0
  73. package/template/apps/api/src/health/health.controller.ts +57 -0
  74. package/template/apps/api/src/health/health.module.ts +12 -0
  75. package/template/apps/api/src/logging/context-logger.ts +18 -0
  76. package/template/apps/api/src/logging/logger.config.ts +23 -0
  77. package/template/apps/api/src/logging/logging.module.ts +22 -0
  78. package/template/apps/api/src/mail/mail.module.ts +12 -0
  79. package/template/apps/api/src/mail/mail.service.spec.ts +47 -0
  80. package/template/apps/api/src/mail/mail.service.ts +123 -0
  81. package/template/apps/api/src/main.ts +41 -0
  82. package/template/apps/api/src/prisma/prisma.module.ts +12 -0
  83. package/template/apps/api/src/prisma/prisma.service.spec.ts +136 -0
  84. package/template/apps/api/src/prisma/prisma.service.ts +138 -0
  85. package/template/apps/api/src/queue/dto/enqueue-example.input.ts +11 -0
  86. package/template/apps/api/src/queue/example-queue.resolver.ts +14 -0
  87. package/template/apps/api/src/queue/example-queue.service.spec.ts +21 -0
  88. package/template/apps/api/src/queue/example-queue.service.ts +32 -0
  89. package/template/apps/api/src/queue/queue.module.ts +27 -0
  90. package/template/apps/api/src/r2/r2.module.ts +12 -0
  91. package/template/apps/api/src/r2/r2.service.spec.ts +117 -0
  92. package/template/apps/api/src/r2/r2.service.ts +131 -0
  93. package/template/apps/api/src/user/dto/user.dto.ts +26 -0
  94. package/template/apps/api/src/user/user.module.ts +10 -0
  95. package/template/apps/api/src/user/user.resolver.spec.ts +18 -0
  96. package/template/apps/api/src/user/user.resolver.ts +24 -0
  97. package/template/apps/api/src/utils/catch-everything-exception.filter.spec.ts +292 -0
  98. package/template/apps/api/src/utils/catch-everything-exception.filter.ts +70 -0
  99. package/template/apps/api/src/utils/error-handler.util.spec.ts +20 -0
  100. package/template/apps/api/src/utils/error-handler.util.ts +12 -0
  101. package/template/apps/api/src/utils/escape-like-pattern.util.spec.ts +29 -0
  102. package/template/apps/api/src/utils/escape-like-pattern.util.ts +11 -0
  103. package/template/apps/api/src/utils/pagination.input.ts +20 -0
  104. package/template/apps/api/src/utils/prisma-exception.filter.spec.ts +186 -0
  105. package/template/apps/api/src/utils/prisma-exception.filter.ts +45 -0
  106. package/template/apps/api/src/utils/prisma-graphql-exception.filter.spec.ts +216 -0
  107. package/template/apps/api/src/utils/prisma-graphql-exception.filter.ts +43 -0
  108. package/template/apps/api/tests/app.e2e-spec.ts +35 -0
  109. package/template/apps/api/tests/auth.e2e-spec.ts +38 -0
  110. package/template/apps/api/tests/helpers/api.ts +27 -0
  111. package/template/apps/api/tsconfig.json +11 -0
  112. package/template/apps/api/vitest.config.e2e.ts +16 -0
  113. package/template/apps/api/vitest.config.ts +40 -0
  114. package/template/apps/web/.env.example +7 -0
  115. package/template/apps/web/.storybook/decorators.tsx +38 -0
  116. package/template/apps/web/.storybook/main.ts +17 -0
  117. package/template/apps/web/.storybook/preview.ts +38 -0
  118. package/template/apps/web/.storybook/vitest.setup.ts +5 -0
  119. package/template/apps/web/AGENTS.md +29 -0
  120. package/template/apps/web/Dockerfile +49 -0
  121. package/template/apps/web/_gitignore +16 -0
  122. package/template/apps/web/knip.config.ts +15 -0
  123. package/template/apps/web/kustomize/base/application.properties +5 -0
  124. package/template/apps/web/kustomize/base/deployment.yaml +60 -0
  125. package/template/apps/web/kustomize/base/kustomization.yaml +17 -0
  126. package/template/apps/web/kustomize/base/service.yaml +10 -0
  127. package/template/apps/web/kustomize/overlays/prod/kustomization.yaml +12 -0
  128. package/template/apps/web/kustomize/overlays/staging/application.properties +2 -0
  129. package/template/apps/web/kustomize/overlays/staging/kustomization.yaml +18 -0
  130. package/template/apps/web/messages/de.json +124 -0
  131. package/template/apps/web/messages/en.json +124 -0
  132. package/template/apps/web/next-env.d.ts +7 -0
  133. package/template/apps/web/next.config.ts +39 -0
  134. package/template/apps/web/oxfmt.config.ts +9 -0
  135. package/template/apps/web/oxlint.config.ts +36 -0
  136. package/template/apps/web/package.json +80 -0
  137. package/template/apps/web/playwright.config.ts +34 -0
  138. package/template/apps/web/postcss.config.mjs +7 -0
  139. package/template/apps/web/sonar-project.properties +9 -0
  140. package/template/apps/web/src/app/[locale]/(auth)/recovery/page.tsx +22 -0
  141. package/template/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +29 -0
  142. package/template/apps/web/src/app/[locale]/(auth)/sign-in/page.tsx +24 -0
  143. package/template/apps/web/src/app/[locale]/(auth)/sign-up/page.tsx +24 -0
  144. package/template/apps/web/src/app/[locale]/(auth)/verify/page.tsx +24 -0
  145. package/template/apps/web/src/app/[locale]/dashboard/page.tsx +34 -0
  146. package/template/apps/web/src/app/[locale]/layout.tsx +60 -0
  147. package/template/apps/web/src/app/[locale]/page.tsx +23 -0
  148. package/template/apps/web/src/app/api/health/route.ts +3 -0
  149. package/template/apps/web/src/app/global-error.tsx +19 -0
  150. package/template/apps/web/src/app/globals.css +98 -0
  151. package/template/apps/web/src/app/icon.svg +1 -0
  152. package/template/apps/web/src/app/robots.ts +15 -0
  153. package/template/apps/web/src/app/sitemap.ts +24 -0
  154. package/template/apps/web/src/components/alert/alert.stories.tsx +25 -0
  155. package/template/apps/web/src/components/alert/alert.tsx +50 -0
  156. package/template/apps/web/src/components/avatar/avatar.stories.tsx +116 -0
  157. package/template/apps/web/src/components/avatar/avatar.tsx +88 -0
  158. package/template/apps/web/src/components/button/button-variants.ts +28 -0
  159. package/template/apps/web/src/components/button/button.stories.tsx +33 -0
  160. package/template/apps/web/src/components/button/button.tsx +32 -0
  161. package/template/apps/web/src/components/checkbox/checkbox.stories.tsx +312 -0
  162. package/template/apps/web/src/components/checkbox/checkbox.tsx +38 -0
  163. package/template/apps/web/src/components/dialog/danger-panel.stories.tsx +41 -0
  164. package/template/apps/web/src/components/dialog/danger-panel.tsx +32 -0
  165. package/template/apps/web/src/components/dialog/dialog.stories.tsx +239 -0
  166. package/template/apps/web/src/components/dialog/dialog.tsx +136 -0
  167. package/template/apps/web/src/components/dialog-buttons.stories.tsx +55 -0
  168. package/template/apps/web/src/components/dialog-buttons.tsx +48 -0
  169. package/template/apps/web/src/components/drawer/drawer.stories.tsx +251 -0
  170. package/template/apps/web/src/components/drawer/drawer.tsx +120 -0
  171. package/template/apps/web/src/components/dropdown-menu/dropdown-menu.stories.tsx +329 -0
  172. package/template/apps/web/src/components/dropdown-menu/dropdown-menu.tsx +263 -0
  173. package/template/apps/web/src/components/empty-state.stories.tsx +49 -0
  174. package/template/apps/web/src/components/empty-state.tsx +33 -0
  175. package/template/apps/web/src/components/field-group.stories.tsx +47 -0
  176. package/template/apps/web/src/components/field-group.tsx +20 -0
  177. package/template/apps/web/src/components/field-label.stories.tsx +34 -0
  178. package/template/apps/web/src/components/field-label.tsx +28 -0
  179. package/template/apps/web/src/components/icons.tsx +25 -0
  180. package/template/apps/web/src/components/input/input-otp.stories.tsx +86 -0
  181. package/template/apps/web/src/components/input/input-otp.tsx +184 -0
  182. package/template/apps/web/src/components/input/input.stories.tsx +35 -0
  183. package/template/apps/web/src/components/input/input.tsx +180 -0
  184. package/template/apps/web/src/components/popover/popover.stories.tsx +479 -0
  185. package/template/apps/web/src/components/popover/popover.tsx +45 -0
  186. package/template/apps/web/src/components/responsive-dialog/responsive-dialog.stories.tsx +307 -0
  187. package/template/apps/web/src/components/responsive-dialog/responsive-dialog.tsx +96 -0
  188. package/template/apps/web/src/components/searchbar/searchbar.stories.tsx +59 -0
  189. package/template/apps/web/src/components/searchbar/searchbar.tsx +64 -0
  190. package/template/apps/web/src/components/select/select.stories.tsx +293 -0
  191. package/template/apps/web/src/components/select/select.tsx +445 -0
  192. package/template/apps/web/src/components/skeleton/skeleton.stories.tsx +76 -0
  193. package/template/apps/web/src/components/skeleton/skeleton.tsx +13 -0
  194. package/template/apps/web/src/components/spinner/spinner.tsx +18 -0
  195. package/template/apps/web/src/components/switch/switch.stories.tsx +71 -0
  196. package/template/apps/web/src/components/switch/switch.tsx +35 -0
  197. package/template/apps/web/src/components/tabs/tabs.stories.tsx +209 -0
  198. package/template/apps/web/src/components/tabs/tabs.tsx +69 -0
  199. package/template/apps/web/src/components/tabs/url-tabs.stories.tsx +68 -0
  200. package/template/apps/web/src/components/tabs/url-tabs.tsx +42 -0
  201. package/template/apps/web/src/components/textarea/textarea.stories.tsx +144 -0
  202. package/template/apps/web/src/components/textarea/textarea.tsx +115 -0
  203. package/template/apps/web/src/components/toast/toaster.tsx +38 -0
  204. package/template/apps/web/src/components/tooltip/tooltip.stories.tsx +237 -0
  205. package/template/apps/web/src/components/tooltip/tooltip.tsx +49 -0
  206. package/template/apps/web/src/config/csp.ts +59 -0
  207. package/template/apps/web/src/config/routes.ts +14 -0
  208. package/template/apps/web/src/config/site.ts +5 -0
  209. package/template/apps/web/src/env.ts +15 -0
  210. package/template/apps/web/src/features/auth/auth-card.tsx +20 -0
  211. package/template/apps/web/src/features/auth/auth-divider.tsx +14 -0
  212. package/template/apps/web/src/features/auth/google-auth-button.tsx +49 -0
  213. package/template/apps/web/src/features/auth/google-one-tap.tsx +23 -0
  214. package/template/apps/web/src/features/auth/lib/auth-error.test.ts +22 -0
  215. package/template/apps/web/src/features/auth/lib/auth-error.ts +16 -0
  216. package/template/apps/web/src/features/auth/lib/return-to.test.ts +41 -0
  217. package/template/apps/web/src/features/auth/lib/return-to.ts +19 -0
  218. package/template/apps/web/src/features/auth/lib/validator.test.ts +45 -0
  219. package/template/apps/web/src/features/auth/lib/validator.ts +49 -0
  220. package/template/apps/web/src/features/auth/passkey-settings.tsx +106 -0
  221. package/template/apps/web/src/features/auth/recovery-form.tsx +61 -0
  222. package/template/apps/web/src/features/auth/resend-verification-button.tsx +36 -0
  223. package/template/apps/web/src/features/auth/reset-password-form.tsx +62 -0
  224. package/template/apps/web/src/features/auth/sign-in-form.stories.tsx +22 -0
  225. package/template/apps/web/src/features/auth/sign-in-form.tsx +116 -0
  226. package/template/apps/web/src/features/auth/sign-out-button.tsx +26 -0
  227. package/template/apps/web/src/features/auth/sign-up-form.tsx +107 -0
  228. package/template/apps/web/src/features/shared/provider.tsx +16 -0
  229. package/template/apps/web/src/features/shared/site-header.tsx +48 -0
  230. package/template/apps/web/src/features/shared/theme-toggle.tsx +24 -0
  231. package/template/apps/web/src/i18n/navigation.ts +5 -0
  232. package/template/apps/web/src/i18n/request.ts +21 -0
  233. package/template/apps/web/src/i18n/routing.ts +6 -0
  234. package/template/apps/web/src/lib/auth-client.ts +19 -0
  235. package/template/apps/web/src/lib/auth-server.ts +46 -0
  236. package/template/apps/web/src/lib/auth.ts +8 -0
  237. package/template/apps/web/src/lib/codegen.ts +30 -0
  238. package/template/apps/web/src/lib/graphql-client.ts +29 -0
  239. package/template/apps/web/src/lib/graphql-server.ts +28 -0
  240. package/template/apps/web/src/lib/graphql-utils.test.ts +32 -0
  241. package/template/apps/web/src/lib/graphql-utils.ts +76 -0
  242. package/template/apps/web/src/lib/hooks/use-media-query.ts +21 -0
  243. package/template/apps/web/src/lib/metadata.ts +17 -0
  244. package/template/apps/web/src/lib/network-error.test.ts +30 -0
  245. package/template/apps/web/src/lib/network-error.ts +17 -0
  246. package/template/apps/web/src/lib/proxy-utils.ts +42 -0
  247. package/template/apps/web/src/lib/query-client.ts +29 -0
  248. package/template/apps/web/src/lib/utils.ts +5 -0
  249. package/template/apps/web/src/proxy.ts +57 -0
  250. package/template/apps/web/test-results/.last-run.json +4 -0
  251. package/template/apps/web/tests/auth.spec.ts +17 -0
  252. package/template/apps/web/tests/csp-theme-script.spec.ts +37 -0
  253. package/template/apps/web/tests/home.spec.ts +25 -0
  254. package/template/apps/web/tsconfig.build.json +4 -0
  255. package/template/apps/web/tsconfig.json +28 -0
  256. package/template/apps/web/vitest.config.ts +40 -0
  257. package/template/apps/web/vitest.shims.d.ts +1 -0
  258. package/template/apps/worker/.env.example +4 -0
  259. package/template/apps/worker/AGENTS.md +11 -0
  260. package/template/apps/worker/Dockerfile +35 -0
  261. package/template/apps/worker/knip.config.ts +3 -0
  262. package/template/apps/worker/kustomize/base/application.properties +4 -0
  263. package/template/apps/worker/kustomize/base/deployment.yaml +62 -0
  264. package/template/apps/worker/kustomize/base/kustomization.yaml +16 -0
  265. package/template/apps/worker/kustomize/overlays/prod/kustomization.yaml +12 -0
  266. package/template/apps/worker/kustomize/overlays/staging/application.properties +1 -0
  267. package/template/apps/worker/kustomize/overlays/staging/kustomization.yaml +18 -0
  268. package/template/apps/worker/oxfmt.config.ts +8 -0
  269. package/template/apps/worker/oxlint.config.ts +15 -0
  270. package/template/apps/worker/package.json +33 -0
  271. package/template/apps/worker/sonar-project.properties +8 -0
  272. package/template/apps/worker/src/env.ts +13 -0
  273. package/template/apps/worker/src/lib/graceful-shutdown.ts +27 -0
  274. package/template/apps/worker/src/lib/logger.ts +20 -0
  275. package/template/apps/worker/src/main.ts +17 -0
  276. package/template/apps/worker/src/processors/example-processor.ts +15 -0
  277. package/template/apps/worker/src/worker/redis.ts +19 -0
  278. package/template/apps/worker/src/worker/worker.ts +37 -0
  279. package/template/apps/worker/tests/example-processor.test.ts +13 -0
  280. package/template/apps/worker/tests/graceful-shutdown.test.ts +34 -0
  281. package/template/apps/worker/tsconfig.json +4 -0
  282. package/template/apps/worker/vitest.config.ts +13 -0
  283. package/template/bun.lock +3053 -0
  284. package/template/bunfig.toml +2 -0
  285. package/template/compose.yaml +53 -0
  286. package/template/docker-bake.hcl +86 -0
  287. package/template/package.json +47 -0
  288. package/template/packages/lint/knip.ts +40 -0
  289. package/template/packages/lint/oxfmt.ts +8 -0
  290. package/template/packages/lint/oxlint.ts +136 -0
  291. package/template/packages/lint/package.json +13 -0
  292. package/template/packages/queues/oxfmt.config.ts +8 -0
  293. package/template/packages/queues/oxlint.config.ts +7 -0
  294. package/template/packages/queues/package.json +15 -0
  295. package/template/packages/queues/src/example-job.ts +11 -0
  296. package/template/packages/queues/src/index.ts +3 -0
  297. package/template/packages/queues/src/queue-names.ts +2 -0
  298. package/template/packages/queues/src/redis-connection.ts +27 -0
  299. package/template/packages/queues/tsconfig.json +4 -0
  300. package/template/packages/tsconfig/base.json +23 -0
  301. package/template/packages/tsconfig/node.json +11 -0
  302. package/template/packages/tsconfig/package.json +4 -0
  303. package/template/turbo.json +50 -0
@@ -0,0 +1,24 @@
1
+ import { getTranslations } from "next-intl/server";
2
+ import { AuthCard } from "@/features/auth/auth-card";
3
+ import { SignInForm } from "@/features/auth/sign-in-form";
4
+ import { Link } from "@/i18n/navigation";
5
+
6
+ export default async function SignInPage() {
7
+ const t = await getTranslations("Auth.SignIn");
8
+
9
+ return (
10
+ <AuthCard
11
+ footer={
12
+ <>
13
+ {t("noAccount")}{" "}
14
+ <Link className="text-link font-medium hover:underline" href="/sign-up">
15
+ {t("signUpLink")}
16
+ </Link>
17
+ </>
18
+ }
19
+ title={t("title")}
20
+ >
21
+ <SignInForm />
22
+ </AuthCard>
23
+ );
24
+ }
@@ -0,0 +1,24 @@
1
+ import { getTranslations } from "next-intl/server";
2
+ import { AuthCard } from "@/features/auth/auth-card";
3
+ import { SignUpForm } from "@/features/auth/sign-up-form";
4
+ import { Link } from "@/i18n/navigation";
5
+
6
+ export default async function SignUpPage() {
7
+ const t = await getTranslations("Auth.SignUp");
8
+
9
+ return (
10
+ <AuthCard
11
+ footer={
12
+ <>
13
+ {t("haveAccount")}{" "}
14
+ <Link className="text-link font-medium hover:underline" href="/sign-in">
15
+ {t("signInLink")}
16
+ </Link>
17
+ </>
18
+ }
19
+ title={t("title")}
20
+ >
21
+ <SignUpForm />
22
+ </AuthCard>
23
+ );
24
+ }
@@ -0,0 +1,24 @@
1
+ import { getTranslations } from "next-intl/server";
2
+ import { Suspense } from "react";
3
+ import { AuthCard } from "@/features/auth/auth-card";
4
+ import { ResendVerificationButton } from "@/features/auth/resend-verification-button";
5
+ import { getSession } from "@/lib/auth";
6
+
7
+ /** Shown after sign-up: the API has mailed a verification link. */
8
+ export default async function VerifyPage() {
9
+ const t = await getTranslations("Auth.Verify");
10
+
11
+ return (
12
+ <AuthCard description={t("description")} title={t("title")}>
13
+ <Suspense>
14
+ <ResendVerification />
15
+ </Suspense>
16
+ </AuthCard>
17
+ );
18
+ }
19
+
20
+ async function ResendVerification() {
21
+ const session = await getSession();
22
+ if (!session || session.user.emailVerified) return null;
23
+ return <ResendVerificationButton email={session.user.email} />;
24
+ }
@@ -0,0 +1,34 @@
1
+ import { getTranslations } from "next-intl/server";
2
+ import { Suspense } from "react";
3
+ import { Alert } from "@/components/alert/alert";
4
+ import { PasskeySettings } from "@/features/auth/passkey-settings";
5
+ import { getSession } from "@/lib/auth";
6
+
7
+ /** Example protected page: the proxy redirects here only with a session. */
8
+ export default async function DashboardPage() {
9
+ const t = await getTranslations("Dashboard");
10
+
11
+ return (
12
+ <section className="space-y-4 pt-16">
13
+ <h1 className="text-3xl font-bold tracking-tight">{t("title")}</h1>
14
+ <Suspense>
15
+ <SessionSummary />
16
+ </Suspense>
17
+ <div className="pt-6">
18
+ <PasskeySettings />
19
+ </div>
20
+ </section>
21
+ );
22
+ }
23
+
24
+ async function SessionSummary() {
25
+ const [session, t] = await Promise.all([getSession(), getTranslations("Dashboard")]);
26
+ if (!session) return null;
27
+
28
+ return (
29
+ <div className="space-y-3">
30
+ <p className="text-muted-foreground">{t("signedInAs", { email: session.user.email })}</p>
31
+ {session.user.emailVerified ? null : <Alert message={t("unverified")} variant="warning" />}
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,60 @@
1
+ import { hasLocale, NextIntlClientProvider } from "next-intl";
2
+ import { Inter } from "next/font/google";
3
+ import { notFound } from "next/navigation";
4
+ import type { ReactNode } from "react";
5
+ import { Suspense } from "react";
6
+ import { Toaster } from "@/components/toast/toaster";
7
+ import { Provider } from "@/features/shared/provider";
8
+ import { HeaderFrame, SiteHeader } from "@/features/shared/site-header";
9
+ import { routing } from "@/i18n/routing";
10
+ import { constructMetadata } from "@/lib/metadata";
11
+ import { cn } from "@/lib/utils";
12
+ import "../globals.css";
13
+
14
+ const inter = Inter({
15
+ display: "swap",
16
+ subsets: ["latin", "latin-ext"],
17
+ variable: "--font-inter",
18
+ });
19
+
20
+ export const metadata = constructMetadata();
21
+ export const viewport = {
22
+ themeColor: [
23
+ { color: "white", media: "(prefers-color-scheme: light)" },
24
+ { color: "black", media: "(prefers-color-scheme: dark)" },
25
+ ],
26
+ };
27
+
28
+ type Props = {
29
+ children: ReactNode;
30
+ params: Promise<{ locale: string }>;
31
+ };
32
+
33
+ export function generateStaticParams() {
34
+ return routing.locales.map((locale) => ({ locale }));
35
+ }
36
+
37
+ export default async function LocaleLayout({ children, params }: Props) {
38
+ const { locale } = await params;
39
+ if (!hasLocale(routing.locales, locale)) {
40
+ notFound();
41
+ }
42
+
43
+ return (
44
+ <html lang={locale} suppressHydrationWarning>
45
+ <body className={cn("antialiased", inter.className)}>
46
+ <Provider>
47
+ <NextIntlClientProvider>
48
+ <Suspense fallback={<HeaderFrame />}>
49
+ <SiteHeader />
50
+ </Suspense>
51
+ <main className="mx-auto min-h-[calc(100dvh-4rem)] max-w-5xl px-4 pt-16">
52
+ {children}
53
+ </main>
54
+ </NextIntlClientProvider>
55
+ </Provider>
56
+ <Toaster />
57
+ </body>
58
+ </html>
59
+ );
60
+ }
@@ -0,0 +1,23 @@
1
+ import { getTranslations } from "next-intl/server";
2
+ import { buttonVariants } from "@/components/button/button-variants"; // @if auth
3
+ import { GoogleOneTap } from "@/features/auth/google-one-tap"; // @if auth
4
+ import { Link } from "@/i18n/navigation"; // @if auth
5
+
6
+ export default async function HomePage() {
7
+ const t = await getTranslations("Home");
8
+
9
+ return (
10
+ <section className="flex flex-col items-center gap-6 pt-28 text-center md:pt-40">
11
+ <h1 className="max-w-2xl text-4xl font-bold tracking-tight text-balance md:text-5xl">
12
+ {t("title")}
13
+ </h1>
14
+ <p className="text-muted-foreground max-w-xl text-base md:text-lg">{t("description")}</p>
15
+ {/* @if auth */}
16
+ <GoogleOneTap />
17
+ <Link className={buttonVariants({ size: "lg" })} href="/sign-up">
18
+ {t("cta")}
19
+ </Link>
20
+ {/* @endif */}
21
+ </section>
22
+ );
23
+ }
@@ -0,0 +1,3 @@
1
+ export function GET() {
2
+ return new Response("OK");
3
+ }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import NextError from "next/error";
4
+ import { useEffect } from "react";
5
+ import { routing } from "@/i18n/routing";
6
+
7
+ export default function GlobalError({ error }: Readonly<{ error: Error }>) {
8
+ useEffect(() => {
9
+ console.error("Unhandled application error:", error);
10
+ }, [error]);
11
+
12
+ return (
13
+ <html lang={routing.defaultLocale}>
14
+ <body>
15
+ <NextError statusCode={0} />
16
+ </body>
17
+ </html>
18
+ );
19
+ }
@@ -0,0 +1,98 @@
1
+ @import "tailwindcss";
2
+ @plugin 'tailwindcss-animate';
3
+ @custom-variant dark (&:is(.dark *));
4
+
5
+ /*
6
+ * Semantic tokens. Components use these (bg-background, text-muted-foreground,
7
+ * border-border, …), never raw hex or ramp steps, so light and dark stay in sync.
8
+ * The .dark block below redefines every token.
9
+ */
10
+ @theme {
11
+ --color-brand-50: #eef3ff;
12
+ --color-brand-100: #d9e4ff;
13
+ --color-brand-200: #bacdff;
14
+ --color-brand-300: #8dabff;
15
+ --color-brand-400: #5b82ff;
16
+ --color-brand-500: #2f5cf7;
17
+ --color-brand-600: #1f47e0;
18
+ --color-brand-700: #1a39bd;
19
+
20
+ --color-background: #f5f5f7;
21
+ --color-foreground: #1d1d1f;
22
+ --color-card: #ffffff;
23
+ --color-card-foreground: #1d1d1f;
24
+ --color-popover: #ffffff;
25
+ --color-popover-foreground: #1d1d1f;
26
+ --color-primary: var(--color-brand-500);
27
+ --color-primary-hover: var(--color-brand-600);
28
+ --color-primary-foreground: #ffffff;
29
+ --color-secondary: #e8e8ed;
30
+ --color-secondary-foreground: #1d1d1f;
31
+ --color-muted: #e8e8ed;
32
+ --color-muted-foreground: #636366;
33
+ --color-accent: #eff2f7;
34
+ --color-accent-foreground: #1d1d1f;
35
+ --color-link: var(--color-brand-600);
36
+ --color-destructive: #be123c;
37
+ --color-destructive-solid: #e11d48;
38
+ --color-destructive-foreground: #ffffff;
39
+ --color-success: #16a34a;
40
+ --color-border: #d2d2d7;
41
+ --color-input: #c7c7cc;
42
+ --color-ring: var(--color-brand-400);
43
+
44
+ --radius-sm: 0.5rem;
45
+ --radius-md: 0.625rem;
46
+ --radius-lg: 0.75rem;
47
+ --radius-xl: 1rem;
48
+ --radius-2xl: 1.25rem;
49
+ }
50
+
51
+ @layer theme {
52
+ .dark {
53
+ --color-brand-400: #3d9dff;
54
+ --color-brand-500: #0a84ff;
55
+ --color-brand-600: #0a6fd8;
56
+
57
+ --color-background: #000000;
58
+ --color-foreground: #f5f5f7;
59
+ --color-card: #1c1c1e;
60
+ --color-card-foreground: #f5f5f7;
61
+ --color-popover: #1c1c1e;
62
+ --color-popover-foreground: #f5f5f7;
63
+ --color-primary: var(--color-brand-500);
64
+ --color-primary-hover: var(--color-brand-400);
65
+ --color-primary-foreground: #ffffff;
66
+ --color-secondary: #2c2c2e;
67
+ --color-secondary-foreground: #f2f2f7;
68
+ --color-muted: #2c2c2e;
69
+ --color-muted-foreground: #aeaeb2;
70
+ --color-accent: #2c2c2e;
71
+ --color-accent-foreground: #f2f2f7;
72
+ --color-link: #6bb5ff;
73
+ --color-destructive: #fb7185;
74
+ --color-destructive-solid: #be123c;
75
+ --color-destructive-foreground: #ffffff;
76
+ --color-success: #4ade80;
77
+ --color-border: #3a3a3c;
78
+ --color-input: #48484a;
79
+ --color-ring: #5e9cff;
80
+ }
81
+ }
82
+
83
+ @layer base {
84
+ *,
85
+ ::before,
86
+ ::after {
87
+ border-color: var(--color-border);
88
+ }
89
+ html {
90
+ @apply bg-background;
91
+ }
92
+ body {
93
+ @apply bg-background text-foreground;
94
+ }
95
+ button {
96
+ cursor: pointer;
97
+ }
98
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="8" fill="#2f5cf7"/><path d="M10 22V10h2.4l7.2 8.2V10H22v12h-2.4l-7.2-8.2V22z" fill="#fff"/></svg>
@@ -0,0 +1,15 @@
1
+ import type { MetadataRoute } from "next";
2
+ import { env } from "@/env";
3
+
4
+ export default function robots(): MetadataRoute.Robots {
5
+ const base = env.NEXT_PUBLIC_HOST_URL.replace(/\/$/, "");
6
+
7
+ return {
8
+ rules: {
9
+ allow: "/",
10
+ disallow: "/*/dashboard", // @if auth
11
+ userAgent: "*",
12
+ },
13
+ sitemap: `${base}/sitemap.xml`,
14
+ };
15
+ }
@@ -0,0 +1,24 @@
1
+ import type { MetadataRoute } from "next";
2
+ import { env } from "@/env";
3
+ import { routing } from "@/i18n/routing";
4
+
5
+ /** Public, indexable pages (locale prefix is added per locale). */
6
+ const staticPaths = [
7
+ "",
8
+ // @if auth
9
+ "/sign-in",
10
+ "/sign-up",
11
+ // @endif
12
+ ];
13
+
14
+ export default function sitemap(): MetadataRoute.Sitemap {
15
+ const base = env.NEXT_PUBLIC_HOST_URL.replace(/\/$/, "");
16
+ const lastModified = new Date();
17
+
18
+ return routing.locales.flatMap((locale) =>
19
+ staticPaths.map((path) => ({
20
+ lastModified,
21
+ url: `${base}/${locale}${path}`,
22
+ })),
23
+ );
24
+ }
@@ -0,0 +1,25 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { withContainer } from "~sb/decorators";
3
+ import { Alert } from "./alert";
4
+
5
+ const meta = {
6
+ args: { message: "Your changes were saved." },
7
+ component: Alert,
8
+ decorators: [withContainer()],
9
+ tags: ["autodocs"],
10
+ } satisfies Meta<typeof Alert>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const Info: Story = {};
16
+
17
+ export const Error: Story = {
18
+ args: { message: "Invalid email or password.", variant: "error" },
19
+ };
20
+
21
+ export const Success: Story = { args: { variant: "success" } };
22
+
23
+ export const Warning: Story = {
24
+ args: { message: "Your session expires soon.", variant: "warning" },
25
+ };
@@ -0,0 +1,50 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import { cva } from "class-variance-authority";
3
+ import { AlertCircle, AlertTriangle, CheckCircle2, Info } from "lucide-react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const alertVariants = cva(
7
+ "mb-4 flex items-center gap-2.5 overflow-hidden rounded-xl border border-border/60 border-l-[3px] bg-card px-3.5 py-2.5 text-foreground shadow-xs dark:border-white/10",
8
+ {
9
+ defaultVariants: {
10
+ variant: "default",
11
+ },
12
+ variants: {
13
+ variant: {
14
+ default:
15
+ "border-l-blue-500 [&>svg]:text-blue-500 dark:border-l-blue-400 dark:[&>svg]:text-blue-400",
16
+ error:
17
+ "border-l-red-500 [&>svg]:text-red-500 dark:border-l-red-400 dark:[&>svg]:text-red-400",
18
+ success:
19
+ "border-l-emerald-500 [&>svg]:text-emerald-500 dark:border-l-emerald-400 dark:[&>svg]:text-emerald-400",
20
+ warning:
21
+ "border-l-amber-500 [&>svg]:text-amber-500 dark:border-l-amber-400 dark:[&>svg]:text-amber-400",
22
+ },
23
+ },
24
+ },
25
+ );
26
+
27
+ type AlertProps = React.HTMLAttributes<HTMLDivElement> &
28
+ VariantProps<typeof alertVariants> & {
29
+ message: string;
30
+ };
31
+
32
+ function Alert({ className, message, variant = "default", ...props }: AlertProps) {
33
+ const Icon = {
34
+ default: Info,
35
+ error: AlertTriangle,
36
+ success: CheckCircle2,
37
+ warning: AlertCircle,
38
+ }[variant || "default"];
39
+
40
+ return (
41
+ <div className={cn(alertVariants({ variant }), className)} {...props}>
42
+ <Icon className="size-[18px] shrink-0" />
43
+ <p className="text-[13px] leading-snug font-medium">{message}</p>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ Alert.displayName = "Alert";
49
+
50
+ export { Alert };
@@ -0,0 +1,116 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { expect, within } from "storybook/test";
3
+ import { Avatar } from "./avatar";
4
+
5
+ const meta = {
6
+ argTypes: {
7
+ color: {
8
+ control: "select",
9
+ options: [
10
+ "red",
11
+ "orange",
12
+ "amber",
13
+ "yellow",
14
+ "lime",
15
+ "green",
16
+ "emerald",
17
+ "cyan",
18
+ "blue",
19
+ "indigo",
20
+ "purple",
21
+ "pink",
22
+ ],
23
+ },
24
+ size: {
25
+ control: "select",
26
+ options: ["sm", "md", "lg", "xl"],
27
+ },
28
+ },
29
+ component: Avatar,
30
+ parameters: {
31
+ layout: "centered",
32
+ },
33
+ tags: ["autodocs"],
34
+ } satisfies Meta<typeof Avatar>;
35
+
36
+ export default meta;
37
+ type Story = StoryObj<typeof meta>;
38
+
39
+ export const Default: Story = {
40
+ args: {
41
+ fallback: "H",
42
+ },
43
+ play: async ({ canvasElement }) => {
44
+ await expect(within(canvasElement).getByText("H")).toBeInTheDocument();
45
+ },
46
+ };
47
+
48
+ export const Small: Story = {
49
+ args: {
50
+ fallback: "S",
51
+ size: "sm",
52
+ },
53
+ };
54
+
55
+ export const Medium: Story = {
56
+ args: {
57
+ fallback: "M",
58
+ size: "md",
59
+ },
60
+ };
61
+
62
+ export const Large: Story = {
63
+ args: {
64
+ fallback: "L",
65
+ size: "lg",
66
+ },
67
+ };
68
+
69
+ export const ExtraLarge: Story = {
70
+ args: {
71
+ fallback: "X",
72
+ size: "xl",
73
+ },
74
+ };
75
+
76
+ export const WithColor: Story = {
77
+ args: {
78
+ color: "blue",
79
+ fallback: "H",
80
+ },
81
+ };
82
+
83
+ export const RainbowColors: Story = {
84
+ render: () => (
85
+ <div className="flex flex-wrap gap-3">
86
+ <Avatar color="red" fallback="R" />
87
+ <Avatar color="orange" fallback="O" />
88
+ <Avatar color="amber" fallback="A" />
89
+ <Avatar color="yellow" fallback="Y" />
90
+ <Avatar color="lime" fallback="L" />
91
+ <Avatar color="green" fallback="G" />
92
+ <Avatar color="emerald" fallback="E" />
93
+ <Avatar color="cyan" fallback="C" />
94
+ <Avatar color="blue" fallback="B" />
95
+ <Avatar color="indigo" fallback="I" />
96
+ <Avatar color="purple" fallback="P" />
97
+ <Avatar color="pink" fallback="K" />
98
+ </div>
99
+ ),
100
+ };
101
+
102
+ export const Group: Story = {
103
+ render: () => (
104
+ <div className="flex -space-x-2">
105
+ {(["red", "green", "blue", "purple"] as const).map((color, i) => (
106
+ <Avatar
107
+ className="outline-background outline-2"
108
+ color={color}
109
+ fallback={["H", "A", "C", "3"][i]}
110
+ key={color}
111
+ size="md"
112
+ />
113
+ ))}
114
+ </div>
115
+ ),
116
+ };
@@ -0,0 +1,88 @@
1
+ "use client";
2
+
3
+ import type { HTMLAttributes, RefObject } from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ export type AvatarProps = HTMLAttributes<HTMLDivElement> & {
7
+ className?: string;
8
+ color?: AvatarColor;
9
+ fallback?: string;
10
+ size?: "lg" | "md" | "sm" | "xl";
11
+ };
12
+
13
+ type AvatarColor =
14
+ | "amber"
15
+ | "blue"
16
+ | "cyan"
17
+ | "emerald"
18
+ | "green"
19
+ | "indigo"
20
+ | "lime"
21
+ | "orange"
22
+ | "pink"
23
+ | "purple"
24
+ | "red"
25
+ | "yellow";
26
+
27
+ const sizeMap: Record<string, string> = {
28
+ lg: "h-12 w-12 text-sm",
29
+ md: "h-10 w-10 text-xs",
30
+ sm: "h-8 w-8 text-[0.625rem]",
31
+ xl: "h-14 w-14 text-base",
32
+ };
33
+
34
+ const colorMap: Record<AvatarColor, string> = {
35
+ amber: "bg-amber-500 text-white ring-amber-600/20 dark:bg-amber-600 dark:ring-amber-400/25",
36
+ blue: "bg-brand-500 text-white ring-brand-600/20 dark:bg-brand-500 dark:ring-brand-400/25",
37
+ cyan: "bg-cyan-500 text-white ring-cyan-600/20 dark:bg-cyan-600 dark:ring-cyan-400/25",
38
+ emerald:
39
+ "bg-emerald-500 text-white ring-emerald-600/20 dark:bg-emerald-600 dark:ring-emerald-400/25",
40
+ green: "bg-green-500 text-white ring-green-600/20 dark:bg-green-600 dark:ring-green-400/25",
41
+ indigo: "bg-indigo-500 text-white ring-indigo-600/20 dark:bg-indigo-600 dark:ring-indigo-400/25",
42
+ lime: "bg-lime-600 text-white ring-lime-700/20 dark:bg-lime-700 dark:ring-lime-400/25",
43
+ orange: "bg-orange-500 text-white ring-orange-600/20 dark:bg-orange-600 dark:ring-orange-400/25",
44
+ pink: "bg-pink-500 text-white ring-pink-600/20 dark:bg-pink-600 dark:ring-pink-400/25",
45
+ purple: "bg-purple-500 text-white ring-purple-600/20 dark:bg-purple-600 dark:ring-purple-400/25",
46
+ red: "bg-red-500 text-white ring-red-600/20 dark:bg-red-600 dark:ring-red-400/25",
47
+ yellow: "bg-yellow-600 text-white ring-yellow-700/20 dark:bg-yellow-700 dark:ring-yellow-400/25",
48
+ };
49
+
50
+ const neutralColor = "bg-muted text-foreground/80 ring-border/70";
51
+
52
+ function Avatar({ ref, ...props }: AvatarProps & { ref?: RefObject<HTMLDivElement | null> }) {
53
+ const { className, color, fallback = "A", size = "md", ...divProps } = props;
54
+
55
+ const colors = color ? colorMap[color] : neutralColor;
56
+
57
+ const getInitials = (text: string): string => {
58
+ const parts = text.split(/\s+/);
59
+ if (parts.length > 1) {
60
+ const first = parts[0].charAt(0);
61
+ const last = (parts.at(-1) ?? "0").charAt(0);
62
+ return `${first}${last}`.toUpperCase();
63
+ }
64
+
65
+ return text.slice(0, 2).toUpperCase();
66
+ };
67
+
68
+ return (
69
+ <div
70
+ className={cn(
71
+ "inline-flex shrink-0 select-none items-center justify-center rounded-full",
72
+ "font-semibold tracking-tight ring-1 ring-inset cursor-pointer",
73
+
74
+ sizeMap[size],
75
+ colors,
76
+ className,
77
+ )}
78
+ ref={ref}
79
+ {...divProps}
80
+ >
81
+ {getInitials(fallback)}
82
+ </div>
83
+ );
84
+ }
85
+
86
+ Avatar.displayName = "Avatar";
87
+
88
+ export { Avatar };
@@ -0,0 +1,28 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ export const buttonVariants = cva(
4
+ "inline-flex cursor-pointer items-center justify-center gap-2 rounded-full border text-sm font-semibold transition-[background-color,border-color,box-shadow] duration-200 outline-none select-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-45",
5
+ {
6
+ defaultVariants: {
7
+ size: "default",
8
+ variant: "primary",
9
+ },
10
+ variants: {
11
+ size: {
12
+ default: "h-11 px-4 py-2",
13
+ icon: "size-9 p-0",
14
+ lg: "h-12 px-8 text-base",
15
+ sm: "h-9 px-3",
16
+ },
17
+ variant: {
18
+ destructive:
19
+ "border-transparent bg-destructive-solid text-destructive-foreground hover:bg-destructive-solid/90",
20
+ ghost: "border-transparent bg-transparent text-foreground hover:bg-foreground/8",
21
+ outline: "border-border bg-card text-foreground shadow-xs hover:bg-accent",
22
+ primary:
23
+ "border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary-hover",
24
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-accent",
25
+ },
26
+ },
27
+ },
28
+ );