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,237 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { Calendar, Copy, Download, Info, Save, Settings, Trash2 } from "lucide-react";
3
+ import { expect, userEvent, within } from "storybook/test";
4
+ import { Button } from "@/components/button/button";
5
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
6
+
7
+ const meta: Meta<typeof Tooltip> = {
8
+ component: Tooltip,
9
+ decorators: [
10
+ (Story) => (
11
+ <TooltipProvider>
12
+ <Story />
13
+ </TooltipProvider>
14
+ ),
15
+ ],
16
+ parameters: {
17
+ layout: "centered",
18
+ },
19
+ tags: ["autodocs"],
20
+ } satisfies Meta<typeof Tooltip>;
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Default: Story = {
26
+ play: async ({ canvasElement }) => {
27
+ await userEvent.hover(within(canvasElement).getByRole("button", { name: "Information" }));
28
+
29
+ await expect(
30
+ await within(document.body).findByText("Click to view more details"),
31
+ ).toBeInTheDocument();
32
+ },
33
+ render: () => (
34
+ <Tooltip>
35
+ <TooltipTrigger asChild>
36
+ <Button variant="outline">
37
+ <Info className="h-4 w-4" />
38
+ Information
39
+ </Button>
40
+ </TooltipTrigger>
41
+ <TooltipContent>
42
+ <p>Click to view more details</p>
43
+ </TooltipContent>
44
+ </Tooltip>
45
+ ),
46
+ };
47
+
48
+ export const IconButton: Story = {
49
+ render: () => (
50
+ <Tooltip>
51
+ <TooltipTrigger asChild>
52
+ <Button aria-label="Settings" size="icon" variant="ghost">
53
+ <Settings className="h-4 w-4" />
54
+ </Button>
55
+ </TooltipTrigger>
56
+ <TooltipContent>
57
+ <p>Settings</p>
58
+ </TooltipContent>
59
+ </Tooltip>
60
+ ),
61
+ };
62
+
63
+ export const Positions: Story = {
64
+ render: () => (
65
+ <div className="grid grid-cols-3 gap-8">
66
+ <div />
67
+ <Tooltip>
68
+ <TooltipTrigger asChild>
69
+ <Button aria-label="Top position" size="icon" variant="outline">
70
+ <Calendar className="h-4 w-4" />
71
+ </Button>
72
+ </TooltipTrigger>
73
+ <TooltipContent side="top">
74
+ <p>Top position</p>
75
+ </TooltipContent>
76
+ </Tooltip>
77
+ <div />
78
+
79
+ <Tooltip>
80
+ <TooltipTrigger asChild>
81
+ <Button aria-label="Left position" size="icon" variant="outline">
82
+ <Calendar className="h-4 w-4" />
83
+ </Button>
84
+ </TooltipTrigger>
85
+ <TooltipContent side="left">
86
+ <p>Left position</p>
87
+ </TooltipContent>
88
+ </Tooltip>
89
+ <div />
90
+ <Tooltip>
91
+ <TooltipTrigger asChild>
92
+ <Button aria-label="Right position" size="icon" variant="outline">
93
+ <Calendar className="h-4 w-4" />
94
+ </Button>
95
+ </TooltipTrigger>
96
+ <TooltipContent side="right">
97
+ <p>Right position</p>
98
+ </TooltipContent>
99
+ </Tooltip>
100
+
101
+ <div />
102
+ <Tooltip>
103
+ <TooltipTrigger asChild>
104
+ <Button aria-label="Bottom position" size="icon" variant="outline">
105
+ <Calendar className="h-4 w-4" />
106
+ </Button>
107
+ </TooltipTrigger>
108
+ <TooltipContent side="bottom">
109
+ <p>Bottom position</p>
110
+ </TooltipContent>
111
+ </Tooltip>
112
+ <div />
113
+ </div>
114
+ ),
115
+ };
116
+
117
+ export const ActionButtons: Story = {
118
+ render: () => (
119
+ <div className="flex gap-2">
120
+ <Tooltip>
121
+ <TooltipTrigger asChild>
122
+ <Button aria-label="Copy to clipboard" size="icon" variant="ghost">
123
+ <Copy className="h-4 w-4" />
124
+ </Button>
125
+ </TooltipTrigger>
126
+ <TooltipContent>
127
+ <p>Copy to clipboard</p>
128
+ </TooltipContent>
129
+ </Tooltip>
130
+
131
+ <Tooltip>
132
+ <TooltipTrigger asChild>
133
+ <Button aria-label="Download file" size="icon" variant="ghost">
134
+ <Download className="h-4 w-4" />
135
+ </Button>
136
+ </TooltipTrigger>
137
+ <TooltipContent>
138
+ <p>Download file</p>
139
+ </TooltipContent>
140
+ </Tooltip>
141
+
142
+ <Tooltip>
143
+ <TooltipTrigger asChild>
144
+ <Button aria-label="Save changes" size="icon" variant="ghost">
145
+ <Save className="h-4 w-4" />
146
+ </Button>
147
+ </TooltipTrigger>
148
+ <TooltipContent>
149
+ <p>Save changes</p>
150
+ </TooltipContent>
151
+ </Tooltip>
152
+
153
+ <Tooltip>
154
+ <TooltipTrigger asChild>
155
+ <Button aria-label="Delete permanently" size="icon" variant="ghost">
156
+ <Trash2 className="h-4 w-4 text-red-500" />
157
+ </Button>
158
+ </TooltipTrigger>
159
+ <TooltipContent>
160
+ <p className="text-red-500">Delete permanently</p>
161
+ </TooltipContent>
162
+ </Tooltip>
163
+ </div>
164
+ ),
165
+ };
166
+
167
+ export const WithKeyboardShortcut: Story = {
168
+ render: () => (
169
+ <Tooltip>
170
+ <TooltipTrigger asChild>
171
+ <Button variant="outline">
172
+ <Save className="h-4 w-4" />
173
+ Save
174
+ </Button>
175
+ </TooltipTrigger>
176
+ <TooltipContent>
177
+ <div className="flex items-center gap-2">
178
+ <span>Save document</span>
179
+ <kbd className="border-border bg-muted pointer-events-none inline-flex h-5 items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 select-none">
180
+ <span className="text-xs">⌘</span>S
181
+ </kbd>
182
+ </div>
183
+ </TooltipContent>
184
+ </Tooltip>
185
+ ),
186
+ };
187
+
188
+ export const DetailedInformation: Story = {
189
+ render: () => (
190
+ <div className="py-12">
191
+ <Tooltip>
192
+ <TooltipTrigger asChild>
193
+ <Button variant="outline">
194
+ <Info className="h-4 w-4" />
195
+ Storage Info
196
+ </Button>
197
+ </TooltipTrigger>
198
+ <TooltipContent className="max-w-xs">
199
+ <div className="space-y-1">
200
+ <p className="font-semibold">Storage Usage</p>
201
+ <p className="text-muted-foreground text-xs">
202
+ You've used 4.2 GB of your 10 GB storage limit. Upgrade to get more space.
203
+ </p>
204
+ </div>
205
+ </TooltipContent>
206
+ </Tooltip>
207
+ </div>
208
+ ),
209
+ };
210
+
211
+ export const CustomOffset: Story = {
212
+ render: () => (
213
+ <div className="flex gap-4 py-6">
214
+ <Tooltip>
215
+ <TooltipTrigger asChild>
216
+ <Button size="sm" variant="outline">
217
+ Default offset
218
+ </Button>
219
+ </TooltipTrigger>
220
+ <TooltipContent>
221
+ <p>5px offset (default)</p>
222
+ </TooltipContent>
223
+ </Tooltip>
224
+
225
+ <Tooltip>
226
+ <TooltipTrigger asChild>
227
+ <Button size="sm" variant="outline">
228
+ Large offset
229
+ </Button>
230
+ </TooltipTrigger>
231
+ <TooltipContent sideOffset={20}>
232
+ <p>20px offset</p>
233
+ </TooltipContent>
234
+ </Tooltip>
235
+ </div>
236
+ ),
237
+ };
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
4
+ import type { ComponentPropsWithoutRef, ComponentRef, RefObject } from "react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function TooltipProvider({
8
+ delayDuration = 0,
9
+ ...props
10
+ }: ComponentPropsWithoutRef<typeof TooltipPrimitive.Provider>) {
11
+ const Provider = TooltipPrimitive.Provider;
12
+ return <Provider delayDuration={delayDuration} {...props} />;
13
+ }
14
+ TooltipProvider.displayName = "TooltipProvider";
15
+
16
+ const Tooltip = TooltipPrimitive.Root;
17
+
18
+ const TooltipTrigger = TooltipPrimitive.Trigger;
19
+
20
+ const TooltipPortal = TooltipPrimitive.Portal;
21
+
22
+ function TooltipContent({
23
+ className,
24
+ ref,
25
+ sideOffset = 4,
26
+ ...props
27
+ }: ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
28
+ ref?: RefObject<ComponentRef<typeof TooltipPrimitive.Content> | null>;
29
+ }) {
30
+ return (
31
+ <TooltipPrimitive.Content
32
+ className={cn(
33
+ "z-50 overflow-hidden rounded-lg px-3 py-1.5",
34
+ "border-border bg-popover text-popover-foreground border",
35
+ "text-sm text-foreground",
36
+ "shadow-lg shadow-black/8",
37
+ "animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
38
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
39
+ className,
40
+ )}
41
+ ref={ref}
42
+ sideOffset={sideOffset}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
48
+
49
+ export { Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger };
@@ -0,0 +1,59 @@
1
+ import "server-only";
2
+ import { env } from "@/env";
3
+
4
+ const isDev = process.env.NODE_ENV === "development";
5
+
6
+ // next-themes 0.4.6 theme-init inline <script> for
7
+ // <ThemeProvider attribute="class" disableTransitionOnChange /> (see
8
+ // src/features/shared/provider.tsx). It gets no per-request nonce: reading one
9
+ // via `headers()` in the layout tree above the page blocks cacheComponents'
10
+ // static shell. Its content is fixed for our props, so a hash allowlist is the
11
+ // correct CSP tool here.
12
+ //
13
+ // Turbopack renders this script differently per mode, so both are listed.
14
+ // Regenerate when next-themes is upgraded, Turbopack's output changes, or the
15
+ // ThemeProvider props change (tests/csp-theme-script.spec.ts fails loudly if a
16
+ // hash goes stale):
17
+ // dev: next dev -> curl /en, extract the <script> containing "colorScheme"
18
+ // prod: next build && next start -> same extraction
19
+ // hash: sha256(scriptText), base64
20
+ const themeScriptHashes = [
21
+ "'sha256-rbbnijHn7DZ6ps39myQ3cVQF1H+U/PJfHh5ei/Q2kb8='", // dev
22
+ "'sha256-n46vPwSWuMC0W703pBofImv82Z26xo4LXymv0E9caPk='", // prod
23
+ ];
24
+
25
+ const inlineScriptHashes = [
26
+ "'sha256-7mu4H06fwDCjmnxxr/xNHyuQC6pLTHr4M2E4jXw5WZs='",
27
+ ...themeScriptHashes,
28
+ ];
29
+
30
+ /** Origins the browser may call: the API (GraphQL + auth). Add CDNs/buckets here (and to img-src). */
31
+ const connectSrc = [env.NEXT_PUBLIC_API_URL];
32
+
33
+ /** Third-party origins that serve scripts, styles and iframes (widgets, embeds). */
34
+ const thirdPartySrc: string[] = [];
35
+ // @if auth
36
+ // Google Identity Services (One Tap) loads its script, styles and iframe from here.
37
+ if (env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) thirdPartySrc.push("https://accounts.google.com/gsi/");
38
+ // @endif
39
+
40
+ export function generateCspHeader({ nonce }: { nonce: string }) {
41
+ const isHttps = !isDev && env.NEXT_PUBLIC_HOST_URL.startsWith("https://");
42
+
43
+ return `
44
+ default-src 'self';
45
+ connect-src 'self' ${[...connectSrc, ...thirdPartySrc].join(" ")} ${isDev ? "localhost:* 127.0.0.1:* ws://localhost:*" : ""};
46
+ script-src 'self' 'nonce-${nonce}' ${inlineScriptHashes.join(" ")} ${thirdPartySrc.join(" ")} ${isDev ? "'unsafe-eval'" : ""};
47
+ style-src 'self' 'unsafe-inline' ${thirdPartySrc.join(" ")};
48
+ frame-src 'self' ${thirdPartySrc.join(" ")};
49
+ img-src 'self' blob: data:;
50
+ font-src 'self';
51
+ object-src 'none';
52
+ base-uri 'self';
53
+ form-action 'self';
54
+ frame-ancestors 'none';
55
+ ${isHttps ? "upgrade-insecure-requests;" : ""}
56
+ `
57
+ .replaceAll(/\s{2,}/g, " ")
58
+ .trim();
59
+ }
@@ -0,0 +1,14 @@
1
+ import { routing } from "@/i18n/routing";
2
+
3
+ /** Paths reachable without a session. Everything else redirects to sign-in. */
4
+ const publicRoutes = ["/", "/sign-in", "/sign-up", "/recovery", "/reset-password", "/verify"];
5
+
6
+ /** Pages a signed-in user is bounced away from (to the dashboard). */
7
+ export const guestOnlyRoutes = ["/sign-in", "/sign-up"];
8
+
9
+ export const publicPaths = new Set([
10
+ ...publicRoutes,
11
+ ...routing.locales.flatMap((locale) =>
12
+ publicRoutes.map((route) => (route === "/" ? `/${locale}` : `/${locale}${route}`)),
13
+ ),
14
+ ]);
@@ -0,0 +1,5 @@
1
+ export const siteConfig = {
2
+ description: "Built with the Nexst monorepo template.",
3
+ name: "Nexst",
4
+ url: "https://example.com",
5
+ };
@@ -0,0 +1,15 @@
1
+ import { createEnv } from "@t3-oss/env-nextjs";
2
+ import { z } from "zod";
3
+
4
+ export const env = createEnv({
5
+ client: {
6
+ NEXT_PUBLIC_API_URL: z.url().default("https://api.example.com"),
7
+ NEXT_PUBLIC_GOOGLE_CLIENT_ID: z.string().min(1).optional(), // @if auth
8
+ NEXT_PUBLIC_HOST_URL: z.url().default("https://example.com"),
9
+ },
10
+ runtimeEnv: {
11
+ NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
12
+ NEXT_PUBLIC_GOOGLE_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID, // @if auth
13
+ NEXT_PUBLIC_HOST_URL: process.env.NEXT_PUBLIC_HOST_URL,
14
+ },
15
+ });
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /** Centered frame shared by every auth page. */
4
+ export function AuthCard({
5
+ children,
6
+ description,
7
+ footer,
8
+ title,
9
+ }: Readonly<{ children: ReactNode; description?: string; footer?: ReactNode; title: string }>) {
10
+ return (
11
+ <div className="mx-auto flex w-full max-w-sm flex-col items-center gap-6 pt-20 md:pt-32">
12
+ <div className="space-y-1.5 text-center">
13
+ <h1 className="text-3xl font-bold tracking-tight">{title}</h1>
14
+ {description ? <p className="text-muted-foreground text-sm">{description}</p> : null}
15
+ </div>
16
+ {children}
17
+ {footer ? <div className="text-muted-foreground text-sm">{footer}</div> : null}
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,14 @@
1
+ import { useTranslations } from "next-intl";
2
+
3
+ /** "or" rule between the email form and the alternative sign-in methods. */
4
+ export function AuthDivider() {
5
+ const t = useTranslations("Auth");
6
+
7
+ return (
8
+ <div className="text-muted-foreground my-5 flex items-center gap-3 text-xs">
9
+ <span className="bg-border h-px flex-1" />
10
+ {t("divider")}
11
+ <span className="bg-border h-px flex-1" />
12
+ </div>
13
+ );
14
+ }
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ import { useMutation } from "@tanstack/react-query";
4
+ import { useTranslations } from "next-intl";
5
+ import { Button } from "@/components/button/button";
6
+ import { Icons } from "@/components/icons";
7
+ import { authClient, type AuthError, isGoogleEnabled } from "@/lib/auth-client";
8
+ import { getReturnTo } from "./lib/return-to";
9
+
10
+ type Props = {
11
+ disabled?: boolean;
12
+ onError: (error: AuthError) => void;
13
+ };
14
+
15
+ /**
16
+ * "Continue with Google" below the email form. Renders nothing unless
17
+ * `NEXT_PUBLIC_GOOGLE_CLIENT_ID` is set (the API needs `GOOGLE_CLIENT_ID` and
18
+ * `GOOGLE_CLIENT_SECRET`). Sign-in and sign-up are the same flow: unknown
19
+ * Google accounts are created, known emails are linked.
20
+ */
21
+ export function GoogleAuthButton({ disabled, onError }: Props) {
22
+ const t = useTranslations("Auth.Google");
23
+
24
+ const { isPending, mutate } = useMutation<void, AuthError>({
25
+ mutationFn: async () => {
26
+ const { error } = await authClient.signIn.social({
27
+ callbackURL: new URL(getReturnTo() ?? "/dashboard", window.location.origin).toString(),
28
+ provider: "google",
29
+ });
30
+ if (error) throw error;
31
+ },
32
+ onError,
33
+ });
34
+
35
+ if (!isGoogleEnabled) return null;
36
+
37
+ return (
38
+ <Button
39
+ className="w-full"
40
+ disabled={disabled}
41
+ isLoading={isPending}
42
+ onClick={() => mutate()}
43
+ variant="outline"
44
+ >
45
+ {isPending ? null : <Icons.Google className="size-5" />}
46
+ {t("continueButton")}
47
+ </Button>
48
+ );
49
+ }
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { authClient, isGoogleEnabled } from "@/lib/auth-client";
5
+
6
+ /** Shows Google One Tap to signed-out visitors. Renders nothing itself. */
7
+ export function GoogleOneTap() {
8
+ useEffect(() => {
9
+ if (!isGoogleEnabled) return;
10
+
11
+ let cancelled = false;
12
+ void authClient
13
+ .getSession()
14
+ .then(({ data }) => (data || cancelled ? undefined : authClient.oneTap()))
15
+ .catch(() => {});
16
+
17
+ return () => {
18
+ cancelled = true;
19
+ };
20
+ }, []);
21
+
22
+ return null;
23
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { AuthError } from "@/lib/auth-client";
3
+ import { authFallbackMessage } from "./auth-error";
4
+
5
+ describe("authFallbackMessage", () => {
6
+ const t = (key: string) => `translated:${key}`;
7
+
8
+ it("replaces the browser wording of a dropped connection with translated copy", () => {
9
+ const error = new TypeError("NetworkError when attempting to fetch resource.");
10
+ expect(authFallbackMessage(error as unknown as AuthError, t)).toBe("translated:networkError");
11
+ });
12
+
13
+ it("shows the server's message for any other failure", () => {
14
+ const error: AuthError = { message: "Too many attempts", status: 429, statusText: "" };
15
+ expect(authFallbackMessage(error, t)).toBe("Too many attempts");
16
+ });
17
+
18
+ it("falls back to the generic error when there is no message", () => {
19
+ const error: AuthError = { status: 500, statusText: "" };
20
+ expect(authFallbackMessage(error, t)).toBe("translated:defaultError");
21
+ });
22
+ });
@@ -0,0 +1,16 @@
1
+ import type { AuthError } from "@/lib/auth-client";
2
+ import { isNetworkError } from "@/lib/network-error";
3
+
4
+ /**
5
+ * Copy for an auth failure that has no dedicated message. A dropped connection
6
+ * gets its own text — the browser's wording for it ("NetworkError when
7
+ * attempting to fetch resource.") means nothing to the user — anything else
8
+ * shows the server's message, then the generic error.
9
+ *
10
+ * @param error - What the auth client threw or returned
11
+ * @param tErrors - `useTranslations('Auth.Errors')` from the calling component
12
+ */
13
+ export function authFallbackMessage(error: AuthError, tErrors: (key: string) => string): string {
14
+ if (isNetworkError(error)) return tErrors("networkError");
15
+ return error.message || tErrors("defaultError");
16
+ }
@@ -0,0 +1,41 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { getReturnTo } from "./return-to";
3
+
4
+ function stubLocation(search: string) {
5
+ vi.stubGlobal("window", { location: { search } });
6
+ }
7
+
8
+ afterEach(() => {
9
+ vi.unstubAllGlobals();
10
+ });
11
+
12
+ describe("getReturnTo", () => {
13
+ it("returns undefined during SSR (no window)", () => {
14
+ expect(getReturnTo()).toBeUndefined();
15
+ });
16
+
17
+ it("returns a clean relative path", () => {
18
+ stubLocation("?returnTo=/dashboard");
19
+ expect(getReturnTo()).toBe("/dashboard");
20
+ });
21
+
22
+ it("ignores a missing returnTo param", () => {
23
+ stubLocation("?foo=bar");
24
+ expect(getReturnTo()).toBeUndefined();
25
+ });
26
+
27
+ it("rejects protocol-relative URLs", () => {
28
+ stubLocation("?returnTo=//evil.com");
29
+ expect(getReturnTo()).toBeUndefined();
30
+ });
31
+
32
+ it("rejects backslash-smuggled URLs", () => {
33
+ stubLocation("?returnTo=/\\evil.com");
34
+ expect(getReturnTo()).toBeUndefined();
35
+ });
36
+
37
+ it("rejects absolute URLs", () => {
38
+ stubLocation("?returnTo=https://evil.com");
39
+ expect(getReturnTo()).toBeUndefined();
40
+ });
41
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Reads and sanitizes the `returnTo` query param from the current URL.
3
+ * Client-only: returns undefined during SSR/prerender so callers fall back to their default.
4
+ */
5
+ export function getReturnTo(): string | undefined {
6
+ if (typeof window === "undefined") return undefined;
7
+
8
+ return sanitizeReturnTo(new URLSearchParams(window.location.search).get("returnTo") ?? undefined);
9
+ }
10
+
11
+ function sanitizeReturnTo(value: string | string[] | undefined): string | undefined {
12
+ const path = Array.isArray(value) ? value[0] : value;
13
+
14
+ if (path && path.startsWith("/") && !path.startsWith("//") && !path.startsWith("/\\")) {
15
+ return path;
16
+ }
17
+
18
+ return undefined;
19
+ }
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createSignInSchema, createSignUpSchema } from "./validator";
3
+
4
+ const t = (key: string) => key;
5
+
6
+ describe("createSignUpSchema", () => {
7
+ const valid = {
8
+ confirmPassword: "long-enough",
9
+ email: "user@example.com",
10
+ name: "Ada",
11
+ password: "long-enough",
12
+ };
13
+
14
+ it("accepts a complete sign-up", () => {
15
+ expect(createSignUpSchema(t).safeParse(valid).success).toBe(true);
16
+ });
17
+
18
+ it("rejects mismatched passwords on the confirmation field", () => {
19
+ const result = createSignUpSchema(t).safeParse({ ...valid, confirmPassword: "different" });
20
+
21
+ expect(result.success).toBe(false);
22
+ expect(result.error?.issues[0]).toMatchObject({
23
+ message: "Auth.Errors.passwordsMustMatch",
24
+ path: ["confirmPassword"],
25
+ });
26
+ });
27
+
28
+ it("rejects short passwords", () => {
29
+ const result = createSignUpSchema(t).safeParse({
30
+ ...valid,
31
+ confirmPassword: "short",
32
+ password: "short",
33
+ });
34
+
35
+ expect(result.error?.issues.map((issue) => issue.message)).toContain(
36
+ "Auth.Errors.passwordTooShort",
37
+ );
38
+ });
39
+ });
40
+
41
+ describe("createSignInSchema", () => {
42
+ it("requires a valid email", () => {
43
+ expect(createSignInSchema(t).safeParse({ email: "nope", password: "x" }).success).toBe(false);
44
+ });
45
+ });