vibecarbon 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 (495) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +188 -0
  3. package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
  4. package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
  5. package/carbon/.claude/settings.json +29 -0
  6. package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
  7. package/carbon/.dockerignore +57 -0
  8. package/carbon/.env.example +219 -0
  9. package/carbon/.github/copilot-instructions.md +3 -0
  10. package/carbon/.github/workflows/deploy.yml +346 -0
  11. package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
  12. package/carbon/.windsurfrules +74 -0
  13. package/carbon/AGENTS.md +422 -0
  14. package/carbon/CLAUDE.md +3 -0
  15. package/carbon/DEVELOPMENT.md +187 -0
  16. package/carbon/Dockerfile +98 -0
  17. package/carbon/LICENSE +21 -0
  18. package/carbon/PRODUCTION.md +364 -0
  19. package/carbon/README.md +193 -0
  20. package/carbon/TESTING.md +138 -0
  21. package/carbon/backup/Dockerfile +75 -0
  22. package/carbon/backup/backup.sh +95 -0
  23. package/carbon/backup/compose-backup.sh +140 -0
  24. package/carbon/biome.json +83 -0
  25. package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
  26. package/carbon/cloud-init/k3s/master-init.sh +215 -0
  27. package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
  28. package/carbon/cloud-init/k3s/worker-init.sh +147 -0
  29. package/carbon/components.json +24 -0
  30. package/carbon/content/blog/authentication-guide.mdx +34 -0
  31. package/carbon/content/blog/getting-started.mdx +41 -0
  32. package/carbon/content/changelog/v0-1-0.mdx +40 -0
  33. package/carbon/content/docs/analytics.mdx +90 -0
  34. package/carbon/content/docs/authentication.mdx +231 -0
  35. package/carbon/content/docs/background-jobs.mdx +116 -0
  36. package/carbon/content/docs/cli.mdx +630 -0
  37. package/carbon/content/docs/database.mdx +236 -0
  38. package/carbon/content/docs/deployment.mdx +227 -0
  39. package/carbon/content/docs/development.mdx +238 -0
  40. package/carbon/content/docs/environments.mdx +84 -0
  41. package/carbon/content/docs/getting-started.mdx +112 -0
  42. package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
  43. package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
  44. package/carbon/content/docs/optional-services.mdx +160 -0
  45. package/carbon/db/Dockerfile +23 -0
  46. package/carbon/docker-compose.dev-init.yml +5 -0
  47. package/carbon/docker-compose.metabase.override.yml +14 -0
  48. package/carbon/docker-compose.metabase.prod.yml +28 -0
  49. package/carbon/docker-compose.metabase.yml +84 -0
  50. package/carbon/docker-compose.n8n.override.yml +14 -0
  51. package/carbon/docker-compose.n8n.prod.yml +31 -0
  52. package/carbon/docker-compose.n8n.yml +97 -0
  53. package/carbon/docker-compose.observability.override.yml +18 -0
  54. package/carbon/docker-compose.observability.prod.yml +28 -0
  55. package/carbon/docker-compose.observability.yml +125 -0
  56. package/carbon/docker-compose.override.yml +28 -0
  57. package/carbon/docker-compose.prod.yml +294 -0
  58. package/carbon/docker-compose.yml +508 -0
  59. package/carbon/docker-entrypoint.sh +12 -0
  60. package/carbon/ha/activate-standby.sh +52 -0
  61. package/carbon/ha/primary-init.sql +36 -0
  62. package/carbon/ha/standby-init.sh +74 -0
  63. package/carbon/k8s/LICENSE +99 -0
  64. package/carbon/k8s/README.md +272 -0
  65. package/carbon/k8s/base/app/deployment.yaml +130 -0
  66. package/carbon/k8s/base/app/hpa.yaml +44 -0
  67. package/carbon/k8s/base/app/kustomization.yaml +10 -0
  68. package/carbon/k8s/base/app/network-policy.yaml +124 -0
  69. package/carbon/k8s/base/app/pdb.yaml +14 -0
  70. package/carbon/k8s/base/app/rbac.yaml +36 -0
  71. package/carbon/k8s/base/app/service.yaml +16 -0
  72. package/carbon/k8s/base/backup/cronjob.yaml +120 -0
  73. package/carbon/k8s/base/backup/kustomization.yaml +7 -0
  74. package/carbon/k8s/base/backup/network-policy.yaml +31 -0
  75. package/carbon/k8s/base/backup/rbac.yaml +7 -0
  76. package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
  77. package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
  78. package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
  79. package/carbon/k8s/base/config/configmap.yaml +48 -0
  80. package/carbon/k8s/base/config/kustomization.yaml +8 -0
  81. package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
  82. package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
  83. package/carbon/k8s/base/kustomization.yaml +43 -0
  84. package/carbon/k8s/base/namespace.yaml +7 -0
  85. package/carbon/k8s/base/network-policies.yaml +95 -0
  86. package/carbon/k8s/base/registry/kustomization.yaml +5 -0
  87. package/carbon/k8s/base/registry/local-registry.yaml +193 -0
  88. package/carbon/k8s/base/traefik/certificate.yaml +21 -0
  89. package/carbon/k8s/base/traefik/configmap.yaml +13 -0
  90. package/carbon/k8s/base/traefik/deployment.yaml +165 -0
  91. package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
  92. package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
  93. package/carbon/k8s/base/traefik/middleware.yaml +109 -0
  94. package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
  95. package/carbon/k8s/base/traefik/service.yaml +38 -0
  96. package/carbon/k8s/flux/README.md +49 -0
  97. package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
  98. package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
  99. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
  100. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
  101. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
  102. package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
  103. package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
  104. package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
  105. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
  106. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
  107. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
  108. package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
  109. package/carbon/k8s/infra/kustomization.yaml +21 -0
  110. package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
  111. package/carbon/k8s/overlays/local/configmap.yaml +23 -0
  112. package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
  113. package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
  114. package/carbon/k8s/overlays/local/namespace.yaml +8 -0
  115. package/carbon/k8s/overlays/local/secrets.yaml +32 -0
  116. package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
  117. package/carbon/k8s/test-local.sh +318 -0
  118. package/carbon/k8s/values/supabase.values.yaml +133 -0
  119. package/carbon/package.json +154 -0
  120. package/carbon/runtime.Dockerfile +17 -0
  121. package/carbon/scripts/_dev-jwt.mjs +45 -0
  122. package/carbon/scripts/check-rls.ts +53 -0
  123. package/carbon/scripts/dev-init.js +191 -0
  124. package/carbon/scripts/dev.js +191 -0
  125. package/carbon/scripts/docker-down.js +63 -0
  126. package/carbon/scripts/docker-logs.js +59 -0
  127. package/carbon/scripts/docker-up.js +222 -0
  128. package/carbon/scripts/generate-dev-configs.sh +131 -0
  129. package/carbon/scripts/generate-rss.ts +116 -0
  130. package/carbon/scripts/generate-sitemap.ts +102 -0
  131. package/carbon/scripts/k8s-apply.js +71 -0
  132. package/carbon/scripts/k8s-delete.js +75 -0
  133. package/carbon/scripts/lib/manifest.js +176 -0
  134. package/carbon/scripts/secret-scan.mjs +278 -0
  135. package/carbon/scripts/validate-dev-configs.sh +101 -0
  136. package/carbon/src/client/App.tsx +202 -0
  137. package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
  138. package/carbon/src/client/assets/hyperformant-light.svg +29 -0
  139. package/carbon/src/client/assets/logos/aider.svg +1 -0
  140. package/carbon/src/client/assets/logos/antigravity.svg +1 -0
  141. package/carbon/src/client/assets/logos/bolt.svg +1 -0
  142. package/carbon/src/client/assets/logos/claude-code.svg +1 -0
  143. package/carbon/src/client/assets/logos/copilot.svg +1 -0
  144. package/carbon/src/client/assets/logos/cursor.svg +1 -0
  145. package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
  146. package/carbon/src/client/assets/logos/lovable.svg +1 -0
  147. package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
  148. package/carbon/src/client/assets/logos/v0.svg +1 -0
  149. package/carbon/src/client/assets/logos/windsurf.svg +1 -0
  150. package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
  151. package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
  152. package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
  153. package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
  154. package/carbon/src/client/components/AppSidebar.tsx +760 -0
  155. package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
  156. package/carbon/src/client/components/CTAFooter.tsx +59 -0
  157. package/carbon/src/client/components/ComparisonSection.tsx +132 -0
  158. package/carbon/src/client/components/ContentPanel.tsx +66 -0
  159. package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
  160. package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
  161. package/carbon/src/client/components/FAQSection.tsx +76 -0
  162. package/carbon/src/client/components/FileUpload.tsx +210 -0
  163. package/carbon/src/client/components/HeaderActions.tsx +17 -0
  164. package/carbon/src/client/components/Hero.tsx +608 -0
  165. package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
  166. package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
  167. package/carbon/src/client/components/Logo.tsx +87 -0
  168. package/carbon/src/client/components/LogoStrip.tsx +76 -0
  169. package/carbon/src/client/components/MetricsStrip.tsx +48 -0
  170. package/carbon/src/client/components/Nav.tsx +195 -0
  171. package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
  172. package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
  173. package/carbon/src/client/components/PageHeader.tsx +24 -0
  174. package/carbon/src/client/components/PlanGate.tsx +36 -0
  175. package/carbon/src/client/components/PricingSection.tsx +371 -0
  176. package/carbon/src/client/components/SEO.tsx +34 -0
  177. package/carbon/src/client/components/SmoothScroll.tsx +45 -0
  178. package/carbon/src/client/components/TechStackSection.tsx +165 -0
  179. package/carbon/src/client/components/WorkflowSection.tsx +604 -0
  180. package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
  181. package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
  182. package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
  183. package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
  184. package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
  185. package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
  186. package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
  187. package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
  188. package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
  189. package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
  190. package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
  191. package/carbon/src/client/components/effects/index.ts +2 -0
  192. package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
  193. package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
  194. package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
  195. package/carbon/src/client/components/scroll/index.ts +2 -0
  196. package/carbon/src/client/components/ui/accordion.tsx +71 -0
  197. package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
  198. package/carbon/src/client/components/ui/alert.tsx +73 -0
  199. package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
  200. package/carbon/src/client/components/ui/avatar.tsx +91 -0
  201. package/carbon/src/client/components/ui/badge.tsx +50 -0
  202. package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
  203. package/carbon/src/client/components/ui/button-group.tsx +78 -0
  204. package/carbon/src/client/components/ui/button.tsx +120 -0
  205. package/carbon/src/client/components/ui/calendar.tsx +182 -0
  206. package/carbon/src/client/components/ui/card.tsx +85 -0
  207. package/carbon/src/client/components/ui/carousel.tsx +227 -0
  208. package/carbon/src/client/components/ui/chart.tsx +357 -0
  209. package/carbon/src/client/components/ui/checkbox.tsx +27 -0
  210. package/carbon/src/client/components/ui/collapsible.tsx +15 -0
  211. package/carbon/src/client/components/ui/command.tsx +178 -0
  212. package/carbon/src/client/components/ui/context-menu.tsx +233 -0
  213. package/carbon/src/client/components/ui/dialog.tsx +132 -0
  214. package/carbon/src/client/components/ui/drawer.tsx +118 -0
  215. package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
  216. package/carbon/src/client/components/ui/empty.tsx +94 -0
  217. package/carbon/src/client/components/ui/field.tsx +226 -0
  218. package/carbon/src/client/components/ui/hover-card.tsx +44 -0
  219. package/carbon/src/client/components/ui/input-group.tsx +146 -0
  220. package/carbon/src/client/components/ui/input-otp.tsx +83 -0
  221. package/carbon/src/client/components/ui/input.tsx +20 -0
  222. package/carbon/src/client/components/ui/item.tsx +188 -0
  223. package/carbon/src/client/components/ui/kbd.tsx +26 -0
  224. package/carbon/src/client/components/ui/label.tsx +21 -0
  225. package/carbon/src/client/components/ui/menubar.tsx +250 -0
  226. package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
  227. package/carbon/src/client/components/ui/pagination.tsx +102 -0
  228. package/carbon/src/client/components/ui/popover.tsx +75 -0
  229. package/carbon/src/client/components/ui/progress.tsx +66 -0
  230. package/carbon/src/client/components/ui/radio-group.tsx +36 -0
  231. package/carbon/src/client/components/ui/resizable.tsx +46 -0
  232. package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
  233. package/carbon/src/client/components/ui/select.tsx +186 -0
  234. package/carbon/src/client/components/ui/separator.tsx +21 -0
  235. package/carbon/src/client/components/ui/sheet.tsx +124 -0
  236. package/carbon/src/client/components/ui/sidebar.tsx +702 -0
  237. package/carbon/src/client/components/ui/skeleton.tsx +13 -0
  238. package/carbon/src/client/components/ui/slider.tsx +57 -0
  239. package/carbon/src/client/components/ui/sonner.tsx +45 -0
  240. package/carbon/src/client/components/ui/spinner.tsx +15 -0
  241. package/carbon/src/client/components/ui/switch.tsx +30 -0
  242. package/carbon/src/client/components/ui/table.tsx +89 -0
  243. package/carbon/src/client/components/ui/tabs.tsx +73 -0
  244. package/carbon/src/client/components/ui/textarea.tsx +18 -0
  245. package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
  246. package/carbon/src/client/components/ui/toggle.tsx +44 -0
  247. package/carbon/src/client/components/ui/tooltip.tsx +56 -0
  248. package/carbon/src/client/hooks/api/index.ts +14 -0
  249. package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
  250. package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
  251. package/carbon/src/client/hooks/use-mobile.ts +21 -0
  252. package/carbon/src/client/hooks/useMousePosition.ts +91 -0
  253. package/carbon/src/client/hooks/useNotifications.tsx +124 -0
  254. package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
  255. package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
  256. package/carbon/src/client/hooks/usePackageManager.ts +69 -0
  257. package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
  258. package/carbon/src/client/hooks/useRunningServices.ts +114 -0
  259. package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
  260. package/carbon/src/client/index.css +467 -0
  261. package/carbon/src/client/index.html +56 -0
  262. package/carbon/src/client/lib/admin-services.ts +151 -0
  263. package/carbon/src/client/lib/api.ts +32 -0
  264. package/carbon/src/client/lib/blog.ts +35 -0
  265. package/carbon/src/client/lib/changelog.ts +33 -0
  266. package/carbon/src/client/lib/docs-search.ts +101 -0
  267. package/carbon/src/client/lib/docs.ts +37 -0
  268. package/carbon/src/client/lib/i18n.ts +32 -0
  269. package/carbon/src/client/lib/supabase.ts +72 -0
  270. package/carbon/src/client/lib/tailwind-colors.ts +357 -0
  271. package/carbon/src/client/lib/theme.ts +117 -0
  272. package/carbon/src/client/lib/utils.ts +22 -0
  273. package/carbon/src/client/locales/de.json +529 -0
  274. package/carbon/src/client/locales/en.json +461 -0
  275. package/carbon/src/client/locales/es.json +529 -0
  276. package/carbon/src/client/locales/fr.json +529 -0
  277. package/carbon/src/client/locales/pt.json +529 -0
  278. package/carbon/src/client/main.tsx +56 -0
  279. package/carbon/src/client/mdx.d.ts +13 -0
  280. package/carbon/src/client/pages/ApiDocs.tsx +76 -0
  281. package/carbon/src/client/pages/AuthCallback.tsx +34 -0
  282. package/carbon/src/client/pages/Blog.tsx +167 -0
  283. package/carbon/src/client/pages/Changelog.tsx +171 -0
  284. package/carbon/src/client/pages/Charts.tsx +388 -0
  285. package/carbon/src/client/pages/Checkout.tsx +227 -0
  286. package/carbon/src/client/pages/Contact.tsx +174 -0
  287. package/carbon/src/client/pages/Dashboard.tsx +368 -0
  288. package/carbon/src/client/pages/Docs.tsx +372 -0
  289. package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
  290. package/carbon/src/client/pages/Home.tsx +187 -0
  291. package/carbon/src/client/pages/Legal.tsx +100 -0
  292. package/carbon/src/client/pages/Login.tsx +408 -0
  293. package/carbon/src/client/pages/MFAVerify.tsx +156 -0
  294. package/carbon/src/client/pages/NotFound.tsx +21 -0
  295. package/carbon/src/client/pages/Onboarding.tsx +246 -0
  296. package/carbon/src/client/pages/ResetPassword.tsx +200 -0
  297. package/carbon/src/client/pages/UIComponents.tsx +390 -0
  298. package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
  299. package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
  300. package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
  301. package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
  302. package/carbon/src/client/pages/admin/Logs.tsx +18 -0
  303. package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
  304. package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
  305. package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
  306. package/carbon/src/client/pages/admin/Settings.tsx +314 -0
  307. package/carbon/src/client/pages/admin/Theme.tsx +465 -0
  308. package/carbon/src/client/pages/admin/Users.tsx +365 -0
  309. package/carbon/src/client/pages/api-docs.css +156 -0
  310. package/carbon/src/client/pages/organizations/Details.tsx +200 -0
  311. package/carbon/src/client/pages/organizations/Members.tsx +402 -0
  312. package/carbon/src/client/pages/settings/Billing.tsx +473 -0
  313. package/carbon/src/client/pages/settings/Profile.tsx +160 -0
  314. package/carbon/src/client/pages/settings/Security.tsx +341 -0
  315. package/carbon/src/client/public/favicon.svg +19 -0
  316. package/carbon/src/client/public/robots.txt +8 -0
  317. package/carbon/src/server/billing/index.ts +104 -0
  318. package/carbon/src/server/billing/provider.ts +81 -0
  319. package/carbon/src/server/billing/providers/paddle.ts +314 -0
  320. package/carbon/src/server/billing/providers/polar.ts +325 -0
  321. package/carbon/src/server/billing/providers/stripe.ts +233 -0
  322. package/carbon/src/server/emails/templates.ts +116 -0
  323. package/carbon/src/server/index.ts +554 -0
  324. package/carbon/src/server/lib/auth.ts +6 -0
  325. package/carbon/src/server/lib/email.ts +64 -0
  326. package/carbon/src/server/lib/env.ts +112 -0
  327. package/carbon/src/server/lib/errors.ts +21 -0
  328. package/carbon/src/server/lib/logger.ts +17 -0
  329. package/carbon/src/server/lib/rate-limiter.ts +288 -0
  330. package/carbon/src/server/lib/request.ts +34 -0
  331. package/carbon/src/server/lib/stripe.ts +42 -0
  332. package/carbon/src/server/lib/supabase.ts +65 -0
  333. package/carbon/src/server/middleware/requirePlan.ts +80 -0
  334. package/carbon/src/server/routes/_internal/services-status.ts +958 -0
  335. package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
  336. package/carbon/src/server/routes/health.ts +48 -0
  337. package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
  338. package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
  339. package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
  340. package/carbon/src/server/routes/v1/auth.ts +390 -0
  341. package/carbon/src/server/routes/v1/billing.ts +718 -0
  342. package/carbon/src/server/routes/v1/contact.ts +93 -0
  343. package/carbon/src/server/routes/v1/index.ts +1333 -0
  344. package/carbon/src/server/routes/v1/newsletter.ts +181 -0
  345. package/carbon/src/server/routes/v1/performance.ts +157 -0
  346. package/carbon/src/server/routes/v1/stats.ts +170 -0
  347. package/carbon/src/server/routes/v1/theme.ts +106 -0
  348. package/carbon/src/server/routes/webhooks/billing.ts +376 -0
  349. package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
  350. package/carbon/src/server/types.ts +11 -0
  351. package/carbon/src/shared/pricing.ts +155 -0
  352. package/carbon/src/shared/types.ts +338 -0
  353. package/carbon/supabase/migrations/00001_init.sql +717 -0
  354. package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
  355. package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
  356. package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
  357. package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
  358. package/carbon/supabase/seed.sql +16 -0
  359. package/carbon/tests/_helpers/app.ts +45 -0
  360. package/carbon/tests/_helpers/env.ts +37 -0
  361. package/carbon/tests/_helpers/factories.ts +69 -0
  362. package/carbon/tests/_helpers/jwt.ts +23 -0
  363. package/carbon/tests/_helpers/setup-integration.ts +20 -0
  364. package/carbon/tests/_helpers/setup-rtl.ts +12 -0
  365. package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
  366. package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
  367. package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
  368. package/carbon/tests/integration/server/routes/health.test.ts +61 -0
  369. package/carbon/tests/structural/i18n-parity.test.ts +42 -0
  370. package/carbon/tests/unit/client/utils.test.ts +49 -0
  371. package/carbon/tests/unit/shared/pricing.test.ts +93 -0
  372. package/carbon/tsconfig.json +27 -0
  373. package/carbon/tsconfig.server.json +27 -0
  374. package/carbon/tsconfig.test.json +9 -0
  375. package/carbon/vite.config.ts +110 -0
  376. package/carbon/vitest.config.ts +74 -0
  377. package/carbon/volumes/db/jwt.sql +57 -0
  378. package/carbon/volumes/db/metabase-init.sh +29 -0
  379. package/carbon/volumes/db/n8n-init.sh +25 -0
  380. package/carbon/volumes/db/realtime.sql +33 -0
  381. package/carbon/volumes/db/roles.sql +93 -0
  382. package/carbon/volumes/db/set-passwords.sh +12 -0
  383. package/carbon/volumes/db/super-admin.dev.sql +113 -0
  384. package/carbon/volumes/db/super-admin.generated.sql +113 -0
  385. package/carbon/volumes/db/super-admin.sql +114 -0
  386. package/carbon/volumes/grafana/dashboards/logs.json +179 -0
  387. package/carbon/volumes/grafana/dashboards/overview.json +523 -0
  388. package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
  389. package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
  390. package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
  391. package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
  392. package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
  393. package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
  394. package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
  395. package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
  396. package/carbon/volumes/kong/kong.yml +208 -0
  397. package/carbon/volumes/loki/loki-config.yml +58 -0
  398. package/carbon/volumes/n8n/hooks.js +131 -0
  399. package/carbon/volumes/n8n/scripts/setup.sh +66 -0
  400. package/carbon/volumes/prometheus/prometheus.yml +43 -0
  401. package/carbon/volumes/promtail/promtail-config.yml +64 -0
  402. package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
  403. package/carbon/volumes/traefik/middlewares.yml +95 -0
  404. package/carbon/volumes/traefik/vite-dev.yml +20 -0
  405. package/package.json +95 -0
  406. package/src/access.js +354 -0
  407. package/src/activate.js +187 -0
  408. package/src/add.js +718 -0
  409. package/src/backup.js +786 -0
  410. package/src/cli.js +350 -0
  411. package/src/configure.js +967 -0
  412. package/src/console.js +155 -0
  413. package/src/create.js +1499 -0
  414. package/src/deploy.js +311 -0
  415. package/src/destroy.js +2033 -0
  416. package/src/diagnose.js +735 -0
  417. package/src/down.js +80 -0
  418. package/src/failover.js +1032 -0
  419. package/src/lib/backup-s3.js +179 -0
  420. package/src/lib/build.js +33 -0
  421. package/src/lib/checksum.js +28 -0
  422. package/src/lib/ci-setup.js +666 -0
  423. package/src/lib/cli/help.js +129 -0
  424. package/src/lib/cli/parse-flags.js +160 -0
  425. package/src/lib/cli/select-action.js +65 -0
  426. package/src/lib/cli/select-environment.js +108 -0
  427. package/src/lib/cli/tty-guard.js +75 -0
  428. package/src/lib/cloudflare.js +447 -0
  429. package/src/lib/colors.js +52 -0
  430. package/src/lib/command.js +361 -0
  431. package/src/lib/config.js +359 -0
  432. package/src/lib/cost.js +103 -0
  433. package/src/lib/deploy/bundle.js +231 -0
  434. package/src/lib/deploy/compose/acme-verify.js +121 -0
  435. package/src/lib/deploy/compose/build-args.js +78 -0
  436. package/src/lib/deploy/compose/ha.js +1874 -0
  437. package/src/lib/deploy/compose/index.js +1294 -0
  438. package/src/lib/deploy/compose/reconcile.js +74 -0
  439. package/src/lib/deploy/github.js +382 -0
  440. package/src/lib/deploy/image.js +191 -0
  441. package/src/lib/deploy/index.js +119 -0
  442. package/src/lib/deploy/k8s/LICENSE +99 -0
  443. package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
  444. package/src/lib/deploy/k8s/ha/index.js +1000 -0
  445. package/src/lib/deploy/k8s/index.js +62 -0
  446. package/src/lib/deploy/k8s/k3s.js +2515 -0
  447. package/src/lib/deploy/orchestrator.js +1401 -0
  448. package/src/lib/deploy/prompts.js +545 -0
  449. package/src/lib/deploy/remote-build.js +130 -0
  450. package/src/lib/deploy/state.js +120 -0
  451. package/src/lib/deploy/utils.js +328 -0
  452. package/src/lib/deploy-logger.js +93 -0
  453. package/src/lib/dns-propagation.js +48 -0
  454. package/src/lib/environment.js +58 -0
  455. package/src/lib/fetch-retry.js +103 -0
  456. package/src/lib/github-environments.js +335 -0
  457. package/src/lib/hetzner-dns.js +377 -0
  458. package/src/lib/hetzner-guided-setup.js +275 -0
  459. package/src/lib/host-keys.js +37 -0
  460. package/src/lib/iac/cloud-init.js +52 -0
  461. package/src/lib/iac/index.js +325 -0
  462. package/src/lib/iac/programs/hetzner-compose.js +130 -0
  463. package/src/lib/iac/programs/hetzner-k8s.js +320 -0
  464. package/src/lib/kubectl.js +81 -0
  465. package/src/lib/licensing/index.js +259 -0
  466. package/src/lib/licensing/tiers.js +181 -0
  467. package/src/lib/licensing/validator.js +171 -0
  468. package/src/lib/merge-package-json.js +90 -0
  469. package/src/lib/operator-ip.js +381 -0
  470. package/src/lib/package-manager.js +111 -0
  471. package/src/lib/perf.js +71 -0
  472. package/src/lib/project-guard.js +39 -0
  473. package/src/lib/project.js +334 -0
  474. package/src/lib/providers/base.js +276 -0
  475. package/src/lib/providers/hetzner-s3.js +656 -0
  476. package/src/lib/providers/hetzner.js +755 -0
  477. package/src/lib/providers/index.js +164 -0
  478. package/src/lib/s3.js +510 -0
  479. package/src/lib/secret-scan.js +583 -0
  480. package/src/lib/secrets.js +63 -0
  481. package/src/lib/server-types.js +195 -0
  482. package/src/lib/shell.js +91 -0
  483. package/src/lib/ssh.js +241 -0
  484. package/src/lib/tracker.js +242 -0
  485. package/src/lib/upgrade-policy.js +170 -0
  486. package/src/lib/validators.js +105 -0
  487. package/src/lib/version.js +5 -0
  488. package/src/remove.js +292 -0
  489. package/src/reset.js +97 -0
  490. package/src/restore.js +871 -0
  491. package/src/scale.js +1734 -0
  492. package/src/shell.js +222 -0
  493. package/src/status.js +981 -0
  494. package/src/up.js +264 -0
  495. package/src/upgrade.js +721 -0
