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,24 @@
1
+ import { notFound } from 'next/navigation'
2
+ import { getOrganizationCached } from '~/app/(app)/actions/organizations'
3
+ import { isManagerRole } from '~/lib/rbac'
4
+
5
+ /**
6
+ * Settings are for owners and admins. Every action behind these pages checks
7
+ * the role again; this guard only keeps members off pages they cannot use.
8
+ */
9
+ export default async function SettingsLayout({
10
+ children,
11
+ params
12
+ }: {
13
+ children: React.ReactNode
14
+ params: Promise<{ orgId: string }>
15
+ }) {
16
+ const { orgId } = await params
17
+ const org = (await getOrganizationCached(orgId))?.data
18
+
19
+ if (!org || !isManagerRole(org.role)) {
20
+ return notFound()
21
+ }
22
+
23
+ return <>{children}</>
24
+ }
@@ -30,13 +30,14 @@ import {
30
30
  SelectValue
31
31
  } from '~/components/ui/select'
32
32
  import { organization } from '~/lib/auth-client'
33
- import { toastErrorMessage, toastSuccessMessage } from '~/lib/toast-message'
33
+ import { ASSIGNABLE_ROLES, ROLE_DESCRIPTIONS } from '~/lib/rbac'
34
+ import { toastActionError, toastSuccessMessage } from '~/lib/toast-message'
34
35
  import { emailValidation } from '~/lib/validations'
35
36
  import { useOrganizationContext } from '../../../../_components/organization-context'
36
37
 
37
38
  const schema = z.object({
38
39
  email: emailValidation,
39
- role: z.enum(['admin', 'member'])
40
+ role: z.enum(ASSIGNABLE_ROLES)
40
41
  })
41
42
 
42
43
  type Form = z.infer<typeof schema>
@@ -62,6 +63,7 @@ export function InviteMemberDialog({
62
63
  defaultValues: { email: '', role: 'member' }
63
64
  })
64
65
 
66
+ // Seed once per open; see "Client data and forms" in AGENTS.md.
65
67
  useEffect(() => {
66
68
  if (open) reset({ email: '', role: 'member' })
67
69
  }, [open, reset])
@@ -74,7 +76,7 @@ export function InviteMemberDialog({
74
76
  })
75
77
 
76
78
  if (error) {
77
- toastErrorMessage(error.message ?? 'Failed to send invitation')
79
+ toastActionError(error, 'The invitation was not sent. Try again.')
78
80
  return
79
81
  }
80
82
 
