create-stackr 0.2.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 (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +642 -0
  3. package/bin/cli.js +12 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +113 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/config/dependencies.d.ts +82 -0
  9. package/dist/config/dependencies.d.ts.map +1 -0
  10. package/dist/config/dependencies.js +82 -0
  11. package/dist/config/dependencies.js.map +1 -0
  12. package/dist/config/presets.d.ts +3 -0
  13. package/dist/config/presets.d.ts.map +1 -0
  14. package/dist/config/presets.js +174 -0
  15. package/dist/config/presets.js.map +1 -0
  16. package/dist/generators/index.d.ts +40 -0
  17. package/dist/generators/index.d.ts.map +1 -0
  18. package/dist/generators/index.js +130 -0
  19. package/dist/generators/index.js.map +1 -0
  20. package/dist/generators/onboarding.d.ts +8 -0
  21. package/dist/generators/onboarding.d.ts.map +1 -0
  22. package/dist/generators/onboarding.js +141 -0
  23. package/dist/generators/onboarding.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +65 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/prompts/features.d.ts +14 -0
  29. package/dist/prompts/features.d.ts.map +1 -0
  30. package/dist/prompts/features.js +96 -0
  31. package/dist/prompts/features.js.map +1 -0
  32. package/dist/prompts/index.d.ts +3 -0
  33. package/dist/prompts/index.d.ts.map +1 -0
  34. package/dist/prompts/index.js +93 -0
  35. package/dist/prompts/index.js.map +1 -0
  36. package/dist/prompts/onboarding.d.ts +6 -0
  37. package/dist/prompts/onboarding.d.ts.map +1 -0
  38. package/dist/prompts/onboarding.js +37 -0
  39. package/dist/prompts/onboarding.js.map +1 -0
  40. package/dist/prompts/orm.d.ts +3 -0
  41. package/dist/prompts/orm.d.ts.map +1 -0
  42. package/dist/prompts/orm.js +23 -0
  43. package/dist/prompts/orm.js.map +1 -0
  44. package/dist/prompts/packageManager.d.ts +2 -0
  45. package/dist/prompts/packageManager.d.ts.map +1 -0
  46. package/dist/prompts/packageManager.js +18 -0
  47. package/dist/prompts/packageManager.js.map +1 -0
  48. package/dist/prompts/platform.d.ts +3 -0
  49. package/dist/prompts/platform.d.ts.map +1 -0
  50. package/dist/prompts/platform.js +21 -0
  51. package/dist/prompts/platform.js.map +1 -0
  52. package/dist/prompts/preset.d.ts +4 -0
  53. package/dist/prompts/preset.d.ts.map +1 -0
  54. package/dist/prompts/preset.js +165 -0
  55. package/dist/prompts/preset.js.map +1 -0
  56. package/dist/prompts/project.d.ts +2 -0
  57. package/dist/prompts/project.d.ts.map +1 -0
  58. package/dist/prompts/project.js +27 -0
  59. package/dist/prompts/project.js.map +1 -0
  60. package/dist/prompts/sdks.d.ts +2 -0
  61. package/dist/prompts/sdks.d.ts.map +1 -0
  62. package/dist/prompts/sdks.js +46 -0
  63. package/dist/prompts/sdks.js.map +1 -0
  64. package/dist/types/index.d.ts +77 -0
  65. package/dist/types/index.d.ts.map +1 -0
  66. package/dist/types/index.js +25 -0
  67. package/dist/types/index.js.map +1 -0
  68. package/dist/utils/cleanup.d.ts +5 -0
  69. package/dist/utils/cleanup.d.ts.map +1 -0
  70. package/dist/utils/cleanup.js +38 -0
  71. package/dist/utils/cleanup.js.map +1 -0
  72. package/dist/utils/copy.d.ts +10 -0
  73. package/dist/utils/copy.d.ts.map +1 -0
  74. package/dist/utils/copy.js +53 -0
  75. package/dist/utils/copy.js.map +1 -0
  76. package/dist/utils/errors.d.ts +33 -0
  77. package/dist/utils/errors.d.ts.map +1 -0
  78. package/dist/utils/errors.js +136 -0
  79. package/dist/utils/errors.js.map +1 -0
  80. package/dist/utils/git.d.ts +5 -0
  81. package/dist/utils/git.d.ts.map +1 -0
  82. package/dist/utils/git.js +33 -0
  83. package/dist/utils/git.js.map +1 -0
  84. package/dist/utils/logger.d.ts +9 -0
  85. package/dist/utils/logger.d.ts.map +1 -0
  86. package/dist/utils/logger.js +22 -0
  87. package/dist/utils/logger.js.map +1 -0
  88. package/dist/utils/package.d.ts +16 -0
  89. package/dist/utils/package.d.ts.map +1 -0
  90. package/dist/utils/package.js +86 -0
  91. package/dist/utils/package.js.map +1 -0
  92. package/dist/utils/system-validation.d.ts +9 -0
  93. package/dist/utils/system-validation.d.ts.map +1 -0
  94. package/dist/utils/system-validation.js +31 -0
  95. package/dist/utils/system-validation.js.map +1 -0
  96. package/dist/utils/template.d.ts +20 -0
  97. package/dist/utils/template.d.ts.map +1 -0
  98. package/dist/utils/template.js +234 -0
  99. package/dist/utils/template.js.map +1 -0
  100. package/dist/utils/validation.d.ts +8 -0
  101. package/dist/utils/validation.d.ts.map +1 -0
  102. package/dist/utils/validation.js +94 -0
  103. package/dist/utils/validation.js.map +1 -0
  104. package/package.json +96 -0
  105. package/templates/base/backend/.dockerignore.ejs +62 -0
  106. package/templates/base/backend/.env.example.ejs +116 -0
  107. package/templates/base/backend/Dockerfile.ejs +142 -0
  108. package/templates/base/backend/controllers/event-queue/index.ts +20 -0
  109. package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
  110. package/templates/base/backend/controllers/rest-api/index.ts +48 -0
  111. package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
  112. package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
  113. package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
  114. package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
  115. package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
  116. package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
  117. package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
  118. package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
  119. package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
  120. package/templates/base/backend/domain/device-session/schema.ts +72 -0
  121. package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
  122. package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
  123. package/templates/base/backend/domain/session/schema.ts +29 -0
  124. package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
  125. package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
  126. package/templates/base/backend/domain/user/schema.ts +14 -0
  127. package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
  128. package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
  129. package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
  130. package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
  131. package/templates/base/backend/lib/constants.ts.ejs +29 -0
  132. package/templates/base/backend/package.json.ejs +50 -0
  133. package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
  134. package/templates/base/backend/prisma.config.prisma.ts +12 -0
  135. package/templates/base/backend/tsconfig.json +39 -0
  136. package/templates/base/backend/utils/db.drizzle.ts +41 -0
  137. package/templates/base/backend/utils/db.prisma.ts +51 -0
  138. package/templates/base/backend/utils/email.ts.ejs +35 -0
  139. package/templates/base/backend/utils/errors.ts +348 -0
  140. package/templates/base/backend/utils/redis.ts.ejs +279 -0
  141. package/templates/base/mobile/.env.example.ejs +35 -0
  142. package/templates/base/mobile/.gitignore.ejs +167 -0
  143. package/templates/base/mobile/app/+not-found.tsx +85 -0
  144. package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
  145. package/templates/base/mobile/app.json.ejs +88 -0
  146. package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
  147. package/templates/base/mobile/assets/images/favicon.png +0 -0
  148. package/templates/base/mobile/assets/images/icon.png +0 -0
  149. package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
  150. package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
  151. package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
  152. package/templates/base/mobile/assets/images/paywall_image.png +0 -0
  153. package/templates/base/mobile/assets/images/splash.png +0 -0
  154. package/templates/base/mobile/eas.json.ejs +49 -0
  155. package/templates/base/mobile/metro.config.js +9 -0
  156. package/templates/base/mobile/package.json.ejs +53 -0
  157. package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
  158. package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
  159. package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
  160. package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
  161. package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
  162. package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
  163. package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
  164. package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
  165. package/templates/base/mobile/src/components/ui/index.ts +6 -0
  166. package/templates/base/mobile/src/constants/Theme.ts +163 -0
  167. package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
  168. package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
  169. package/templates/base/mobile/src/services/api.ts.ejs +71 -0
  170. package/templates/base/mobile/src/services/errorService.ts +179 -0
  171. package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
  172. package/templates/base/mobile/src/store/index.ts.ejs +18 -0
  173. package/templates/base/mobile/src/store/ui.store.ts +100 -0
  174. package/templates/base/mobile/src/utils/formatters.ts +105 -0
  175. package/templates/base/mobile/src/utils/logger.ts +73 -0
  176. package/templates/base/mobile/src/utils/responsive.ts +234 -0
  177. package/templates/base/mobile/tsconfig.json +32 -0
  178. package/templates/base/web/.env.example.ejs +26 -0
  179. package/templates/base/web/components.json +22 -0
  180. package/templates/base/web/eslint.config.mjs +18 -0
  181. package/templates/base/web/next.config.ts +7 -0
  182. package/templates/base/web/package.json.ejs +35 -0
  183. package/templates/base/web/postcss.config.mjs +7 -0
  184. package/templates/base/web/public/.gitkeep +0 -0
  185. package/templates/base/web/public/file.svg +1 -0
  186. package/templates/base/web/public/globe.svg +1 -0
  187. package/templates/base/web/public/next.svg +1 -0
  188. package/templates/base/web/public/vercel.svg +1 -0
  189. package/templates/base/web/public/window.svg +1 -0
  190. package/templates/base/web/src/app/favicon.ico +0 -0
  191. package/templates/base/web/src/app/globals.css +152 -0
  192. package/templates/base/web/src/app/layout.tsx.ejs +54 -0
  193. package/templates/base/web/src/app/page.tsx.ejs +92 -0
  194. package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
  195. package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
  196. package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
  197. package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
  198. package/templates/base/web/src/components/theme-toggle.tsx +34 -0
  199. package/templates/base/web/src/components/ui/button.tsx +62 -0
  200. package/templates/base/web/src/components/ui/card.tsx +92 -0
  201. package/templates/base/web/src/components/ui/input.tsx +21 -0
  202. package/templates/base/web/src/components/ui/label.tsx +24 -0
  203. package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
  204. package/templates/base/web/src/components/ui/spinner.tsx +20 -0
  205. package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
  206. package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
  207. package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
  208. package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
  209. package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
  210. package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
  211. package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
  212. package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
  213. package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
  214. package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
  215. package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
  216. package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
  217. package/templates/base/web/src/lib/utils.ts +6 -0
  218. package/templates/base/web/src/proxy.ts.ejs +66 -0
  219. package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
  220. package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
  221. package/templates/base/web/tsconfig.json +34 -0
  222. package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
  223. package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
  224. package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
  225. package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
  226. package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
  227. package/templates/features/mobile/auth/components/auth/index.ts +2 -0
  228. package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
  229. package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
  230. package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
  231. package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
  232. package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
  233. package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
  234. package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
  235. package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
  236. package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
  237. package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
  238. package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
  239. package/templates/features/web/.gitkeep +0 -0
  240. package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
  241. package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
  242. package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
  243. package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
  244. package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
  245. package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
  246. package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
  247. package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
  248. package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
  249. package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
  250. package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
  251. package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
  252. package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
  253. package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
  254. package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
  255. package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
  256. package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
  257. package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
  258. package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
  259. package/templates/integrations/mobile/att/services/attService.ts +84 -0
  260. package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
  261. package/templates/integrations/mobile/att/store/att.store.ts +162 -0
  262. package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
  263. package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
  264. package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
  265. package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
  266. package/templates/integrations/web/.gitkeep +0 -0
  267. package/templates/shared/.env.example.ejs +21 -0
  268. package/templates/shared/.gitignore.ejs +145 -0
  269. package/templates/shared/README.md.ejs +134 -0
  270. package/templates/shared/docker-compose.prod.yml.ejs +120 -0
  271. package/templates/shared/docker-compose.yml.ejs +129 -0
  272. package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
  273. package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
  274. package/templates/shared/scripts/setup.sh.ejs +979 -0
@@ -0,0 +1,92 @@
1
+ import Link from "next/link";
2
+ import { Button } from "@/components/ui/button";
3
+ import { ThemeToggle } from "@/components/theme-toggle";
4
+
5
+ export default function Home() {
6
+ return (
7
+ <div className="min-h-screen flex flex-col bg-background">
8
+ {/* Navbar */}
9
+ <header className="fixed top-0 w-full border-b border-border/40 bg-background/80 backdrop-blur-xl z-50">
10
+ <div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
11
+ <Link href="/" className="text-lg font-bold tracking-tight flex items-center gap-2">
12
+ <div className="h-6 w-6 rounded-md bg-primary flex items-center justify-center">
13
+ <span className="text-primary-foreground text-xs font-bold">A</span>
14
+ </div>
15
+ <%= projectName %>
16
+ </Link>
17
+ <div className="flex items-center gap-4">
18
+ <ThemeToggle />
19
+ <Link href="/login" className="hidden sm:block">
20
+ <Button variant="ghost" className="text-muted-foreground hover:text-foreground">Sign in</Button>
21
+ </Link>
22
+ <Link href="/register">
23
+ <Button className="font-medium shadow-lg shadow-primary/20">Get started</Button>
24
+ </Link>
25
+ </div>
26
+ </div>
27
+ </header>
28
+
29
+ {/* Hero Section */}
30
+ <main className="flex-1 flex flex-col">
31
+ <section className="relative pt-32 pb-24 lg:pt-48 lg:pb-32 overflow-hidden">
32
+ <div className="max-w-7xl mx-auto px-6 text-center space-y-10">
33
+ {/* Announcement Pill */}
34
+ <div className="fade-in-up delay-100 flex justify-center">
35
+ <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-border/50 bg-background/50 backdrop-blur-sm text-sm text-muted-foreground hover:bg-muted/50 transition-colors cursor-pointer">
36
+ <span className="relative flex h-2 w-2">
37
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
38
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
39
+ </span>
40
+ <span className="font-medium">v1.0 is now live</span>
41
+ <span className="text-muted-foreground/50">|</span>
42
+ <span className="hidden sm:inline">Check out what's new &rarr;</span>
43
+ </div>
44
+ </div>
45
+
46
+ {/* Headline */}
47
+ <div className="space-y-6 max-w-4xl mx-auto">
48
+ <h1 className="text-5xl sm:text-6xl lg:text-7xl font-bold tracking-tight text-foreground leading-[1.1] sm:leading-[1.1]">
49
+ Build something <br className="hidden sm:block" />
50
+ <span className="text-foreground">
51
+ remarkable today.
52
+ </span>
53
+ </h1>
54
+
55
+ <p className="text-lg sm:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
56
+ Ship your next big idea with authentication, premium UI components, and solid engineering practices built-in.
57
+ </p>
58
+ </div>
59
+
60
+ {/* CTAs */}
61
+ <div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4">
62
+ <Link href="/register">
63
+ <Button size="lg" className="h-12 px-8 text-base shadow-xl shadow-primary/20 hover:shadow-primary/30 transition-all duration-300">
64
+ Start building free
65
+ </Button>
66
+ </Link>
67
+ <Link href="/login">
68
+ <Button variant="outline" size="lg" className="h-12 px-8 text-base backdrop-blur-sm hover:bg-muted/50">
69
+ View demo
70
+ </Button>
71
+ </Link>
72
+ </div>
73
+ </div>
74
+ </section>
75
+ </main>
76
+
77
+ {/* Slimmer Footer */}
78
+ <footer className="border-t border-border/40 py-6 bg-muted/20">
79
+ <div className="max-w-7xl mx-auto px-6 flex flex-col sm:flex-row items-center justify-between gap-4">
80
+ <p className="text-sm text-muted-foreground">
81
+ &copy; {new Date().getFullYear()} <%= projectName %>.
82
+ </p>
83
+ <div className="flex gap-6 text-sm text-muted-foreground">
84
+ <Link href="#" className="hover:text-foreground transition-colors">Privacy</Link>
85
+ <Link href="#" className="hover:text-foreground transition-colors">Terms</Link>
86
+ <Link href="#" className="hover:text-foreground transition-colors">GitHub</Link>
87
+ </div>
88
+ </div>
89
+ </footer>
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { useAuthStore } from "@/store/auth.store";
5
+ import type { AuthSession } from "@/lib/auth/actions";
6
+
7
+ /**
8
+ * Hydrates the auth store with the initial session from RSC.
9
+ * Renders nothing - just a side effect component.
10
+ */
11
+ export function AuthHydrator({ session }: { session: AuthSession | null }) {
12
+ const hydrate = useAuthStore((s) => s.hydrate);
13
+
14
+ useEffect(() => {
15
+ hydrate(session);
16
+ }, [hydrate, session]);
17
+
18
+ return null;
19
+ }
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import { useEffect, useCallback } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { useSession } from "@/hooks/use-session";
6
+ import { getSession } from "@/lib/auth/actions";
7
+ import { useAuthActions } from "@/store/auth.store";
8
+
9
+ interface ProtectedRouteProps {
10
+ children: React.ReactNode;
11
+ /**
12
+ * URL to redirect to if not authenticated
13
+ * @default "/login"
14
+ */
15
+ redirectTo?: string;
16
+ /**
17
+ * Custom loading component
18
+ */
19
+ loadingComponent?: React.ReactNode;
20
+ }
21
+
22
+ /**
23
+ * Client-side route protection component
24
+ *
25
+ * Wraps content that should only be visible to authenticated users.
26
+ * Redirects to login if user is not authenticated.
27
+ *
28
+ * Note: For server-side protection, use the proxy or check session
29
+ * in server components. This component provides client-side protection
30
+ * as an additional layer.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * <ProtectedRoute>
35
+ * <DashboardContent />
36
+ * </ProtectedRoute>
37
+ * ```
38
+ */
39
+ export function ProtectedRoute({
40
+ children,
41
+ redirectTo = "/login",
42
+ loadingComponent,
43
+ }: ProtectedRouteProps) {
44
+ const { isAuthenticated, isLoading } = useSession();
45
+ const { handleAuthError } = useAuthActions();
46
+ const router = useRouter();
47
+
48
+ // Revalidate session when window regains focus
49
+ // This detects session expiration when user returns to the tab
50
+ const revalidateSession = useCallback(async () => {
51
+ if (!isAuthenticated) return;
52
+
53
+ const session = await getSession();
54
+ if (!session) {
55
+ // Session expired on backend - update UI state
56
+ handleAuthError();
57
+ }
58
+ }, [isAuthenticated, handleAuthError]);
59
+
60
+ // Focus-based revalidation
61
+ useEffect(() => {
62
+ window.addEventListener("focus", revalidateSession);
63
+ return () => window.removeEventListener("focus", revalidateSession);
64
+ }, [revalidateSession]);
65
+
66
+ // Redirect when not authenticated
67
+ useEffect(() => {
68
+ if (!isLoading && !isAuthenticated) {
69
+ // Include current path as redirect parameter
70
+ const currentPath = window.location.pathname;
71
+ const loginUrl = `${redirectTo}?redirect=${encodeURIComponent(currentPath)}`;
72
+ router.push(loginUrl);
73
+ }
74
+ }, [isAuthenticated, isLoading, redirectTo, router]);
75
+
76
+ // Show loading state
77
+ if (isLoading) {
78
+ return (
79
+ loadingComponent ?? (
80
+ <div className="flex min-h-screen items-center justify-center">
81
+ <div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
82
+ </div>
83
+ )
84
+ );
85
+ }
86
+
87
+ // Don't render content if not authenticated
88
+ if (!isAuthenticated) {
89
+ return null;
90
+ }
91
+
92
+ return <>{children}</>;
93
+ }
94
+
95
+ /**
96
+ * HOC version of ProtectedRoute for class components or convenience
97
+ */
98
+ export function withProtectedRoute<P extends object>(
99
+ Component: React.ComponentType<P>,
100
+ options?: Omit<ProtectedRouteProps, "children">
101
+ ) {
102
+ return function ProtectedComponent(props: P) {
103
+ return (
104
+ <ProtectedRoute {...options}>
105
+ <Component {...props} />
106
+ </ProtectedRoute>
107
+ );
108
+ };
109
+ }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import {
5
+ useDeviceSessionStore,
6
+ useDeviceSessionActions,
7
+ } from "@/store/deviceSession.store";
8
+ import { AUTH_CONFIG } from "@/lib/auth/config";
9
+
10
+ /**
11
+ * DeviceSessionSetup handles:
12
+ * 1. Session initialization on mount
13
+ * 2. Periodic heartbeat (every 5 minutes)
14
+ * 3. Visibility-based activity updates
15
+ */
16
+ export function DeviceSessionSetup() {
17
+ const deviceSession = useDeviceSessionStore((s) => s.deviceSession);
18
+ const isInitialized = useDeviceSessionStore((s) => s.isInitialized);
19
+ const { initializeSession, sendHeartbeat } = useDeviceSessionActions();
20
+
21
+ // Initialize on mount
22
+ useEffect(() => {
23
+ if (!isInitialized) {
24
+ initializeSession();
25
+ }
26
+ }, [isInitialized, initializeSession]);
27
+
28
+ // Heartbeat - update activity every 5 minutes
29
+ useEffect(() => {
30
+ if (!deviceSession) return;
31
+
32
+ const interval = setInterval(() => {
33
+ sendHeartbeat();
34
+ }, AUTH_CONFIG.deviceHeartbeatInterval);
35
+
36
+ return () => clearInterval(interval);
37
+ }, [deviceSession, sendHeartbeat]);
38
+
39
+ // Update activity on visibility change (tab becomes visible)
40
+ useEffect(() => {
41
+ if (!deviceSession) return;
42
+
43
+ const handleVisibilityChange = () => {
44
+ if (document.visibilityState === "visible") {
45
+ sendHeartbeat();
46
+ }
47
+ };
48
+
49
+ document.addEventListener("visibilitychange", handleVisibilityChange);
50
+ return () => {
51
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
52
+ };
53
+ }, [deviceSession, sendHeartbeat]);
54
+
55
+ return null;
56
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ThemeProvider as NextThemesProvider } from "next-themes";
5
+
6
+ export function ThemeProvider({ children }: { children: React.ReactNode }) {
7
+ return (
8
+ <NextThemesProvider
9
+ attribute="class"
10
+ defaultTheme="system"
11
+ enableSystem
12
+ disableTransitionOnChange
13
+ >
14
+ {children}
15
+ </NextThemesProvider>
16
+ );
17
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Moon, Sun } from "lucide-react";
5
+ import { useTheme } from "next-themes";
6
+ import { Button } from "@/components/ui/button";
7
+
8
+ export function ThemeToggle() {
9
+ const { setTheme, resolvedTheme } = useTheme();
10
+ const [mounted, setMounted] = React.useState(false);
11
+
12
+ // Avoid hydration mismatch by only rendering after mount
13
+ React.useEffect(() => setMounted(true), []);
14
+
15
+ if (!mounted) {
16
+ return (
17
+ <Button variant="ghost" size="icon" aria-label="Toggle theme">
18
+ <span className="h-5 w-5" />
19
+ </Button>
20
+ );
21
+ }
22
+
23
+ return (
24
+ <Button
25
+ variant="ghost"
26
+ size="icon"
27
+ onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
28
+ aria-label="Toggle theme"
29
+ >
30
+ <Sun className="h-5 w-5 rotate-0 scale-100 transition-transform dark:-rotate-90 dark:scale-0" />
31
+ <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-transform dark:rotate-0 dark:scale-100" />
32
+ </Button>
33
+ );
34
+ }
@@ -0,0 +1,62 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ "icon-sm": "size-8",
29
+ "icon-lg": "size-10",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ size: "default",
35
+ },
36
+ }
37
+ )
38
+
39
+ function Button({
40
+ className,
41
+ variant = "default",
42
+ size = "default",
43
+ asChild = false,
44
+ ...props
45
+ }: React.ComponentProps<"button"> &
46
+ VariantProps<typeof buttonVariants> & {
47
+ asChild?: boolean
48
+ }) {
49
+ const Comp = asChild ? Slot : "button"
50
+
51
+ return (
52
+ <Comp
53
+ data-slot="button"
54
+ data-variant={variant}
55
+ data-size={size}
56
+ className={cn(buttonVariants({ variant, size, className }))}
57
+ {...props}
58
+ />
59
+ )
60
+ }
61
+
62
+ export { Button, buttonVariants }
@@ -0,0 +1,92 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }
@@ -0,0 +1,21 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ export { Input }
@@ -0,0 +1,24 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as LabelPrimitive from "@radix-ui/react-label"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Label({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
12
+ return (
13
+ <LabelPrimitive.Root
14
+ data-slot="label"
15
+ className={cn(
16
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ export { Label }
@@ -0,0 +1,13 @@
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4
+ return (
5
+ <div
6
+ data-slot="skeleton"
7
+ className={cn("bg-accent animate-pulse rounded-md", className)}
8
+ {...props}
9
+ />
10
+ )
11
+ }
12
+
13
+ export { Skeleton }
@@ -0,0 +1,20 @@
1
+ import { cn } from "@/lib/utils";
2
+
3
+ interface SpinnerProps {
4
+ size?: "sm" | "md" | "lg";
5
+ className?: string;
6
+ }
7
+
8
+ export function Spinner({ size = "md", className }: SpinnerProps) {
9
+ const sizes = { sm: "h-4 w-4 border-2", md: "h-6 w-6 border-2", lg: "h-8 w-8 border-[3px]" };
10
+
11
+ return (
12
+ <div
13
+ className={cn("animate-spin rounded-full border-muted-foreground border-t-primary", sizes[size], className)}
14
+ role="status"
15
+ aria-label="Loading"
16
+ >
17
+ <span className="sr-only">Loading...</span>
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import {
4
+ useDeviceSession as useDeviceSessionStore,
5
+ useDeviceSessionActions,
6
+ } from "@/store/deviceSession.store";
7
+
8
+ /**
9
+ * Hook to access the current device session
10
+ *
11
+ * Provides a simplified interface to the device session store.
12
+ * Use this hook in components that need access to the device session.
13
+ */
14
+ export function useDeviceSession() {
15
+ const { deviceSession, deviceId, isLoading, hasSession, error } = useDeviceSessionStore();
16
+ const { refreshSession, sendHeartbeat, clearError } = useDeviceSessionActions();
17
+
18
+ return {
19
+ // Session data
20
+ deviceSession,
21
+ deviceId,
22
+
23
+ // Loading state
24
+ isLoading,
25
+
26
+ // Convenience boolean for session checks
27
+ hasSession,
28
+
29
+ // Error state
30
+ error,
31
+
32
+ // Actions
33
+ refreshSession,
34
+ sendHeartbeat,
35
+ clearError,
36
+ };
37
+ }
38
+
39
+ // Re-export store hooks for direct access when needed
40
+ export { useDeviceSessionStore, useDeviceSessionActions } from "@/store/deviceSession.store";
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { useAuth, useAuthActions } from "@/store/auth.store";
4
+
5
+ /**
6
+ * Hook to access the current session
7
+ *
8
+ * Provides a simplified interface to the auth store.
9
+ * Use this hook in components that need access to the current user/session.
10
+ */
11
+ export function useSession() {
12
+ const { session, user, isLoading, isAuthenticated, error } = useAuth();
13
+ const { signOut, clearError } = useAuthActions();
14
+
15
+ return {
16
+ // Session data
17
+ session,
18
+ user,
19
+
20
+ // Loading state
21
+ isLoading,
22
+
23
+ // Convenience boolean for auth checks
24
+ isAuthenticated,
25
+
26
+ // Error state
27
+ error,
28
+
29
+ // Actions
30
+ signOut,
31
+ clearError,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Hook to require authentication
37
+ *
38
+ * Returns the session and throws if not authenticated.
39
+ * Use this in components that should only render when authenticated.
40
+ */
41
+ export function useRequireSession() {
42
+ const { session, user, isLoading, isAuthenticated } = useAuth();
43
+
44
+ if (!isLoading && !isAuthenticated) {
45
+ throw new Error("Authentication required");
46
+ }
47
+
48
+ return {
49
+ session: session!,
50
+ user: user!,
51
+ isLoading,
52
+ };
53
+ }
54
+
55
+ // Re-export store hooks for direct access when needed
56
+ export { useAuth, useAuthActions } from "@/store/auth.store";