create-ej-app 26.0.0 → 27.0.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 (357) hide show
  1. package/README.md +125 -226
  2. package/cli.ts +79 -29
  3. package/package.json +2 -2
  4. package/template/api/.vscode/settings.json +1 -1
  5. package/template/api/{.github/copilot-instructions.md → AGENTS.md} +0 -2
  6. package/template/api/CLAUDE.md +1 -0
  7. package/template/api/package.json +10 -10
  8. package/template/nextjs/.dockerignore +1 -0
  9. package/template/nextjs/AGENTS.md +260 -0
  10. package/template/nextjs/CLAUDE.md +1 -0
  11. package/template/nextjs/README.md +109 -0
  12. package/template/nextjs/bunfig.toml +3 -0
  13. package/template/nextjs/eslint.config.mjs +6 -5
  14. package/template/nextjs/gitignore +6 -1
  15. package/template/nextjs/next.config.ts +8 -1
  16. package/template/nextjs/package.json +46 -39
  17. package/template/nextjs/public/logo-dark.svg +4 -0
  18. package/template/nextjs/public/logo.svg +4 -0
  19. package/template/nextjs/src/app/(admin)/actions/users.ts +183 -0
  20. package/template/nextjs/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
  21. package/template/nextjs/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
  22. package/template/nextjs/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
  23. package/template/nextjs/src/app/(admin)/admin/layout.tsx +53 -0
  24. package/template/nextjs/src/app/(admin)/admin/page.tsx +11 -2
  25. package/template/nextjs/src/app/(admin)/admin/settings/layout.tsx +24 -0
  26. package/template/nextjs/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
  27. package/template/nextjs/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
  28. package/template/nextjs/src/app/(admin)/admin/settings/users/page.tsx +377 -0
  29. package/template/nextjs/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
  30. package/template/nextjs/src/app/(admin)/admin/settings/users/validation.ts +23 -0
  31. package/template/nextjs/src/app/(app)/app/layout.tsx +44 -0
  32. package/template/nextjs/src/app/(app)/app/page.tsx +14 -2
  33. package/template/nextjs/src/app/(auth)/(account)/layout.tsx +64 -0
  34. package/template/nextjs/src/app/(auth)/(account)/profile/page.tsx +16 -0
  35. package/template/nextjs/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
  36. package/template/nextjs/src/app/(auth)/login/email-login-form.tsx +56 -29
  37. package/template/nextjs/src/app/(auth)/login/page.tsx +45 -12
  38. package/template/nextjs/src/app/(auth)/login/social-login-buttons.tsx +85 -17
  39. package/template/nextjs/src/app/(marketing)/layout.tsx +35 -4
  40. package/template/nextjs/src/app/(marketing)/legal.tsx +56 -0
  41. package/template/nextjs/src/app/(marketing)/page.tsx +20 -2
  42. package/template/nextjs/src/app/(marketing)/privacy/page.tsx +43 -2
  43. package/template/nextjs/src/app/(marketing)/terms/page.tsx +33 -2
  44. package/template/nextjs/src/app/error.tsx +37 -0
  45. package/template/nextjs/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
  46. package/template/nextjs/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
  47. package/template/nextjs/src/app/fonts/index.ts +60 -0
  48. package/template/nextjs/src/app/global-error.tsx +38 -0
  49. package/template/nextjs/src/app/globals.css +29 -0
  50. package/template/nextjs/src/app/layout.tsx +45 -18
  51. package/template/nextjs/src/app/not-found.tsx +25 -3
  52. package/template/nextjs/src/app/robots.ts +29 -0
  53. package/template/nextjs/src/app/sitemap.ts +21 -0
  54. package/template/nextjs/src/components/confirm-dialog.tsx +79 -0
  55. package/template/nextjs/src/components/dashboard-header.tsx +70 -0
  56. package/template/nextjs/src/components/data-table/enum.ts +5 -0
  57. package/template/nextjs/src/components/data-table/features.ts +49 -0
  58. package/template/nextjs/src/components/data-table/index.tsx +961 -0
  59. package/template/nextjs/src/components/data-table/types.ts +76 -0
  60. package/template/nextjs/src/components/error.tsx +71 -0
  61. package/template/nextjs/src/components/loader.tsx +18 -2
  62. package/template/nextjs/src/components/logo.tsx +49 -0
  63. package/template/nextjs/src/components/navigation-search.tsx +140 -0
  64. package/template/nextjs/src/components/navigation-sidebar.tsx +241 -0
  65. package/template/nextjs/src/components/page-heading.tsx +38 -0
  66. package/template/nextjs/src/components/react-query-provider.tsx +41 -2
  67. package/template/nextjs/src/components/route-error.tsx +55 -0
  68. package/template/nextjs/src/components/screen-size.tsx +36 -0
  69. package/template/nextjs/src/components/site-footer.tsx +20 -4
  70. package/template/nextjs/src/components/theme-provider.tsx +48 -0
  71. package/template/nextjs/src/components/theme-toggle.tsx +58 -5
  72. package/template/nextjs/src/components/ui/alert-dialog.tsx +199 -0
  73. package/template/nextjs/src/components/ui/avatar.tsx +112 -0
  74. package/template/nextjs/src/components/ui/badge.tsx +54 -0
  75. package/template/nextjs/src/components/ui/button.tsx +3 -1
  76. package/template/nextjs/src/components/ui/card.tsx +4 -4
  77. package/template/nextjs/src/components/ui/collapsible.tsx +33 -0
  78. package/template/nextjs/src/components/ui/command.tsx +192 -0
  79. package/template/nextjs/src/components/ui/dialog.tsx +164 -0
  80. package/template/nextjs/src/components/ui/dropdown-menu.tsx +273 -0
  81. package/template/nextjs/src/components/ui/input-group.tsx +156 -0
  82. package/template/nextjs/src/components/ui/select.tsx +195 -0
  83. package/template/nextjs/src/components/ui/sheet.tsx +143 -0
  84. package/template/nextjs/src/components/ui/sidebar.tsx +702 -0
  85. package/template/nextjs/src/components/ui/skeleton.tsx +13 -0
  86. package/template/nextjs/src/components/ui/table.tsx +116 -0
  87. package/template/nextjs/src/components/ui/textarea.tsx +18 -0
  88. package/template/nextjs/src/components/ui/tooltip.tsx +57 -0
  89. package/template/nextjs/src/components/user-menu.tsx +206 -0
  90. package/template/nextjs/src/db/common.ts +10 -0
  91. package/template/nextjs/src/db/index.ts +16 -6
  92. package/template/nextjs/src/db/relations.ts +21 -0
  93. package/template/nextjs/src/db/schema.ts +85 -46
  94. package/template/nextjs/src/emails/components/email-layout.tsx +27 -11
  95. package/template/nextjs/src/emails/magic-link.tsx +15 -13
  96. package/template/nextjs/src/emails/test.tsx +11 -9
  97. package/template/nextjs/src/emails/welcome.tsx +12 -9
  98. package/template/nextjs/src/env.ts +1 -1
  99. package/template/nextjs/src/hooks/use-mobile.ts +14 -12
  100. package/template/nextjs/src/instrumentation.ts +20 -2
  101. package/template/nextjs/src/lib/actions.ts +55 -0
  102. package/template/nextjs/src/lib/admin-navigation.ts +43 -0
  103. package/template/nextjs/src/lib/auth-client.ts +20 -9
  104. package/template/nextjs/src/lib/auth-permissions.ts +61 -0
  105. package/template/nextjs/src/lib/auth.ts +33 -3
  106. package/template/nextjs/src/lib/callback-url.ts +28 -0
  107. package/template/nextjs/src/lib/constants.ts +21 -0
  108. package/template/nextjs/src/lib/delay.ts +10 -0
  109. package/template/nextjs/src/lib/email-service.ts +21 -0
  110. package/template/nextjs/src/lib/errors.ts +74 -0
  111. package/template/nextjs/src/lib/format-currency.ts +22 -24
  112. package/template/nextjs/src/lib/format-date.ts +69 -6
  113. package/template/nextjs/src/lib/nodemailer.ts +27 -3
  114. package/template/nextjs/src/lib/otel.ts +92 -2
  115. package/template/nextjs/src/lib/safe-action-client.ts +60 -9
  116. package/template/nextjs/src/lib/safe-action.ts +152 -16
  117. package/template/nextjs/src/lib/search.ts +16 -0
  118. package/template/nextjs/src/lib/seo.ts +7 -0
  119. package/template/nextjs/src/lib/siteConfig.ts +17 -1
  120. package/template/nextjs/src/lib/toast-message.ts +87 -1
  121. package/template/nextjs/src/lib/validations.ts +30 -0
  122. package/template/nextjs/src/proxy.ts +48 -0
  123. package/template/nextjs/src/scripts/reset-db.ts +22 -0
  124. package/template/nextjs/src/scripts/seed.ts +59 -8
  125. package/template/nextjs/src/scripts/test-mail.ts +28 -20
  126. package/template/nextjs/tests/admin/users.test.ts +64 -0
  127. package/template/nextjs/tests/db/fk-indexes.test.ts +62 -0
  128. package/template/nextjs/tests/db/seed.test.ts +49 -0
  129. package/template/nextjs/tests/helpers/action.ts +71 -0
  130. package/template/nextjs/tests/helpers/fixtures.ts +78 -0
  131. package/template/nextjs/tests/helpers/index.ts +2 -0
  132. package/template/nextjs/tests/lib/callback-url.test.ts +48 -0
  133. package/template/nextjs/tests/lib/client-server-boundary.test.ts +57 -0
  134. package/template/nextjs/tests/lib/errors.test.ts +10 -0
  135. package/template/nextjs/tests/lib/search.test.ts +23 -0
  136. package/template/nextjs/tests/setup/pglite-db.ts +63 -0
  137. package/template/nextjs/tests/setup/preload.ts +95 -0
  138. package/template/nextjs/tsconfig.json +1 -0
  139. package/template/saas-starter/.dockerignore +1 -0
  140. package/template/saas-starter/.env.example +15 -15
  141. package/template/saas-starter/AGENTS.md +302 -0
  142. package/template/saas-starter/CLAUDE.md +1 -0
  143. package/template/saas-starter/README.md +124 -2
  144. package/template/saas-starter/bunfig.toml +3 -0
  145. package/template/saas-starter/eslint.config.mjs +6 -5
  146. package/template/saas-starter/gitignore +6 -1
  147. package/template/saas-starter/next.config.ts +19 -1
  148. package/template/saas-starter/package.json +49 -44
  149. package/template/saas-starter/public/logo-dark.svg +4 -0
  150. package/template/saas-starter/public/logo.svg +4 -0
  151. package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
  152. package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
  153. package/template/saas-starter/src/app/(admin)/actions/users.ts +145 -59
  154. package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +4 -1
  155. package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +12 -10
  156. package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
  157. package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
  158. package/template/saas-starter/src/app/(admin)/admin/layout.tsx +24 -28
  159. package/template/saas-starter/src/app/(admin)/admin/page.tsx +59 -2
  160. package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
  161. package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
  162. package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
  163. package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +372 -10
  164. package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
  165. package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
  166. package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
  167. package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
  168. package/template/saas-starter/src/app/(app)/actions/billing.ts +171 -107
  169. package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
  170. package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
  171. package/template/saas-starter/src/app/(app)/actions/members.ts +149 -123
  172. package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
  173. package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
  174. package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +6 -9
  175. package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +6 -9
  176. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +45 -53
  177. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +40 -47
  178. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +80 -105
  179. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +72 -82
  180. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +51 -52
  181. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
  182. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +15 -16
  183. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +77 -44
  184. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +11 -18
  185. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +14 -28
  186. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
  187. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
  188. package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +15 -13
  189. package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +15 -6
  190. package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +6 -3
  191. package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
  192. package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +5 -9
  193. package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +68 -8
  194. package/template/saas-starter/src/app/(app)/app/layout.tsx +44 -32
  195. package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +75 -87
  196. package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +10 -12
  197. package/template/saas-starter/src/app/(app)/app/page.tsx +16 -21
  198. package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
  199. package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
  200. package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
  201. package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
  202. package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
  203. package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +49 -27
  204. package/template/saas-starter/src/app/(auth)/login/page.tsx +39 -21
  205. package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +80 -18
  206. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +16 -10
  207. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +28 -42
  208. package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
  209. package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
  210. package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
  211. package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
  212. package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
  213. package/template/saas-starter/src/app/(marketing)/layout.tsx +38 -4
  214. package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
  215. package/template/saas-starter/src/app/(marketing)/page.tsx +23 -2
  216. package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +62 -2
  217. package/template/saas-starter/src/app/(marketing)/terms/page.tsx +40 -2
  218. package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
  219. package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
  220. package/template/saas-starter/src/app/actions/uploads.ts +36 -0
  221. package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
  222. package/template/saas-starter/src/app/error.tsx +37 -0
  223. package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
  224. package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
  225. package/template/saas-starter/src/app/fonts/index.ts +60 -0
  226. package/template/saas-starter/src/app/global-error.tsx +38 -0
  227. package/template/saas-starter/src/app/globals.css +29 -0
  228. package/template/saas-starter/src/app/layout.tsx +43 -19
  229. package/template/saas-starter/src/app/not-found.tsx +25 -3
  230. package/template/saas-starter/src/app/robots.ts +14 -10
  231. package/template/saas-starter/src/app/sitemap.ts +2 -1
  232. package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
  233. package/template/saas-starter/src/components/dashboard-header.tsx +7 -1
  234. package/template/saas-starter/src/components/data-table/features.ts +49 -0
  235. package/template/saas-starter/src/components/data-table/index.tsx +65 -64
  236. package/template/saas-starter/src/components/data-table/types.ts +5 -4
  237. package/template/saas-starter/src/components/error.tsx +45 -0
  238. package/template/saas-starter/src/components/file-upload.tsx +65 -26
  239. package/template/saas-starter/src/components/loader.tsx +18 -2
  240. package/template/saas-starter/src/components/logo.tsx +49 -0
  241. package/template/saas-starter/src/components/navigation-search.tsx +2 -2
  242. package/template/saas-starter/src/components/page-heading.tsx +6 -0
  243. package/template/saas-starter/src/components/react-query-provider.tsx +41 -2
  244. package/template/saas-starter/src/components/route-error.tsx +55 -0
  245. package/template/saas-starter/src/components/screen-size.tsx +36 -0
  246. package/template/saas-starter/src/components/site-footer.tsx +20 -4
  247. package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
  248. package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
  249. package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
  250. package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
  251. package/template/saas-starter/src/components/theme-provider.tsx +48 -0
  252. package/template/saas-starter/src/components/theme-toggle.tsx +58 -5
  253. package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
  254. package/template/saas-starter/src/components/ui/badge.tsx +6 -1
  255. package/template/saas-starter/src/components/ui/button.tsx +2 -0
  256. package/template/saas-starter/src/components/ui/command.tsx +1 -1
  257. package/template/saas-starter/src/components/user-menu.tsx +147 -24
  258. package/template/saas-starter/src/db/common.ts +33 -1
  259. package/template/saas-starter/src/db/enums.ts +24 -0
  260. package/template/saas-starter/src/db/index.ts +14 -5
  261. package/template/saas-starter/src/db/relations.ts +40 -3
  262. package/template/saas-starter/src/db/schema.ts +186 -35
  263. package/template/saas-starter/src/emails/components/email-layout.tsx +27 -11
  264. package/template/saas-starter/src/emails/invitation.tsx +34 -21
  265. package/template/saas-starter/src/emails/magic-link.tsx +15 -13
  266. package/template/saas-starter/src/emails/test.tsx +11 -9
  267. package/template/saas-starter/src/emails/welcome.tsx +12 -9
  268. package/template/saas-starter/src/env.ts +1 -1
  269. package/template/saas-starter/src/hooks/use-mobile.ts +14 -12
  270. package/template/saas-starter/src/instrumentation.ts +20 -2
  271. package/template/saas-starter/src/lib/actions.ts +34 -0
  272. package/template/saas-starter/src/lib/admin-navigation.ts +33 -1
  273. package/template/saas-starter/src/lib/app-navigation.ts +45 -41
  274. package/template/saas-starter/src/lib/auth-client.ts +8 -3
  275. package/template/saas-starter/src/lib/auth-permissions.ts +50 -11
  276. package/template/saas-starter/src/lib/auth.ts +122 -21
  277. package/template/saas-starter/src/lib/billing.ts +59 -24
  278. package/template/saas-starter/src/lib/callback-url.ts +28 -0
  279. package/template/saas-starter/src/lib/constants.ts +78 -9
  280. package/template/saas-starter/src/lib/delay.ts +10 -0
  281. package/template/saas-starter/src/lib/email-service.ts +30 -1
  282. package/template/saas-starter/src/lib/errors.ts +74 -0
  283. package/template/saas-starter/src/lib/format-currency.ts +22 -24
  284. package/template/saas-starter/src/lib/format-date.ts +69 -6
  285. package/template/saas-starter/src/lib/image-url.ts +54 -0
  286. package/template/saas-starter/src/lib/nodemailer.ts +27 -3
  287. package/template/saas-starter/src/lib/organization-access.ts +56 -76
  288. package/template/saas-starter/src/lib/otel.ts +92 -2
  289. package/template/saas-starter/src/lib/plan-limits.ts +37 -0
  290. package/template/saas-starter/src/lib/plans.ts +40 -13
  291. package/template/saas-starter/src/lib/rbac.ts +44 -0
  292. package/template/saas-starter/src/lib/safe-action-client.ts +60 -9
  293. package/template/saas-starter/src/lib/safe-action.ts +137 -17
  294. package/template/saas-starter/src/lib/search.ts +16 -0
  295. package/template/saas-starter/src/lib/seo.ts +7 -0
  296. package/template/saas-starter/src/lib/siteConfig.ts +17 -1
  297. package/template/saas-starter/src/lib/storage.ts +176 -46
  298. package/template/saas-starter/src/lib/stripe.ts +4 -1
  299. package/template/saas-starter/src/lib/toast-message.ts +87 -1
  300. package/template/saas-starter/src/lib/validations.ts +85 -2
  301. package/template/saas-starter/src/proxy.ts +48 -0
  302. package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
  303. package/template/saas-starter/src/scripts/seed.ts +82 -8
  304. package/template/saas-starter/src/scripts/setup-db.ts +72 -0
  305. package/template/saas-starter/src/scripts/setup-stripe-plans.ts +13 -7
  306. package/template/saas-starter/src/scripts/test-mail.ts +28 -20
  307. package/template/saas-starter/tests/admin/users.test.ts +64 -0
  308. package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
  309. package/template/saas-starter/tests/db/seed.test.ts +57 -0
  310. package/template/saas-starter/tests/helpers/action.ts +71 -0
  311. package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
  312. package/template/saas-starter/tests/helpers/index.ts +2 -0
  313. package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
  314. package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
  315. package/template/saas-starter/tests/lib/errors.test.ts +10 -0
  316. package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
  317. package/template/saas-starter/tests/lib/search.test.ts +23 -0
  318. package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
  319. package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
  320. package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
  321. package/template/saas-starter/tests/setup/preload.ts +108 -0
  322. package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
  323. package/template/saas-starter/tsconfig.json +1 -0
  324. package/cli.js +0 -147
  325. package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
  326. package/template/nextjs/src/app/(app)/layout.tsx +0 -20
  327. package/template/nextjs/terraform/artifact-registry.tf +0 -5
  328. package/template/nextjs/terraform/backend-service.tf +0 -13
  329. package/template/nextjs/terraform/cleanup.sh +0 -9
  330. package/template/nextjs/terraform/cloudbuild.tf +0 -70
  331. package/template/nextjs/terraform/cloudrun.tf +0 -68
  332. package/template/nextjs/terraform/database.tf +0 -60
  333. package/template/nextjs/terraform/forwarding-rule.tf +0 -43
  334. package/template/nextjs/terraform/gitignore +0 -4
  335. package/template/nextjs/terraform/ip-address.tf +0 -11
  336. package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
  337. package/template/nextjs/terraform/network.tf +0 -21
  338. package/template/nextjs/terraform/pre.sh +0 -3
  339. package/template/nextjs/terraform/providers.tf +0 -18
  340. package/template/nextjs/terraform/secret.txt +0 -11
  341. package/template/nextjs/terraform/setup.sh +0 -25
  342. package/template/nextjs/terraform/ssl-certs.tf +0 -32
  343. package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
  344. package/template/nextjs/terraform/target-https-proxy.tf +0 -14
  345. package/template/nextjs/terraform/terraform.tfvars +0 -4
  346. package/template/nextjs/terraform/url-map.tf +0 -23
  347. package/template/nextjs/terraform/variables.tf +0 -21
  348. package/template/saas-starter/next-env.d.ts +0 -6
  349. package/template/saas-starter/src/actions/uploads.ts +0 -28
  350. package/template/saas-starter/src/actions/uploads.validation.ts +0 -14
  351. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +0 -183
  352. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +0 -129
  353. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +0 -415
  354. package/template/saas-starter/src/app/(app)/actions/organization.ts +0 -92
  355. package/template/saas-starter/src/app/proxy.ts +0 -28
  356. package/template/saas-starter/src/components/impersonation-banner.tsx +0 -44
  357. package/template/saas-starter/src/lib/organization-plan-limits.ts +0 -38