@@ -88,10 +90,10 @@ export function InviteMemberDialog({
88
90
  <DialogTrigger asChild>{children}</DialogTrigger>
89
91
  <DialogContent className="sm:max-w-md">
90
92
  <DialogHeader>
91
- <DialogTitle>Invite a teammate</DialogTitle>
93
+ <DialogTitle>Invite Member</DialogTitle>
92
94
  <DialogDescription>
93
- Send an email invitation to join this organization. They&apos;ll
94
- need to sign in with this email to accept.
95
+ We email an invitation link. They accept it by signing in with this
96
+ address.
95
97
  </DialogDescription>
96
98
  </DialogHeader>
97
99
 
@@ -129,12 +131,11 @@ export function InviteMemberDialog({
129
131
  <SelectValue placeholder="Select role" />
130
132
  </SelectTrigger>
131
133
  <SelectContent>
132
- <SelectItem value="admin">
133
- Admin - manage members & settings
134
- </SelectItem>
135
- <SelectItem value="member">
136
- Member - standard access
137
- </SelectItem>
134
+ {ASSIGNABLE_ROLES.map(role => (
135
+ <SelectItem key={role} value={role}>
136
+ {ROLE_DESCRIPTIONS[role]}
137
+ </SelectItem>
138
+ ))}
138
139
  </SelectContent>
139
140
  </Select>
140
141
  )}
@@ -156,10 +157,8 @@ export function InviteMemberDialog({
156
157
  Cancel
157
158
  </Button>
158
159
  <Button type="submit" disabled={isSubmitting}>
159
- {isSubmitting && (
160
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
161
- )}
162
- Send invitation
160
+ {isSubmitting && <Loader2 className="animate-spin" />}
161
+ Send Invitation
163
162
  </Button>
164
163
  </DialogFooter>
165
164
  </form>
@@ -1,7 +1,6 @@
1
1
  'use client'
2
2
 
3
3
  import { useQueryClient } from '@tanstack/react-query'
4
- import { ColumnDef } from '@tanstack/react-table'
5
4
  import {
6
5
  MoreHorizontal,
7
6
  RefreshCcw,
@@ -17,7 +16,8 @@ import {
17
16
  useQueryStates
18
17
  } from 'nuqs'
19
18
  import { useState } from 'react'
20
- import { DataTable } from '~/components/data-table'
19
+ import { ConfirmDialog } from '~/components/confirm-dialog'
20
+ import { DataTable, type DataTableColumnDef } from '~/components/data-table'
21
21
  import { SortOrderEnum } from '~/components/data-table/enum'
22
22
  import { DataTableFilter } from '~/components/data-table/types'
23
23
  import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'
@@ -38,8 +38,9 @@ import {
38
38
  } from '~/components/ui/tooltip'
39
39
  import { organization } from '~/lib/auth-client'
40
40
  import { formatDate } from '~/lib/format-date'
41
+ import { roleArticle, type AssignableRole } from '~/lib/rbac'
41
42
  import { useSafeActionQuery } from '~/lib/safe-action-client'
42
- import { toastErrorMessage, toastSuccessMessage } from '~/lib/toast-message'
43
+ import { toastActionError, toastSuccessMessage } from '~/lib/toast-message'
43
44
  import { getInvitations, getMembers } from '../../../../../actions/members'
44
45
  import { useOrganizationContext } from '../../../../_components/organization-context'
45
46
  import { InviteMemberDialog } from './invite-member-dialog'
@@ -67,6 +68,10 @@ type Invitation = {
67
68
  const MEMBERS_QUERY_KEY = 'org-members'
68
69
  const INVITATIONS_QUERY_KEY = 'org-invitations'
69
70
 
71
+ type PendingConfirm =
72
+ | { kind: 'remove'; member: Member }
73
+ | { kind: 'cancel'; invitation: Invitation }
74
+
70
75
  function roleVariant(role: string): 'default' | 'secondary' | 'outline' {
71
76
  if (role === 'owner') return 'default'
72
77
  if (role === 'admin') return 'secondary'
@@ -75,10 +80,11 @@ function roleVariant(role: string): 'default' | 'secondary' | 'outline' {
75
80
 
76
81
  function statusVariant(
77
82
  status: string
78
- ): 'default' | 'secondary' | 'outline' | 'destructive' {
79
- if (status === 'accepted') return 'default'
80
- if (status === 'pending') return 'secondary'
81
- return 'destructive'
83
+ ): 'success' | 'info' | 'secondary' | 'destructive' {
84
+ if (status === 'accepted') return 'success'
85
+ if (status === 'pending') return 'info'
86
+ if (status === 'rejected') return 'destructive'
87
+ return 'secondary'
82
88
  }
83
89
 
84
90
  function getInitials(name: string, email: string) {
@@ -129,6 +135,9 @@ export function MembersTabs({
129
135
  const router = useRouter()
130
136
  const queryClient = useQueryClient()
131
137
  const [busyId, setBusyId] = useState<string | null>(null)
138
+ const [pendingConfirm, setPendingConfirm] = useState<PendingConfirm | null>(
139
+ null
140
+ )
132
141
 
133
142
  const atMemberLimit = memberCount >= maxMembers
134
143
 
@@ -154,14 +163,9 @@ export function MembersTabs({
154
163
  page: memberQuery.mPage,
155
164
  limit: memberQuery.mLimit,
156
165
  sortBy: (memberQuery.mSortBy ?? undefined) as
157
- | 'name'
158
- | 'email'
159
- | 'role'
160
- | 'createdAt'
161
- | undefined,
166
+ 'name' | 'email' | 'role' | 'createdAt' | undefined,
162
167
  sortOrder: (memberQuery.mSortOrder ?? undefined) as
163
- | SortOrderEnum
164
- | undefined,
168
+ SortOrderEnum | undefined,
165
169
  search: memberQuery.mSearch ?? undefined,
166
170
  filters: memberQuery.mFilters ?? undefined
167
171
  }
@@ -172,7 +176,7 @@ export function MembersTabs({
172
176
  memberParams
173
177
  )
174
178
 
175
- async function changeRole(member: Member, role: 'admin' | 'member') {
179
+ async function changeRole(member: Member, role: AssignableRole) {
176
180
  setBusyId(member.id)
177
181
  const { error } = await organization.updateMemberRole({
178
182
  organizationId: orgId,
@@ -181,15 +185,14 @@ export function MembersTabs({
181
185
  })
182
186
  setBusyId(null)
183
187
  if (error) {
184
- toastErrorMessage(error.message ?? 'Failed to update role')
188
+ toastActionError(error, 'The role was not changed. Try again.')
185
189
  return
186
190
  }
187
- toastSuccessMessage(`${member.name} is now ${role}`)
191
+ toastSuccessMessage(`${member.name} is now ${roleArticle(role)}`)
188
192
  invalidateMembers()
189
193
  }
190
194
 
191
195
  async function removeMember(member: Member) {
192
- if (!confirm(`Remove ${member.name} from this organization?`)) return
193
196
  setBusyId(member.id)
194
197
  const { error } = await organization.removeMember({
195
198
  organizationId: orgId,
@@ -197,16 +200,17 @@ export function MembersTabs({
197
200
  })
198
201
  setBusyId(null)
199
202
  if (error) {
200
- toastErrorMessage(error.message ?? 'Failed to remove member')
203
+ toastActionError(error, 'The member was not removed. Try again.')
201
204
  return
202
205
  }
206
+ setPendingConfirm(null)
203
207
  toastSuccessMessage(`${member.name} removed`)
204
208
  invalidateMembers()
205
209
  // Refresh the server component so the member-limit gate reflects the freed slot.
206
210
  router.refresh()
207
211
  }
208
212
 
209
- const memberColumns: ColumnDef<Member, unknown>[] = [
213
+ const memberColumns: DataTableColumnDef<Member>[] = [
210
214
  {
211
215
  accessorKey: 'name',
212
216
  header: 'Member',
@@ -285,18 +289,18 @@ export function MembersTabs({
285
289
  <DropdownMenuContent align="end">
286
290
  {member.role !== 'admin' ? (
287
291
  <DropdownMenuItem onClick={() => changeRole(member, 'admin')}>
288
- Make admin
292
+ Make Admin
289
293
  </DropdownMenuItem>
290
294
  ) : null}
291
295
  {member.role !== 'member' ? (
292
296
  <DropdownMenuItem onClick={() => changeRole(member, 'member')}>
293
- Make member
297
+ Make Member
294
298
  </DropdownMenuItem>
295
299
  ) : null}
296
300
  <DropdownMenuSeparator />
297
301
  <DropdownMenuItem
298
302
  variant="destructive"
299
- onClick={() => removeMember(member)}
303
+ onClick={() => setPendingConfirm({ kind: 'remove', member })}
300
304
  >
301
305
  <Trash2 className="mr-2 h-4 w-4" />
302
306
  Remove
@@ -325,14 +329,9 @@ export function MembersTabs({
325
329
  page: inviteQuery.iPage,
326
330
  limit: inviteQuery.iLimit,
327
331
  sortBy: (inviteQuery.iSortBy ?? undefined) as
328
- | 'email'
329
- | 'status'
330
- | 'createdAt'
331
- | 'expiresAt'
332
- | undefined,
332
+ 'email' | 'status' | 'createdAt' | 'expiresAt' | undefined,
333
333
  sortOrder: (inviteQuery.iSortOrder ?? undefined) as
334
- | SortOrderEnum
335
- | undefined,
334
+ SortOrderEnum | undefined,
336
335
  search: inviteQuery.iSearch ?? undefined,
337
336
  filters: inviteQuery.iFilters ?? undefined
338
337
  }
@@ -353,7 +352,7 @@ export function MembersTabs({
353
352
  })
354
353
  setBusyId(null)
355
354
  if (error) {
356
- toastErrorMessage(error.message ?? 'Failed to resend invitation')
355
+ toastActionError(error, 'The invitation was not resent. Try again.')
357
356
  return
358
357
  }
359
358
  toastSuccessMessage(`Invitation resent to ${invitation.email}`)
@@ -361,21 +360,21 @@ export function MembersTabs({
361
360
  }
362
361
 
363
362
  async function cancelInvitation(invitation: Invitation) {
364
- if (!confirm(`Cancel the invitation for ${invitation.email}?`)) return
365
363
  setBusyId(invitation.id)
366
364
  const { error } = await organization.cancelInvitation({
367
365
  invitationId: invitation.id
368
366
  })
369
367
  setBusyId(null)
370
368
  if (error) {
371
- toastErrorMessage(error.message ?? 'Failed to cancel invitation')
369
+ toastActionError(error, 'The invitation was not canceled. Try again.')
372
370
  return
373
371
  }
374
- toastSuccessMessage('Invitation cancelled')
372
+ setPendingConfirm(null)
373
+ toastSuccessMessage('Invitation canceled')
375
374
  invalidateInvitations()
376
375
  }
377
376
 
378
- const invitationColumns: ColumnDef<Invitation, unknown>[] = [
377
+ const invitationColumns: DataTableColumnDef<Invitation>[] = [
379
378
  {
380
379
  accessorKey: 'email',
381
380
  header: 'Email',
@@ -452,7 +451,9 @@ export function MembersTabs({
452
451
  <DropdownMenuSeparator />
453
452
  <DropdownMenuItem
454
453
  variant="destructive"
455
- onClick={() => cancelInvitation(invitation)}
454
+ onClick={() =>
455
+ setPendingConfirm({ kind: 'cancel', invitation })
456
+ }
456
457
  >
457
458
  <XCircle className="mr-2 h-4 w-4" />
458
459
  Cancel
@@ -477,21 +478,21 @@ export function MembersTabs({
477
478
  <TooltipTrigger asChild>
478
479
  <span tabIndex={0}>
479
480
  <Button size="sm" disabled>
480
- <UserPlus className="mr-2 size-4" />
481
- Invite member
481
+ <UserPlus />
482
+ Invite Member
482
483
  </Button>
483
484
  </span>
484
485
  </TooltipTrigger>
485
486
  <TooltipContent>
486
- Member limit reached ({memberCount}/{maxMembers}). Upgrade your
487
- plan to invite more members.
487
+ Your plan allows {maxMembers} members and you have {memberCount}.
488
+ Upgrade your plan to invite more.
488
489
  </TooltipContent>
489
490
  </Tooltip>
490
491
  ) : (
491
492
  <InviteMemberDialog onInvited={invalidateInvitations}>
492
493
  <Button size="sm">
493
- <UserPlus className="mr-2 size-4" />
494
- Invite member
494
+ <UserPlus />
495
+ Invite Member
495
496
  </Button>
496
497
  </InviteMemberDialog>
497
498
  )}
@@ -505,7 +506,7 @@ export function MembersTabs({
505
506
  totalCount={memberData?.[1]}
506
507
  enableSearch
507
508
  initialSearch={memberParams.search}
508
- searchPlaceholder="Search members..."
509
+ searchPlaceholder="Search members…"
509
510
  manualPagination
510
511
  manualSorting
511
512
  pageIndex={memberParams.page - 1}
@@ -540,7 +541,7 @@ export function MembersTabs({
540
541
  totalCount={inviteData?.[1]}
541
542
  enableSearch
542
543
  initialSearch={inviteParams.search}
543
- searchPlaceholder="Search invitations..."
544
+ searchPlaceholder="Search invitations…"
544
545
  manualPagination
545
546
  manualSorting
546
547
  pageIndex={inviteParams.page - 1}
@@ -566,6 +567,38 @@ export function MembersTabs({
566
567
  }
567
568
  />
568
569
  </TabsContent>
570
+
571
+ <ConfirmDialog
572
+ open={pendingConfirm !== null}
573
+ onOpenChange={open => {
574
+ if (!open) setPendingConfirm(null)
575
+ }}
576
+ title={
577
+ pendingConfirm?.kind === 'remove'
578
+ ? 'Remove Member'
579
+ : 'Cancel Invitation'
580
+ }
581
+ description={
582
+ pendingConfirm?.kind === 'remove'
583
+ ? `${pendingConfirm.member.name} loses access to this organization.`
584
+ : pendingConfirm?.kind === 'cancel'
585
+ ? `The invitation link sent to ${pendingConfirm.invitation.email} stops working.`
586
+ : ''
587
+ }
588
+ confirmLabel={
589
+ pendingConfirm?.kind === 'remove'
590
+ ? 'Remove Member'
591
+ : 'Cancel Invitation'
592
+ }
593
+ pending={busyId !== null}
594
+ onConfirm={() => {
595
+ if (pendingConfirm?.kind === 'remove') {
596
+ void removeMember(pendingConfirm.member)
597
+ } else if (pendingConfirm?.kind === 'cancel') {
598
+ void cancelInvitation(pendingConfirm.invitation)
599
+ }
600
+ }}
601
+ />
569
602
  </Tabs>
570
603
  )
571
604
  }
@@ -1,40 +1,33 @@
1
1
  import { Users } from 'lucide-react'
2
- import { redirect } from 'next/navigation'
2
+ import { notFound } from 'next/navigation'
3
+ import { getMemberUsage } from '~/app/(app)/actions/members'
3
4
  import { PageHeading } from '~/components/page-heading'
4
- import { canManageOrganization } from '~/lib/app-navigation'
5
- import {
6
- getOrganizationMemberCount,
7
- getUserMembershipCached
8
- } from '~/lib/organization-access'
9
- import { getOrganizationPlanCached } from '~/lib/organization-plan-limits'
10
5
  import { MembersTabs } from './_components/members-tabs'
11
6
 
12
- export default async function Page({
7
+ export default async function MembersPage({
13
8
  params
14
9
  }: {
15
10
  params: Promise<{ orgId: string }>
16
11
  }) {
17
12
  const { orgId } = await params
13
+ const usage = (await getMemberUsage(orgId))?.data
18
14
 
19
- const membership = await getUserMembershipCached(orgId)
20
- if (!membership || !canManageOrganization(membership.role)) {
21
- return redirect(`/app/${orgId}/dashboard`)
15
+ if (!usage) {
16
+ return notFound()
22
17
  }
23
18
 
24
- const [memberCount, { plan }] = await Promise.all([
25
- getOrganizationMemberCount(orgId),
26
- getOrganizationPlanCached(orgId)
27
- ])
28
-
29
19
  return (
30
20
  <div className="mx-auto w-full max-w-4xl space-y-6">
31
21
  <PageHeading
32
22
  title="Members"
33
- description="Invite teammates, manage roles and review pending invitations."
23
+ description="Invite teammates, change roles, and review invitations."
34
24
  icon={Users}
35
25
  />
36
26
 
37
- <MembersTabs memberCount={memberCount} maxMembers={plan.maxMembers} />
27
+ <MembersTabs
28
+ memberCount={usage.memberCount}
29
+ maxMembers={usage.maxMembers}
30
+ />
38
31
  </div>
39
32
  )
40
33
  }
@@ -1,50 +1,36 @@
1
1
  import { Settings } from 'lucide-react'
2
- import { redirect } from 'next/navigation'
2
+ import { notFound } from 'next/navigation'
3
+ import {
4
+ getOrganizationCached,
5
+ getTransferTargets
6
+ } from '~/app/(app)/actions/organizations'
3
7
  import { PageHeading } from '~/components/page-heading'
4
8
  import { Separator } from '~/components/ui/separator'
5
- import { canManageOrganization } from '~/lib/app-navigation'
6
- import {
7
- getOrganizationMembers,
8
- getUserMembershipCached,
9
- getUserOrganizationsCached
10
- } from '~/lib/organization-access'
11
- import { resolveImageUrl } from '~/lib/storage'
12
9
  import { DangerZone } from './_components/danger-zone'
13
10
  import { GeneralSettingsForm } from './_components/general-settings-form'
14
11
 
15
- export default async function Page({
12
+ export default async function GeneralSettingsPage({
16
13
  params
17
14
  }: {
18
15
  params: Promise<{ orgId: string }>
19
16
  }) {
20
17
  const { orgId } = await params
18
+ const org = (await getOrganizationCached(orgId))?.data
21
19
 
22
- const membership = await getUserMembershipCached(orgId)
23
- if (!membership || !canManageOrganization(membership.role)) {
24
- return redirect(`/app/${orgId}/dashboard`)
25
- }
26
-
27
- const org = (await getUserOrganizationsCached()).find(o => o.id === orgId)
28
20
  if (!org) {
29
- return redirect('/app')
21
+ return notFound()
30
22
  }
31
23
 
32
- const members = await getOrganizationMembers(orgId)
33
- const isOwner = membership.role === 'owner'
34
- const transferTargets = members
35
- .filter(m => m.id !== membership.id)
36
- .map(m => ({
37
- memberId: m.id,
38
- name: m.user.name,
39
- email: m.user.email,
40
- role: m.role
41
- }))
24
+ const isOwner = org.role === 'owner'
25
+ const transferTargets = isOwner
26
+ ? ((await getTransferTargets(orgId))?.data ?? [])
27
+ : []
42
28
 
43
29
  return (
44
30
  <div className="mx-auto w-full max-w-3xl space-y-8">
45
31
  <PageHeading
46
32
  title="General"
47
- description="Manage your organization's profile and settings."
33
+ description="Change your organization's name, link, and logo."
48
34
  icon={Settings}
49
35
  />
50
36
 
@@ -52,8 +38,8 @@ export default async function Page({
52
38
  orgId={orgId}
53
39
  defaultName={org.name}
54
40
  defaultSlug={org.slug}
55
- currentLogoUrl={resolveImageUrl(org.logo)}
56
41
  currentLogoKey={org.logo}
42
+ currentLogoUrl={org.logoUrl}
57
43
  />
58
44
 
59
45
  <Separator />
@@ -0,0 +1,25 @@
1
+ import { notFound } from 'next/navigation'
2
+ import { getOrganizationCached } from '~/app/(app)/actions/organizations'
3
+ import { SupportTicketConversation } from '~/components/support/support-ticket-conversation'
4
+ import { isManagerRole } from '~/lib/rbac'
5
+
6
+ export default async function OrganizationSupportTicketPage({
7
+ params
8
+ }: {
9
+ params: Promise<{ orgId: string; ticketId: string }>
10
+ }) {
11
+ const { orgId, ticketId } = await params
12
+ const org = (await getOrganizationCached(orgId))?.data
13
+
14
+ if (!org || !isManagerRole(org.role)) {
15
+ return notFound()
16
+ }
17
+
18
+ return (
19
+ <SupportTicketConversation
20
+ mode="app"
21
+ ticketId={ticketId}
22
+ backHref={`/app/${orgId}/support-tickets`}
23
+ />
24
+ )
25
+ }
@@ -0,0 +1,25 @@
1
+ import { notFound } from 'next/navigation'
2
+ import { getOrganizationCached } from '~/app/(app)/actions/organizations'
3
+ import { SupportTicketsTable } from '~/components/support/support-tickets-table'
4
+ import { isManagerRole } from '~/lib/rbac'
5
+
6
+ export default async function OrganizationSupportTicketsPage({
7
+ params
8
+ }: {
9
+ params: Promise<{ orgId: string }>
10
+ }) {
11
+ const { orgId } = await params
12
+ const org = (await getOrganizationCached(orgId))?.data
13
+
14
+ if (!org || !isManagerRole(org.role)) {
15
+ return notFound()
16
+ }
17
+
18
+ return (
19
+ <SupportTicketsTable
20
+ mode="app"
21
+ organizationId={orgId}
22
+ basePath={`/app/${orgId}/support-tickets`}
23
+ />
24
+ )
25
+ }
@@ -1,29 +1,31 @@
1
1
  'use client'
2
2
 
3
- import { useParams } from 'next/navigation'
4
- import { FC } from 'react'
5
3
  import { DashboardHeader } from '~/components/dashboard-header'
6
- import type { UserOrganization } from '~/lib/app-navigation'
7
4
  import { AppSearch } from './app-search'
5
+ import { FeedbackDialog } from './feedback-dialog'
8
6
 
9
- export const AppHeader: FC<{
7
+ export function AppHeader({
8
+ user,
9
+ organizations,
10
+ isSuperAdmin,
11
+ isImpersonating
12
+ }: {
10
13
  user: {
11
14
  name?: string | null
12
15
  email?: string | null
13
16
  image?: string | null
14
17
  }
15
- organizations: UserOrganization[]
16
- }> = ({ user, organizations }) => {
17
- const params = useParams()
18
- const orgId = params?.orgId as string | undefined
19
- const role = organizations.find(o => o.id === orgId)?.role
20
-
18
+ organizations: { id: string; role: string }[]
19
+ isSuperAdmin: boolean
20
+ isImpersonating: boolean
21
+ }) {
21
22
  return (
22
23
  <DashboardHeader
23
24
  user={user}
24
- search={
25
- orgId && role ? <AppSearch orgId={orgId} role={role} /> : undefined
26
- }
25
+ isSuperAdmin={isSuperAdmin}
26
+ isImpersonating={isImpersonating}
27
+ search={<AppSearch organizations={organizations} />}
28
+ actions={<FeedbackDialog />}
27
29
  />
28
30
  )
29
31
  }
@@ -1,17 +1,26 @@
1
1
  'use client'
2
2
 
3
- import { FC } from 'react'
3
+ import { useParams } from 'next/navigation'
4
4
  import { NavigationSearch } from '~/components/navigation-search'
5
5
  import { getAppNavigation } from '~/lib/app-navigation'
6
6
 
7
- export const AppSearch: FC<{ orgId: string; role: string }> = ({
8
- orgId,
9
- role
10
- }) => {
7
+ export function AppSearch({
8
+ organizations
9
+ }: {
10
+ organizations: { id: string; role: string }[]
11
+ }) {
12
+ const params = useParams()
13
+ const orgId = params?.orgId as string | undefined
14
+ const role = organizations.find(o => o.id === orgId)?.role
15
+
16
+ if (!orgId || !role) {
17
+ return null
18
+ }
19
+
11
20
  return (
12
21
  <NavigationSearch
13
22
  groups={getAppNavigation(orgId, role)}
14
- dialogDescription="Search across this organization's pages and settings."
23
+ dialogDescription="Search this organization's pages and settings."
15
24
  />
16
25
  )
17
26
  }
@@ -2,13 +2,16 @@
2
2
 
3
3
  import { useParams } from 'next/navigation'
4
4
  import { NavigationSidebar } from '~/components/navigation-sidebar'
5
- import { getAppNavigation, type UserOrganization } from '~/lib/app-navigation'
6
- import { OrganizationSwitcher } from './organization-switcher'
5
+ import { getAppNavigation } from '~/lib/app-navigation'
6
+ import {
7
+ OrganizationSwitcher,
8
+ type SwitcherOrganization
9
+ } from './organization-switcher'
7
10
 
8
11
  export function AppSidebar({
9
12
  organizations
10
13
  }: {
11
- organizations: UserOrganization[]
14
+ organizations: (SwitcherOrganization & { role: string })[]
12
15
  }) {
13
16
  const params = useParams()
14
17
  const orgId = params?.orgId as string | undefined