@@ -0,0 +1,67 @@
1
+ import { Globe } from 'lucide-react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuTrigger,
8
+ } from '@/components/ui/dropdown-menu';
9
+
10
+ /**
11
+ * Language codes and their display names.
12
+ * To add a new language: add an entry here and create a matching locale file
13
+ * at src/client/locales/<code>.json with all keys from en.json translated.
14
+ */
15
+ const LANGUAGE_OPTIONS: Record<string, string> = {
16
+ en: 'English',
17
+ es: 'Español',
18
+ fr: 'Français',
19
+ de: 'Deutsch',
20
+ pt: 'Português',
21
+ };
22
+
23
+ interface LanguageSwitcherProps {
24
+ /** Only show languages in this list. If empty/undefined, shows all. */
25
+ enabledLanguages?: string[];
26
+ className?: string;
27
+ }
28
+
29
+ /**
30
+ * Language switcher dropdown. Only renders when more than one language is enabled.
31
+ * Persists selection to localStorage via i18next's LanguageDetector.
32
+ */
33
+ export function LanguageSwitcher({ enabledLanguages, className }: LanguageSwitcherProps) {
34
+ const { i18n } = useTranslation();
35
+
36
+ const languages = enabledLanguages?.length
37
+ ? enabledLanguages.filter((code) => code in LANGUAGE_OPTIONS)
38
+ : Object.keys(LANGUAGE_OPTIONS);
39
+
40
+ // Don't render if only one language is available
41
+ if (languages.length <= 1) return null;
42
+
43
+ const currentLanguage = i18n.language?.split('-')[0] || 'en';
44
+ const currentLabel = LANGUAGE_OPTIONS[currentLanguage] || currentLanguage;
45
+
46
+ return (
47
+ <DropdownMenu>
48
+ <DropdownMenuTrigger
49
+ className={`cursor-pointer inline-flex items-center gap-1.5 rounded-full border border-border/50 bg-muted/30 px-3 h-8 text-sm font-medium hover:bg-muted/60 transition-colors ${className || ''}`}
50
+ >
51
+ <Globe className="size-4" />
52
+ <span>{currentLabel}</span>
53
+ </DropdownMenuTrigger>
54
+ <DropdownMenuContent align="end">
55
+ {languages.map((code) => (
56
+ <DropdownMenuItem
57
+ key={code}
58
+ onClick={() => i18n.changeLanguage(code)}
59
+ className={code === currentLanguage ? 'bg-accent' : ''}
60
+ >
61
+ {LANGUAGE_OPTIONS[code] || code}
62
+ </DropdownMenuItem>
63
+ ))}
64
+ </DropdownMenuContent>
65
+ </DropdownMenu>
66
+ );
67
+ }
@@ -0,0 +1,87 @@
1
+ import { cn } from '@/lib/utils';
2
+
3
+ // Split across two lines so the formatted output is stable regardless of the
4
+ // rendered project-name length (biome wraps >100-char lines, which caused CI
5
+ // lint failures in e2e when the project slug was long).
6
+ const DEFAULT_PROJECT_NAME = '{{PROJECT_NAME}}';
7
+ export const PROJECT_NAME = import.meta.env.VITE_PROJECT_NAME ?? DEFAULT_PROJECT_NAME;
8
+
9
+ interface LogoProps {
10
+ size?: 'sm' | 'default' | 'lg';
11
+ className?: string;
12
+ }
13
+
14
+ const sizeClasses = {
15
+ sm: 'size-6',
16
+ default: 'size-8',
17
+ lg: 'size-10',
18
+ };
19
+
20
+ /**
21
+ * Default logo mark — replace the SVG below with your own brand icon.
22
+ * Liquid glass hex with circular cutout — gradient fill + specular highlight.
23
+ */
24
+ export function Logo({ size = 'default', className }: LogoProps) {
25
+ return (
26
+ <svg
27
+ viewBox="0 0 64 64"
28
+ fill="none"
29
+ xmlns="http://www.w3.org/2000/svg"
30
+ className={cn('shrink-0', sizeClasses[size], className)}
31
+ aria-hidden="true"
32
+ >
33
+ <defs>
34
+ <linearGradient id="logo-fill" x1="0" y1="0" x2="0" y2="1">
35
+ <stop offset="0%" stopColor="#5eead4" />
36
+ <stop offset="100%" stopColor="#0d9488" />
37
+ </linearGradient>
38
+ <linearGradient id="logo-shine" x1="0.2" y1="0" x2="0.8" y2="0.6">
39
+ <stop offset="0%" stopColor="white" stopOpacity="0.5" />
40
+ <stop offset="100%" stopColor="white" stopOpacity="0" />
41
+ </linearGradient>
42
+ </defs>
43
+ <path
44
+ fillRule="evenodd"
45
+ clipRule="evenodd"
46
+ d="M32 2L58 17V47L32 62L6 47V17Z M48 32A16 16 0 1 1 16 32A16 16 0 1 1 48 32Z"
47
+ fill="url(#logo-fill)"
48
+ />
49
+ <path d="M32 2L58 17V30L32 38L6 30V17Z" fill="url(#logo-shine)" />
50
+ </svg>
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Logo mark + project name text.
56
+ * Sizes: sm (footers), default (nav/sidebar), lg (auth hero pages).
57
+ * Wrap in a <Link> with className="group" for hover effects.
58
+ */
59
+ interface WordmarkProps {
60
+ size?: 'sm' | 'default' | 'lg';
61
+ className?: string;
62
+ textClassName?: string;
63
+ }
64
+
65
+ const wordmarkConfig = {
66
+ sm: { logoSize: 'sm' as const, text: 'text-sm font-medium', gap: 'gap-1.5' },
67
+ default: {
68
+ logoSize: 'lg' as const,
69
+ text: 'text-[1.24rem] font-semibold tracking-tight',
70
+ gap: 'gap-1',
71
+ },
72
+ lg: {
73
+ logoSize: 'lg' as const,
74
+ text: 'text-[1.375rem] font-semibold tracking-tight',
75
+ gap: 'gap-1',
76
+ },
77
+ };
78
+
79
+ export function Wordmark({ size = 'default', className, textClassName }: WordmarkProps) {
80
+ const { logoSize, text, gap } = wordmarkConfig[size];
81
+ return (
82
+ <span className={cn('inline-flex items-center', gap, className)}>
83
+ <Logo size={logoSize} />
84
+ <span className={cn(text, textClassName)}>{PROJECT_NAME}</span>
85
+ </span>
86
+ );
87
+ }
@@ -0,0 +1,76 @@
1
+ import { motion } from 'framer-motion';
2
+ import { useTranslation } from 'react-i18next';
3
+ import aiderLogo from '../assets/logos/aider.svg';
4
+ import antigravityLogo from '../assets/logos/antigravity.svg';
5
+ import boltLogo from '../assets/logos/bolt.svg';
6
+ import claudeLogo from '../assets/logos/claude-code.svg';
7
+ import copilotLogo from '../assets/logos/copilot.svg';
8
+ import cursorLogo from '../assets/logos/cursor.svg';
9
+ import geminiLogo from '../assets/logos/gemini-cli.svg';
10
+ import lovableLogo from '../assets/logos/lovable.svg';
11
+ import openaiLogo from '../assets/logos/openai-codex.svg';
12
+ import v0Logo from '../assets/logos/v0.svg';
13
+ import windsurfLogo from '../assets/logos/windsurf.svg';
14
+
15
+ const LOGO_ITEMS = [
16
+ { src: claudeLogo, name: 'Claude Code', href: 'https://docs.anthropic.com/en/docs/claude-code' },
17
+ { src: geminiLogo, name: 'Gemini CLI', href: 'https://github.com/google-gemini/gemini-cli' },
18
+ { src: openaiLogo, name: 'OpenAI Codex', href: 'https://openai.com/codex/' },
19
+ { src: antigravityLogo, name: 'Antigravity', href: 'https://antigravityai.org/' },
20
+ { src: cursorLogo, name: 'Cursor', href: 'https://cursor.com' },
21
+ { src: windsurfLogo, name: 'Windsurf', href: 'https://windsurf.com' },
22
+ { src: boltLogo, name: 'Bolt', href: 'https://bolt.new' },
23
+ { src: v0Logo, name: 'v0', href: 'https://v0.dev' },
24
+ { src: lovableLogo, name: 'Lovable', href: 'https://lovable.dev' },
25
+ { src: aiderLogo, name: 'Aider', href: 'https://aider.chat' },
26
+ { src: copilotLogo, name: 'Copilot', href: 'https://github.com/features/copilot' },
27
+ ];
28
+
29
+ export function LogoStrip() {
30
+ const { t } = useTranslation();
31
+ return (
32
+ <motion.section
33
+ className="relative py-10 overflow-hidden border-y border-border/20 bg-gradient-to-b from-transparent via-white/[0.02] to-transparent"
34
+ initial={{ opacity: 0 }}
35
+ whileInView={{ opacity: 1 }}
36
+ viewport={{ once: true }}
37
+ >
38
+ <div className="max-w-7xl mx-auto">
39
+ <p className="text-center text-sm uppercase tracking-widest text-muted-foreground mb-6">
40
+ {t('landing.logoStrip.label')}
41
+ </p>
42
+ <div
43
+ className="relative overflow-hidden"
44
+ style={{
45
+ maskImage: 'linear-gradient(to right, transparent, black 15%, black 85%, transparent)',
46
+ }}
47
+ >
48
+ <div className="marquee-track flex w-max">
49
+ {[...LOGO_ITEMS, ...LOGO_ITEMS].map((item, i) => (
50
+ <a
51
+ // Marquee duplicates LOGO_ITEMS for the scroll loop, so the
52
+ // stable key is `${item.name}-${copyIndex}` — `i` divided by
53
+ // the original length tells us which copy we're on.
54
+ key={`${item.name}-${i < LOGO_ITEMS.length ? 'a' : 'b'}`}
55
+ href={item.href}
56
+ target="_blank"
57
+ rel="noopener noreferrer"
58
+ className="group flex items-center gap-2.5 px-8 shrink-0"
59
+ >
60
+ <img
61
+ src={item.src}
62
+ alt=""
63
+ style={{ height: 22, width: 'auto' }}
64
+ className="dark:brightness-0 dark:invert opacity-70 group-hover:brightness-100 group-hover:invert-0 group-hover:opacity-100 transition-all duration-300"
65
+ />
66
+ <span className="text-sm font-medium text-muted-foreground/70 group-hover:text-foreground whitespace-nowrap transition-colors duration-300">
67
+ {item.name}
68
+ </span>
69
+ </a>
70
+ ))}
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </motion.section>
75
+ );
76
+ }
@@ -0,0 +1,48 @@
1
+ import { motion, useInView } from 'framer-motion';
2
+ import { useRef } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ export function MetricsStrip() {
6
+ const { t } = useTranslation();
7
+ const ref = useRef<HTMLElement>(null);
8
+ const isInView = useInView(ref, { once: true, margin: '-50px' });
9
+
10
+ const METRICS = [
11
+ { value: t('landing.metrics.time.value'), label: t('landing.metrics.time.label') },
12
+ { value: t('landing.metrics.services.value'), label: t('landing.metrics.services.label') },
13
+ { value: t('landing.metrics.config.value'), label: t('landing.metrics.config.label') },
14
+ { value: t('landing.metrics.commands.value'), label: t('landing.metrics.commands.label') },
15
+ ];
16
+
17
+ return (
18
+ <section ref={ref} className="relative overflow-hidden border-y border-border/30">
19
+ <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-primary/30 to-transparent" />
20
+ <div className="absolute inset-x-0 bottom-0 h-px bg-gradient-to-r from-transparent via-primary/30 to-transparent" />
21
+
22
+ {/* Horizontal gradient band behind */}
23
+ <div className="absolute inset-0 bg-gradient-to-r from-transparent via-primary/5 to-transparent" />
24
+ <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[300px] rounded-full bg-primary/6 blur-[120px] pointer-events-none" />
25
+
26
+ <div className="relative mx-auto max-w-7xl px-6 py-16">
27
+ <div className="grid grid-cols-2 md:grid-cols-4 divide-x divide-border/30">
28
+ {METRICS.map((metric, i) => (
29
+ <motion.div
30
+ key={metric.label}
31
+ initial={{ opacity: 0, y: 20 }}
32
+ animate={isInView ? { opacity: 1, y: 0 } : {}}
33
+ transition={{ duration: 0.5, delay: i * 0.1 }}
34
+ className="px-6 py-4 text-center hover:bg-primary/[0.03] transition-colors duration-300"
35
+ >
36
+ <div className="metric-value text-2xl sm:text-3xl md:text-4xl font-black tracking-tight mb-2">
37
+ {metric.value}
38
+ </div>
39
+ <div className="text-xs sm:text-sm text-muted-foreground uppercase tracking-widest">
40
+ {metric.label}
41
+ </div>
42
+ </motion.div>
43
+ ))}
44
+ </div>
45
+ </div>
46
+ </section>
47
+ );
48
+ }
@@ -0,0 +1,195 @@
1
+ import { ArrowRight, Menu, Star, X } from 'lucide-react';
2
+ import { useEffect, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Link } from 'react-router-dom';
5
+ import { useAuthSettings } from '@/hooks/api';
6
+ import { cn } from '@/lib/utils';
7
+ import { useAuth } from './auth/AuthProvider';
8
+ import { LanguageSwitcher } from './LanguageSwitcher';
9
+ import { Wordmark } from './Logo';
10
+ import { Button } from './ui/button';
11
+
12
+ const GITHUB_REPO = 'hyperformant/vibecarbon';
13
+
14
+ function GitHubStarButton({ className }: { className?: string }) {
15
+ const [stars, setStars] = useState<number | null>(null);
16
+
17
+ useEffect(() => {
18
+ fetch(`https://api.github.com/repos/${GITHUB_REPO}`)
19
+ .then((r) => r.json())
20
+ .then((d) => {
21
+ if (typeof d.stargazers_count === 'number') setStars(d.stargazers_count);
22
+ })
23
+ .catch(() => {});
24
+ }, []);
25
+
26
+ const formatted =
27
+ stars !== null
28
+ ? stars >= 1000
29
+ ? `${(stars / 1000).toFixed(1).replace(/\.0$/, '')}k`
30
+ : String(stars)
31
+ : null;
32
+
33
+ return (
34
+ <a
35
+ href={`https://github.com/${GITHUB_REPO}`}
36
+ target="_blank"
37
+ rel="noopener noreferrer"
38
+ className={cn(
39
+ 'inline-flex items-center gap-2 rounded-full border border-border/50 bg-muted/30 px-3 h-8 text-sm font-medium text-foreground transition-colors hover:bg-muted/60',
40
+ className
41
+ )}
42
+ >
43
+ <svg className="size-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
44
+ <path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z" />
45
+ </svg>
46
+ <Star className="size-3.5 fill-amber-400 text-amber-400" />
47
+ {formatted !== null && <span>{formatted}</span>}
48
+ </a>
49
+ );
50
+ }
51
+
52
+ export function Nav() {
53
+ const { t } = useTranslation();
54
+ const { user } = useAuth();
55
+ const { data: authSettings } = useAuthSettings();
56
+ const [isScrolled, setIsScrolled] = useState(false);
57
+
58
+ const enabledLanguages = authSettings?.settings?.enabledLanguages ?? ['en'];
59
+ const localizationEnabled = authSettings?.settings?.localizationEnabled ?? true;
60
+ const [mobileOpen, setMobileOpen] = useState(false);
61
+
62
+ useEffect(() => {
63
+ const handleScroll = () => {
64
+ setIsScrolled(window.scrollY > 20);
65
+ };
66
+
67
+ window.addEventListener('scroll', handleScroll, { passive: true });
68
+ return () => window.removeEventListener('scroll', handleScroll);
69
+ }, []);
70
+
71
+ // Close mobile menu on resize to desktop
72
+ useEffect(() => {
73
+ const handleResize = () => {
74
+ if (window.innerWidth >= 768) setMobileOpen(false);
75
+ };
76
+ window.addEventListener('resize', handleResize);
77
+ return () => window.removeEventListener('resize', handleResize);
78
+ }, []);
79
+
80
+ return (
81
+ <nav className="fixed inset-x-0 z-50 h-20" style={{ top: 'var(--notification-bar-h, 0px)' }}>
82
+ {/* Background layer that fades in/out smoothly */}
83
+ <div
84
+ className={cn(
85
+ 'absolute inset-0 bg-background/80 backdrop-blur-xl border-b border-border/50 shadow-sm transition-opacity duration-500 ease-out',
86
+ isScrolled || mobileOpen ? 'opacity-100' : 'opacity-0'
87
+ )}
88
+ />
89
+ <div className="relative mx-auto max-w-7xl h-full px-6 flex items-center justify-between">
90
+ {/* Logo & Brand */}
91
+ <Link to="/" className="group">
92
+ <Wordmark size="lg" />
93
+ </Link>
94
+
95
+ {/* Desktop nav */}
96
+ <div className="hidden md:flex items-center gap-2">
97
+ {localizationEnabled && <LanguageSwitcher enabledLanguages={enabledLanguages} />}
98
+ <GitHubStarButton />
99
+ <Button variant="ghost" size="sm" asChild>
100
+ <Link to="/docs">{t('nav.docs')}</Link>
101
+ </Button>
102
+ <Button variant="ghost" size="sm" asChild>
103
+ <Link to="/api/docs">{t('nav.apiDocs')}</Link>
104
+ </Button>
105
+ {user ? (
106
+ <Button variant="exciting" size="sm" asChild className="font-semibold gap-2">
107
+ <Link to="/dashboard">
108
+ {t('nav.dashboard')}
109
+ <ArrowRight className="size-4 shrink-0" />
110
+ </Link>
111
+ </Button>
112
+ ) : (
113
+ <>
114
+ <Button variant="ghost" size="sm" asChild>
115
+ <Link to="/login">{t('nav.logIn')}</Link>
116
+ </Button>
117
+ <Button size="sm" asChild>
118
+ <Link to="/signup">{t('nav.signUp')}</Link>
119
+ </Button>
120
+ </>
121
+ )}
122
+ </div>
123
+
124
+ {/* Mobile hamburger */}
125
+ <button
126
+ type="button"
127
+ className="relative md:hidden p-2 text-muted-foreground hover:text-foreground transition-colors"
128
+ onClick={() => setMobileOpen(!mobileOpen)}
129
+ aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
130
+ >
131
+ {mobileOpen ? <X className="size-6" /> : <Menu className="size-6" />}
132
+ </button>
133
+ </div>
134
+
135
+ {/* Mobile menu */}
136
+ {mobileOpen && (
137
+ <div className="relative md:hidden bg-background/95 backdrop-blur-xl border-b border-border">
138
+ <div className="mx-auto max-w-7xl px-6 py-6 flex flex-col gap-4">
139
+ <div className="flex items-center gap-3">
140
+ <GitHubStarButton className="w-fit" />
141
+ {localizationEnabled && <LanguageSwitcher enabledLanguages={enabledLanguages} />}
142
+ </div>
143
+ <Link
144
+ to="/docs"
145
+ onClick={() => setMobileOpen(false)}
146
+ className="text-foreground font-medium transition-colors text-base"
147
+ >
148
+ {t('nav.docs')}
149
+ </Link>
150
+ <Link
151
+ to="/api/docs"
152
+ onClick={() => setMobileOpen(false)}
153
+ className="text-foreground font-medium transition-colors text-base"
154
+ >
155
+ {t('nav.apiDocs')}
156
+ </Link>
157
+ <div className="flex flex-col gap-3 pt-2">
158
+ {user ? (
159
+ <Button
160
+ variant="exciting"
161
+ size="lg"
162
+ asChild
163
+ className="w-full text-base font-semibold justify-center gap-2"
164
+ >
165
+ <Link to="/dashboard" onClick={() => setMobileOpen(false)}>
166
+ {t('nav.dashboard')}
167
+ <ArrowRight className="size-4 shrink-0" />
168
+ </Link>
169
+ </Button>
170
+ ) : (
171
+ <>
172
+ <Button
173
+ variant="ghost"
174
+ size="lg"
175
+ asChild
176
+ className="w-full text-base justify-center"
177
+ >
178
+ <Link to="/login" onClick={() => setMobileOpen(false)}>
179
+ {t('nav.logIn')}
180
+ </Link>
181
+ </Button>
182
+ <Button size="lg" asChild className="w-full text-base justify-center">
183
+ <Link to="/signup" onClick={() => setMobileOpen(false)}>
184
+ {t('nav.signUp')}
185
+ </Link>
186
+ </Button>
187
+ </>
188
+ )}
189
+ </div>
190
+ </div>
191
+ </div>
192
+ )}
193
+ </nav>
194
+ );
195
+ }
@@ -0,0 +1,147 @@
1
+ import { useMutation } from '@tanstack/react-query';
2
+ import { CheckCircle, Loader2 } from 'lucide-react';
3
+ import { useState } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+
8
+ interface NewsletterSignupProps {
9
+ className?: string;
10
+ variant?: 'inline' | 'stacked';
11
+ buttonText?: string;
12
+ }
13
+
14
+ /**
15
+ * Newsletter signup form. Embeddable in landing pages, footers, or standalone.
16
+ * Uses double opt-in (sends confirmation email).
17
+ */
18
+ export function NewsletterSignup({
19
+ className,
20
+ variant = 'inline',
21
+ buttonText,
22
+ }: NewsletterSignupProps) {
23
+ const { t } = useTranslation();
24
+ const resolvedButtonText = buttonText ?? t('landing.newsletter.button');
25
+ const [email, setEmail] = useState('');
26
+ const [validationError, setValidationError] = useState<string | null>(null);
27
+
28
+ const subscribeMutation = useMutation({
29
+ mutationFn: async () => {
30
+ const response = await fetch('/api/v1/newsletter/subscribe', {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify({ email }),
34
+ });
35
+ if (!response.ok) {
36
+ const err = await response.json();
37
+ throw new Error(err.error || 'Failed to subscribe');
38
+ }
39
+ return response.json();
40
+ },
41
+ onSuccess: () => {
42
+ setEmail('');
43
+ setValidationError(null);
44
+ },
45
+ });
46
+
47
+ const handleSubmit = () => {
48
+ if (!email.trim()) {
49
+ setValidationError(t('landing.newsletter.errorEmpty'));
50
+ return;
51
+ }
52
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
53
+ setValidationError(t('landing.newsletter.errorInvalid'));
54
+ return;
55
+ }
56
+ setValidationError(null);
57
+ subscribeMutation.mutate();
58
+ };
59
+
60
+ const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
61
+ setEmail(e.target.value);
62
+ if (validationError) setValidationError(null);
63
+ };
64
+
65
+ const hasError = !!validationError || !!subscribeMutation.error;
66
+ const errorMessage = validationError || subscribeMutation.error?.message;
67
+
68
+ if (subscribeMutation.isSuccess) {
69
+ return (
70
+ <div className={`flex items-center gap-2 text-sm ${className || ''}`}>
71
+ <CheckCircle className="size-4 text-primary" />
72
+ <span className="text-muted-foreground">{t('landing.newsletter.success')}</span>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ if (variant === 'stacked') {
78
+ return (
79
+ <div className={`space-y-2 ${className || ''}`}>
80
+ <Input
81
+ type="email"
82
+ value={email}
83
+ onChange={handleEmailChange}
84
+ placeholder={t('landing.newsletter.placeholder')}
85
+ className="bg-background"
86
+ aria-invalid={hasError}
87
+ onKeyDown={(e) => {
88
+ if (e.key === 'Enter') {
89
+ e.preventDefault();
90
+ handleSubmit();
91
+ }
92
+ }}
93
+ />
94
+ <Button
95
+ onClick={handleSubmit}
96
+ disabled={subscribeMutation.isPending}
97
+ className="w-full"
98
+ size="sm"
99
+ sparkle
100
+ >
101
+ {subscribeMutation.isPending && <Loader2 className="size-3 mr-1 animate-spin" />}
102
+ {resolvedButtonText}
103
+ </Button>
104
+ {hasError && (
105
+ <p className="text-sm text-destructive text-center animate-in fade-in slide-in-from-top-1 duration-200">
106
+ {errorMessage}
107
+ </p>
108
+ )}
109
+ </div>
110
+ );
111
+ }
112
+
113
+ return (
114
+ <div className={`flex flex-col gap-2 ${className || ''}`}>
115
+ <div className="flex items-center gap-2">
116
+ <Input
117
+ type="email"
118
+ value={email}
119
+ onChange={handleEmailChange}
120
+ placeholder={t('landing.newsletter.placeholder')}
121
+ className="bg-background min-w-[280px] sm:min-w-[340px] h-9"
122
+ aria-invalid={hasError}
123
+ onKeyDown={(e) => {
124
+ if (e.key === 'Enter') {
125
+ e.preventDefault();
126
+ handleSubmit();
127
+ }
128
+ }}
129
+ />
130
+ <Button
131
+ onClick={handleSubmit}
132
+ disabled={subscribeMutation.isPending}
133
+ size="default"
134
+ sparkle
135
+ >
136
+ {subscribeMutation.isPending && <Loader2 className="size-3 mr-1 animate-spin" />}
137
+ {resolvedButtonText}
138
+ </Button>
139
+ </div>
140
+ {hasError && (
141
+ <p className="text-sm text-destructive text-center animate-in fade-in slide-in-from-top-1 duration-200">
142
+ {errorMessage}
143
+ </p>
144
+ )}
145
+ </div>
146
+ );
147
+ }