@@ -0,0 +1,108 @@
1
+ 'use client'
2
+
3
+ import { MessageCircle, Star } from 'lucide-react'
4
+ import { useAction } from 'next-safe-action/hooks'
5
+ import { useParams } from 'next/navigation'
6
+ import { useState } from 'react'
7
+ import { Button } from '~/components/ui/button'
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogFooter,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ DialogTrigger
16
+ } from '~/components/ui/dialog'
17
+ import { Textarea } from '~/components/ui/textarea'
18
+ import { toastActionError, toastSuccessMessage } from '~/lib/toast-message'
19
+ import { createFeedbackAction } from '../../actions/feedbacks'
20
+
21
+ export function FeedbackDialog() {
22
+ const params = useParams()
23
+ const organizationId =
24
+ typeof params?.orgId === 'string' ? params.orgId : undefined
25
+ const [open, setOpen] = useState(false)
26
+ const [rating, setRating] = useState(5)
27
+ const [comment, setComment] = useState('')
28
+
29
+ const { execute, isExecuting } = useAction(createFeedbackAction, {
30
+ onSuccess: () => {
31
+ toastSuccessMessage('Thanks for the feedback')
32
+ setOpen(false)
33
+ setRating(5)
34
+ setComment('')
35
+ },
36
+ onError: ({ error }) => {
37
+ toastActionError(error, 'Your feedback was not sent. Try again.')
38
+ }
39
+ })
40
+
41
+ return (
42
+ <Dialog open={open} onOpenChange={setOpen}>
43
+ <DialogTrigger asChild>
44
+ <Button
45
+ variant="outline"
46
+ size="sm"
47
+ className={organizationId ? 'inline-flex' : 'hidden'}
48
+ aria-label="Share feedback"
49
+ >
50
+ <MessageCircle className="size-4" aria-hidden="true" />
51
+ <span className="hidden sm:inline">Feedback</span>
52
+ </Button>
53
+ </DialogTrigger>
54
+ <DialogContent>
55
+ <DialogHeader>
56
+ <DialogTitle>Share Feedback</DialogTitle>
57
+ <DialogDescription>
58
+ Tell us what works well and what should improve.
59
+ </DialogDescription>
60
+ </DialogHeader>
61
+
62
+ <div className="space-y-4">
63
+ <div className="flex gap-1">
64
+ {Array.from({ length: 5 }, (_, index) => {
65
+ const value = index + 1
66
+ return (
67
+ <Button
68
+ key={value}
69
+ type="button"
70
+ variant="ghost"
71
+ size="icon"
72
+ onClick={() => setRating(value)}
73
+ aria-label={`${value} star rating`}
74
+ >
75
+ <Star
76
+ className={`h-5 w-5 ${
77
+ value <= rating
78
+ ? 'fill-warning text-warning'
79
+ : 'text-muted-foreground'
80
+ }`}
81
+ />
82
+ </Button>
83
+ )
84
+ })}
85
+ </div>
86
+
87
+ <Textarea
88
+ value={comment}
89
+ onChange={event => setComment(event.target.value)}
90
+ placeholder="Write your feedback…"
91
+ className="min-h-32"
92
+ />
93
+ </div>
94
+
95
+ <DialogFooter>
96
+ <Button
97
+ onClick={() =>
98
+ organizationId && execute({ organizationId, rating, comment })
99
+ }
100
+ disabled={isExecuting || !comment.trim()}
101
+ >
102
+ Submit Feedback
103
+ </Button>
104
+ </DialogFooter>
105
+ </DialogContent>
106
+ </Dialog>
107
+ )
108
+ }
@@ -7,16 +7,11 @@ type OrganizationContextType = {
7
7
  name: string
8
8
  role: string
9
9
  memberId: string
10
- userId: string
11
10
  }
12
11
 
13
- const OrganizationContext = createContext<OrganizationContextType>({
14
- id: '',
15
- name: '',
16
- role: 'member',
17
- memberId: '',
18
- userId: ''
19
- })
12
+ // No default: a component rendered outside the provider is a bug, and the
13
+ // hook throws rather than passing off an empty organization as real.
14
+ const OrganizationContext = createContext<OrganizationContextType | null>(null)
20
15
 
21
16
  export const OrganizationContextProvider = ({
22
17
  children,
@@ -28,12 +23,13 @@ export const OrganizationContextProvider = ({
28
23
  return <OrganizationContext value={value}>{children}</OrganizationContext>
29
24
  }
30
25
 
26
+ /** The organization in the current `/app/[orgId]` route. */
31
27
  export const useOrganizationContext = () => {
32
28
  const context = useContext(OrganizationContext)
33
29
 
34
30
  if (!context) {
35
31
  throw new Error(
36
- 'useOrganizationContext must be used within a OrganizationContextProvider'
32
+ 'useOrganizationContext must be used within an OrganizationContextProvider'
37
33
  )
38
34
  }
39
35
 
@@ -1,9 +1,10 @@
1
1
  'use client'
2
2
 
3
- import { Building2, ChevronsUpDown, PlusCircle } from 'lucide-react'
3
+ import { Building2, ChevronsUpDown, LogOut, PlusCircle } from 'lucide-react'
4
4
  import Image from 'next/image'
5
5
  import { useParams, useRouter } from 'next/navigation'
6
- import { useMemo } from 'react'
6
+ import { useMemo, useState } from 'react'
7
+ import { ConfirmDialog } from '~/components/confirm-dialog'
7
8
  import {
8
9
  DropdownMenu,
9
10
  DropdownMenuContent,
@@ -17,7 +18,15 @@ import {
17
18
  SidebarMenuButton,
18
19
  SidebarMenuItem
19
20
  } from '~/components/ui/sidebar'
20
- import type { UserOrganization } from '~/lib/app-navigation'
21
+ import { organization } from '~/lib/auth-client'
22
+ import { toastActionError, toastSuccessMessage } from '~/lib/toast-message'
23
+
24
+ export type SwitcherOrganization = {
25
+ id: string
26
+ name: string
27
+ role: string
28
+ logoUrl: string | null
29
+ }
21
30
 
22
31
  function OrganizationLogo({
23
32
  logo,
@@ -61,7 +70,7 @@ function OrganizationLogo({
61
70
  export function OrganizationSwitcher({
62
71
  organizations
63
72
  }: {
64
- organizations: UserOrganization[]
73
+ organizations: SwitcherOrganization[]
65
74
  }) {
66
75
  const router = useRouter()
67
76
  const params = useParams()
@@ -72,6 +81,31 @@ export function OrganizationSwitcher({
72
81
  [organizations, activeOrgId]
73
82
  )
74
83
 
84
+ // Managers leave from Settings; members cannot open Settings, so they get
85
+ // the option here.
86
+ const canLeaveHere = activeOrgId !== undefined && activeOrg?.role === 'member'
87
+ const [confirmLeave, setConfirmLeave] = useState(false)
88
+ const [isLeaving, setIsLeaving] = useState(false)
89
+
90
+ async function leaveOrganization() {
91
+ if (!activeOrg) return
92
+ setIsLeaving(true)
93
+ const { error } = await organization.leave({
94
+ organizationId: activeOrg.id
95
+ })
96
+ setIsLeaving(false)
97
+
98
+ if (error) {
99
+ toastActionError(error, 'You are still a member. Try again.')
100
+ return
101
+ }
102
+
103
+ setConfirmLeave(false)
104
+ toastSuccessMessage(`You left ${activeOrg.name}`)
105
+ router.push('/app')
106
+ router.refresh()
107
+ }
108
+
75
109
  return (
76
110
  <SidebarMenu>
77
111
  <SidebarMenuItem>
@@ -82,13 +116,13 @@ export function OrganizationSwitcher({
82
116
  className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
83
117
  >
84
118
  <OrganizationLogo
85
- logo={activeOrg?.logo ?? null}
119
+ logo={activeOrg?.logoUrl ?? null}
86
120
  name={activeOrg?.name ?? ''}
87
121
  size="lg"
88
122
  />
89
123
  <div className="grid flex-1 text-left text-sm leading-tight">
90
124
  <span className="truncate font-medium">
91
- {activeOrg?.name ?? 'Select organization'}
125
+ {activeOrg?.name ?? 'Select Organization'}
92
126
  </span>
93
127
  {activeOrg ? (
94
128
  <span className="text-muted-foreground truncate text-xs capitalize">
@@ -115,7 +149,11 @@ export function OrganizationSwitcher({
115
149
  className="gap-2 p-2"
116
150
  >
117
151
  <div className="flex size-6 items-center justify-center overflow-hidden rounded-sm border">
118
- <OrganizationLogo logo={org.logo} name={org.name} size="sm" />
152
+ <OrganizationLogo
153
+ logo={org.logoUrl}
154
+ name={org.name}
155
+ size="sm"
156
+ />
119
157
  </div>
120
158
  <span className="truncate">{org.name}</span>
121
159
  </DropdownMenuItem>
@@ -129,12 +167,34 @@ export function OrganizationSwitcher({
129
167
  <PlusCircle className="size-4" />
130
168
  </div>
131
169
  <div className="text-muted-foreground font-medium">
132
- Create organization
170
+ Create Organization
133
171
  </div>
134
172
  </DropdownMenuItem>
173
+ {canLeaveHere ? (
174
+ <DropdownMenuItem
175
+ variant="destructive"
176
+ className="gap-2 p-2"
177
+ onClick={() => setConfirmLeave(true)}
178
+ >
179
+ <div className="flex size-6 items-center justify-center rounded-md border">
180
+ <LogOut className="size-4" />
181
+ </div>
182
+ <div className="font-medium">Leave Organization</div>
183
+ </DropdownMenuItem>
184
+ ) : null}
135
185
  </DropdownMenuContent>
136
186
  </DropdownMenu>
137
187
  </SidebarMenuItem>
188
+
189
+ <ConfirmDialog
190
+ open={confirmLeave}
191
+ onOpenChange={setConfirmLeave}
192
+ title="Leave Organization"
193
+ description={`You lose access to ${activeOrg?.name ?? 'this organization'}. An owner or admin must invite you to return.`}
194
+ confirmLabel="Leave Organization"
195
+ pending={isLeaving}
196
+ onConfirm={leaveOrganization}
197
+ />
138
198
  </SidebarMenu>
139
199
  )
140
200
  }
@@ -1,52 +1,64 @@
1
- import { cookies } from 'next/headers'
1
+ import type { Metadata } from 'next'
2
+ import { cookies, headers } from 'next/headers'
2
3
  import { redirect } from 'next/navigation'
3
- import { NuqsAdapter } from 'nuqs/adapters/react'
4
- import { ImpersonationBanner } from '~/components/impersonation-banner'
4
+ import { NuqsAdapter } from 'nuqs/adapters/next/app'
5
+ import { ErrorDisplay } from '~/components/error'
5
6
  import { SidebarInset, SidebarProvider } from '~/components/ui/sidebar'
6
- import { TooltipProvider } from '~/components/ui/tooltip'
7
7
  import { getAuthSession } from '~/lib/auth'
8
- import { getUserOrganizationsCached } from '~/lib/organization-access'
8
+ import { sanitizeCallbackUrl } from '~/lib/callback-url'
9
+ import { privatePageRobots } from '~/lib/seo'
10
+ import { getOrganizationsCached } from '../actions/organizations'
9
11
  import { AppHeader } from './_components/app-header'
10
12
  import { AppSidebar } from './_components/app-sidebar'
11
13
 
12
- export default async function Layout({
14
+ export const metadata: Metadata = {
15
+ robots: privatePageRobots
16
+ }
17
+
18
+ export default async function AppLayout({
13
19
  children
14
- }: Readonly<{
20
+ }: {
15
21
  children: React.ReactNode
16
- }>) {
22
+ }) {
17
23
  const authSession = await getAuthSession()
18
24
 
19
25
  if (!authSession) {
20
- return redirect('/login')
26
+ const headerStore = await headers()
27
+ const callbackUrl =
28
+ sanitizeCallbackUrl(headerStore.get('x-pathname') ?? undefined) ?? '/app'
29
+ redirect(`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`)
30
+ }
31
+
32
+ const organizations = await getOrganizationsCached()
33
+
34
+ if (!organizations?.data) {
35
+ return (
36
+ <ErrorDisplay
37
+ title="Organizations Failed to Load"
38
+ message="Refresh the page to try again."
39
+ link={{ href: '/app', text: 'Retry' }}
40
+ />
41
+ )
21
42
  }
22
43
 
23
44
  const cookieStore = await cookies()
24
45
  const sidebarStateCookie = cookieStore.get('sidebar_state')?.value
25
46
  const defaultOpen = sidebarStateCookie !== 'false'
26
47
 
27
- const organizations = await getUserOrganizationsCached()
28
-
29
48
  return (
30
- <TooltipProvider>
31
- <NuqsAdapter>
32
- <SidebarProvider defaultOpen={defaultOpen}>
33
- <AppSidebar organizations={organizations} />
34
- <SidebarInset>
35
- {authSession.session.impersonatedBy ? (
36
- <ImpersonationBanner />
37
- ) : null}
38
- <AppHeader
39
- user={{
40
- name: authSession.user.name,
41
- email: authSession.user.email,
42
- image: authSession.user.image
43
- }}
44
- organizations={organizations}
45
- />
46
- <div className="flex flex-1 flex-col gap-4 p-4">{children}</div>
47
- </SidebarInset>
48
- </SidebarProvider>
49
- </NuqsAdapter>
50
- </TooltipProvider>
49
+ <NuqsAdapter>
50
+ <SidebarProvider defaultOpen={defaultOpen}>
51
+ <AppSidebar organizations={organizations.data} />
52
+ <SidebarInset>
53
+ <AppHeader
54
+ user={authSession.user}
55
+ organizations={organizations.data}
56
+ isSuperAdmin={authSession.isSuperAdmin}
57
+ isImpersonating={!!authSession.session.impersonatedBy}
58
+ />
59
+ <div className="flex flex-1 flex-col gap-4 p-4">{children}</div>
60
+ </SidebarInset>
61
+ </SidebarProvider>
62
+ </NuqsAdapter>
51
63
  )
52
64
  }
@@ -2,126 +2,114 @@
2
2
 
3
3
  import { zodResolver } from '@hookform/resolvers/zod'
4
4
  import { Loader2 } from 'lucide-react'
5
+ import { useRouter } from 'next/navigation'
5
6
  import { useState } from 'react'
6
- import { useForm } from 'react-hook-form'
7
+ import { Controller, useForm } from 'react-hook-form'
7
8
  import { z } from 'zod'
9
+ import { organizationDetailsSchema } from '~/app/(app)/actions/organizations.validation'
8
10
  import { Button } from '~/components/ui/button'
9
11
  import {
10
12
  Field,
11
- FieldContent,
12
13
  FieldDescription,
13
14
  FieldError,
14
- FieldTitle
15
+ FieldLabel
15
16
  } from '~/components/ui/field'
16
17
  import { Input } from '~/components/ui/input'
17
18
  import { organization } from '~/lib/auth-client'
18
- import { toastErrorMessage, toastSuccessMessage } from '~/lib/toast-message'
19
- import { stringValidation } from '~/lib/validations'
19
+ import { toastActionError, toastSuccessMessage } from '~/lib/toast-message'
20
+ import { slugifyName } from '~/lib/validations'
20
21
 
21
- const slugify = (value: string) =>
22
- value
23
- .toLowerCase()
24
- .trim()
25
- .replace(/[^a-z0-9]+/g, '-')
26
- .replace(/^-+|-+$/g, '')
27
-
28
- const schema = z.object({
29
- name: stringValidation,
30
- slug: z
31
- .string()
32
- .trim()
33
- .min(1, 'Slug is required')
34
- .max(255)
35
- .regex(
36
- /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
37
- 'Use lowercase letters, numbers and hyphens only'
38
- )
39
- })
40
-
41
- type Form = z.infer<typeof schema>
22
+ type Form = z.infer<typeof organizationDetailsSchema>
42
23
 
43
24
  export function CreateOrganizationForm() {
44
- const [submitting, setSubmitting] = useState(false)
25
+ const router = useRouter()
26
+ const [slugEdited, setSlugEdited] = useState(false)
45
27
 
46
- const {
47
- register,
48
- handleSubmit,
49
- setValue,
50
- formState: { errors }
51
- } = useForm<Form>({
52
- resolver: zodResolver(schema),
28
+ const form = useForm<Form>({
29
+ resolver: zodResolver(organizationDetailsSchema),
53
30
  defaultValues: { name: '', slug: '' }
54
31
  })
55
32
 
56
- const [slugEdited, setSlugEdited] = useState(false)
57
-
58
33
  async function onSubmit(values: Form) {
59
- setSubmitting(true)
60
-
61
- const { data, error } = await organization.create({
62
- name: values.name,
63
- slug: values.slug
64
- })
34
+ const { data, error } = await organization.create(values)
65
35
 
66
36
  if (error || !data) {
67
- toastErrorMessage(error?.message ?? 'Failed to create organization')
68
- setSubmitting(false)
37
+ toastActionError(error, 'The organization was not created. Try again.')
69
38
  return
70
39
  }
71
40
 
72
41
  toastSuccessMessage(`${data.name} created`)
73
- window.location.assign(`/app/${data.id}/dashboard`)
42
+ router.push(`/app/${data.id}/dashboard`)
43
+ // Re-render the app layout so the sidebar lists the new organization.
44
+ router.refresh()
74
45
  }
75
46
 
47
+ const isSubmitting = form.formState.isSubmitting
48
+
76
49
  return (
77
- <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-6">
78
- <Field orientation="vertical">
79
- <FieldContent>
80
- <FieldTitle className="text-foreground mb-1.5 text-[13px] font-semibold">
81
- Organization name
82
- </FieldTitle>
83
- <Input
84
- autoFocus
85
- placeholder="Acme Inc."
86
- {...register('name', {
87
- onChange: e => {
50
+ <form
51
+ onSubmit={form.handleSubmit(onSubmit)}
52
+ className="flex flex-col gap-6"
53
+ >
54
+ <Controller
55
+ control={form.control}
56
+ name="name"
57
+ render={({ field, fieldState }) => (
58
+ <Field data-invalid={fieldState.invalid}>
59
+ <FieldLabel htmlFor={field.name}>Organization name</FieldLabel>
60
+ <Input
61
+ id={field.name}
62
+ autoFocus
63
+ placeholder="Acme Inc."
64
+ aria-invalid={fieldState.invalid}
65
+ {...field}
66
+ onChange={event => {
67
+ field.onChange(event)
88
68
  if (!slugEdited) {
89
- setValue('slug', slugify(e.target.value), {
90
- shouldValidate: true
69
+ form.setValue('slug', slugifyName(event.target.value), {
70
+ shouldValidate: form.formState.isSubmitted
91
71
  })
92
72
  }
93
- }
94
- })}
95
- aria-invalid={!!errors.name}
96
- />
97
- <FieldError errors={[errors.name]} className="mt-1.5 text-[13px]" />
98
- </FieldContent>
99
- </Field>
73
+ }}
74
+ />
75
+ <FieldError
76
+ errors={fieldState.error ? [fieldState.error] : undefined}
77
+ />
78
+ </Field>
79
+ )}
80
+ />
100
81
 
101
- <Field orientation="vertical">
102
- <FieldContent>
103
- <FieldTitle className="text-foreground mb-1.5 text-[13px] font-semibold">
104
- Slug
105
- </FieldTitle>
106
- <Input
107
- placeholder="acme-inc"
108
- {...register('slug', {
109
- onChange: () => setSlugEdited(true)
110
- })}
111
- aria-invalid={!!errors.slug}
112
- />
113
- <FieldDescription className="mt-1.5 text-[13px]">
114
- A unique identifier used in links. Lowercase letters, numbers and
115
- hyphens.
116
- </FieldDescription>
117
- <FieldError errors={[errors.slug]} className="mt-1.5 text-[13px]" />
118
- </FieldContent>
119
- </Field>
82
+ <Controller
83
+ control={form.control}
84
+ name="slug"
85
+ render={({ field, fieldState }) => (
86
+ <Field data-invalid={fieldState.invalid}>
87
+ <FieldLabel htmlFor={field.name}>Slug</FieldLabel>
88
+ <Input
89
+ id={field.name}
90
+ placeholder="acme-inc"
91
+ aria-invalid={fieldState.invalid}
92
+ {...field}
93
+ onChange={event => {
94
+ setSlugEdited(true)
95
+ field.onChange(event)
96
+ }}
97
+ />
98
+ <FieldDescription>
99
+ Identifies the organization in links. Use lowercase letters,
100
+ numbers, and hyphens.
101
+ </FieldDescription>
102
+ <FieldError
103
+ errors={fieldState.error ? [fieldState.error] : undefined}
104
+ />
105
+ </Field>
106
+ )}
107
+ />
120
108
 
121
109
  <div>
122
- <Button type="submit" disabled={submitting}>
123
- {submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
124
- Create organization
110
+ <Button type="submit" disabled={isSubmitting}>
111
+ {isSubmitting && <Loader2 className="animate-spin" />}
112
+ Create Organization
125
113
  </Button>
126
114
  </div>
127
115
  </form>
@@ -1,17 +1,17 @@
1
1
  import { ArrowLeft } from 'lucide-react'
2
2
  import Link from 'next/link'
3
3
  import { Button } from '~/components/ui/button'
4
- import { getUserOrganizationsCached } from '~/lib/organization-access'
4
+ import { getOrganizationsCached } from '../../actions/organizations'
5
5
  import { CreateOrganizationForm } from './_components/create-organization-form'
6
6
 
7
- export default async function Page() {
8
- const organizations = await getUserOrganizationsCached()
7
+ export default async function OnboardingPage() {
8
+ const organizations = (await getOrganizationsCached())?.data ?? []
9
9
  const isFirstOrg = organizations.length === 0
10
10
 
11
11
  return (
12
12
  <div className="mx-auto flex w-full max-w-xl flex-col gap-8 px-2 py-8">
13
13
  <div className="flex flex-col gap-2">
14
- {!isFirstOrg ? (
14
+ {isFirstOrg ? null : (
15
15
  <Button
16
16
  asChild
17
17
  variant="ghost"
@@ -19,20 +19,18 @@ export default async function Page() {
19
19
  className="text-muted-foreground -ml-2 w-fit"
20
20
  >
21
21
  <Link href="/app">
22
- <ArrowLeft className="mr-2 size-4" />
23
- Back to organizations
22
+ <ArrowLeft />
23
+ Back to Organizations
24
24
  </Link>
25
25
  </Button>
26
- ) : null}
26
+ )}
27
27
  <h1 className="text-3xl font-bold tracking-tight">
28
- {isFirstOrg
29
- ? 'Create your organization'
30
- : 'Create a new organization'}
28
+ {isFirstOrg ? 'Create Your Organization' : 'Create an Organization'}
31
29
  </h1>
32
30
  <p className="text-muted-foreground">
33
31
  {isFirstOrg
34
- ? 'Set up an organization to get started. You can invite teammates afterwards.'
35
- : 'Spin up another workspace. You can switch between organizations at any time.'}
32
+ ? 'Set up an organization to get started. You can invite teammates next.'
33
+ : 'Each organization has its own members, settings, and billing.'}
36
34
  </p>
37
35
  </div>
38
36