create-ej-app 25.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 (385) hide show
  1. package/README.md +125 -226
  2. package/cli.ts +83 -28
  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 -0
  15. package/template/nextjs/next.config.ts +8 -1
  16. package/template/nextjs/package.json +47 -40
  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 +8 -0
  140. package/template/saas-starter/.env.example +25 -0
  141. package/template/saas-starter/.vscode/extensions.json +11 -0
  142. package/template/saas-starter/.vscode/settings.json +3 -0
  143. package/template/saas-starter/AGENTS.md +302 -0
  144. package/template/saas-starter/CLAUDE.md +1 -0
  145. package/template/saas-starter/Dockerfile +15 -0
  146. package/template/saas-starter/README.md +125 -0
  147. package/template/saas-starter/bunfig.toml +3 -0
  148. package/template/saas-starter/components.json +25 -0
  149. package/template/saas-starter/drizzle.config.ts +10 -0
  150. package/template/saas-starter/eslint.config.mjs +24 -0
  151. package/template/saas-starter/gitignore +14 -0
  152. package/template/saas-starter/next.config.ts +32 -0
  153. package/template/saas-starter/package.json +97 -0
  154. package/template/saas-starter/postcss.config.mjs +7 -0
  155. package/template/saas-starter/prettier.config.mjs +11 -0
  156. package/template/saas-starter/public/logo-dark.svg +4 -0
  157. package/template/saas-starter/public/logo.svg +4 -0
  158. package/template/saas-starter/public/placeholder.svg +1 -0
  159. package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
  160. package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
  161. package/template/saas-starter/src/app/(admin)/actions/users.ts +183 -0
  162. package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
  163. package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
  164. package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
  165. package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
  166. package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
  167. package/template/saas-starter/src/app/(admin)/admin/layout.tsx +53 -0
  168. package/template/saas-starter/src/app/(admin)/admin/page.tsx +60 -0
  169. package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
  170. package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
  171. package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
  172. package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +377 -0
  173. package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
  174. package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
  175. package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
  176. package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
  177. package/template/saas-starter/src/app/(app)/actions/billing.ts +216 -0
  178. package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
  179. package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
  180. package/template/saas-starter/src/app/(app)/actions/members.ts +189 -0
  181. package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
  182. package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
  183. package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +17 -0
  184. package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +31 -0
  185. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +92 -0
  186. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +357 -0
  187. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +130 -0
  188. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +222 -0
  189. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +115 -0
  190. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
  191. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +168 -0
  192. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +604 -0
  193. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +33 -0
  194. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +55 -0
  195. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
  196. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
  197. package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +31 -0
  198. package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +26 -0
  199. package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +30 -0
  200. package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
  201. package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +37 -0
  202. package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +200 -0
  203. package/template/saas-starter/src/app/(app)/app/layout.tsx +64 -0
  204. package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +117 -0
  205. package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +40 -0
  206. package/template/saas-starter/src/app/(app)/app/page.tsx +76 -0
  207. package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
  208. package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
  209. package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
  210. package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
  211. package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
  212. package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +131 -0
  213. package/template/saas-starter/src/app/(auth)/login/page.tsx +105 -0
  214. package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +137 -0
  215. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +94 -0
  216. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +153 -0
  217. package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
  218. package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
  219. package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
  220. package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
  221. package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
  222. package/template/saas-starter/src/app/(marketing)/layout.tsx +48 -0
  223. package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
  224. package/template/saas-starter/src/app/(marketing)/page.tsx +24 -0
  225. package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +63 -0
  226. package/template/saas-starter/src/app/(marketing)/terms/page.tsx +41 -0
  227. package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
  228. package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
  229. package/template/saas-starter/src/app/actions/uploads.ts +36 -0
  230. package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
  231. package/template/saas-starter/src/app/api/auth/[...all]/route.ts +4 -0
  232. package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
  233. package/template/saas-starter/src/app/error.tsx +37 -0
  234. package/template/saas-starter/src/app/favicon.ico +0 -0
  235. package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
  236. package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
  237. package/template/saas-starter/src/app/fonts/index.ts +60 -0
  238. package/template/saas-starter/src/app/global-error.tsx +38 -0
  239. package/template/saas-starter/src/app/globals.css +269 -0
  240. package/template/saas-starter/src/app/layout.tsx +77 -0
  241. package/template/saas-starter/src/app/not-found.tsx +49 -0
  242. package/template/saas-starter/src/app/robots.ts +36 -0
  243. package/template/saas-starter/src/app/sitemap.ts +21 -0
  244. package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
  245. package/template/saas-starter/src/components/dashboard-header.tsx +70 -0
  246. package/template/saas-starter/src/components/data-table/enum.ts +5 -0
  247. package/template/saas-starter/src/components/data-table/features.ts +49 -0
  248. package/template/saas-starter/src/components/data-table/index.tsx +961 -0
  249. package/template/saas-starter/src/components/data-table/types.ts +76 -0
  250. package/template/saas-starter/src/components/error.tsx +71 -0
  251. package/template/saas-starter/src/components/file-upload.tsx +254 -0
  252. package/template/saas-starter/src/components/loader.tsx +41 -0
  253. package/template/saas-starter/src/components/logo.tsx +49 -0
  254. package/template/saas-starter/src/components/navigation-search.tsx +140 -0
  255. package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
  256. package/template/saas-starter/src/components/page-heading.tsx +38 -0
  257. package/template/saas-starter/src/components/react-query-provider.tsx +51 -0
  258. package/template/saas-starter/src/components/route-error.tsx +55 -0
  259. package/template/saas-starter/src/components/screen-size.tsx +76 -0
  260. package/template/saas-starter/src/components/site-footer.tsx +39 -0
  261. package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
  262. package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
  263. package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
  264. package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
  265. package/template/saas-starter/src/components/theme-provider.tsx +115 -0
  266. package/template/saas-starter/src/components/theme-toggle.tsx +75 -0
  267. package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
  268. package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
  269. package/template/saas-starter/src/components/ui/badge.tsx +54 -0
  270. package/template/saas-starter/src/components/ui/button.tsx +69 -0
  271. package/template/saas-starter/src/components/ui/card.tsx +103 -0
  272. package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
  273. package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
  274. package/template/saas-starter/src/components/ui/command.tsx +192 -0
  275. package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
  276. package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
  277. package/template/saas-starter/src/components/ui/field.tsx +238 -0
  278. package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
  279. package/template/saas-starter/src/components/ui/input.tsx +19 -0
  280. package/template/saas-starter/src/components/ui/label.tsx +24 -0
  281. package/template/saas-starter/src/components/ui/popover.tsx +89 -0
  282. package/template/saas-starter/src/components/ui/select.tsx +195 -0
  283. package/template/saas-starter/src/components/ui/separator.tsx +28 -0
  284. package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
  285. package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
  286. package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
  287. package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
  288. package/template/saas-starter/src/components/ui/switch.tsx +33 -0
  289. package/template/saas-starter/src/components/ui/table.tsx +116 -0
  290. package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
  291. package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
  292. package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
  293. package/template/saas-starter/src/components/user-menu.tsx +213 -0
  294. package/template/saas-starter/src/db/common.ts +63 -0
  295. package/template/saas-starter/src/db/enums.ts +50 -0
  296. package/template/saas-starter/src/db/index.ts +60 -0
  297. package/template/saas-starter/src/db/relations.ts +112 -0
  298. package/template/saas-starter/src/db/schema.ts +342 -0
  299. package/template/saas-starter/src/emails/components/email-layout.tsx +79 -0
  300. package/template/saas-starter/src/emails/invitation.tsx +69 -0
  301. package/template/saas-starter/src/emails/magic-link.tsx +51 -0
  302. package/template/saas-starter/src/emails/test.tsx +34 -0
  303. package/template/saas-starter/src/emails/welcome.tsx +37 -0
  304. package/template/saas-starter/src/env.ts +66 -0
  305. package/template/saas-starter/src/hooks/use-mobile.ts +21 -0
  306. package/template/saas-starter/src/instrumentation.ts +44 -0
  307. package/template/saas-starter/src/lib/actions.ts +55 -0
  308. package/template/saas-starter/src/lib/admin-navigation.ts +63 -0
  309. package/template/saas-starter/src/lib/app-navigation.ts +63 -0
  310. package/template/saas-starter/src/lib/auth-client.ts +34 -0
  311. package/template/saas-starter/src/lib/auth-permissions.ts +61 -0
  312. package/template/saas-starter/src/lib/auth.ts +312 -0
  313. package/template/saas-starter/src/lib/billing.ts +152 -0
  314. package/template/saas-starter/src/lib/callback-url.ts +28 -0
  315. package/template/saas-starter/src/lib/cn.ts +4 -0
  316. package/template/saas-starter/src/lib/constants.ts +91 -0
  317. package/template/saas-starter/src/lib/delay.ts +12 -0
  318. package/template/saas-starter/src/lib/email-service.ts +118 -0
  319. package/template/saas-starter/src/lib/errors.ts +74 -0
  320. package/template/saas-starter/src/lib/format-currency.ts +25 -0
  321. package/template/saas-starter/src/lib/format-date.ts +75 -0
  322. package/template/saas-starter/src/lib/image-url.ts +54 -0
  323. package/template/saas-starter/src/lib/nodemailer.ts +67 -0
  324. package/template/saas-starter/src/lib/organization-access.ts +73 -0
  325. package/template/saas-starter/src/lib/otel.ts +117 -0
  326. package/template/saas-starter/src/lib/plan-limits.ts +37 -0
  327. package/template/saas-starter/src/lib/plans.ts +90 -0
  328. package/template/saas-starter/src/lib/rbac.ts +44 -0
  329. package/template/saas-starter/src/lib/safe-action-client.ts +126 -0
  330. package/template/saas-starter/src/lib/safe-action.ts +187 -0
  331. package/template/saas-starter/src/lib/search.ts +16 -0
  332. package/template/saas-starter/src/lib/seo.ts +7 -0
  333. package/template/saas-starter/src/lib/siteConfig.ts +20 -0
  334. package/template/saas-starter/src/lib/storage.ts +311 -0
  335. package/template/saas-starter/src/lib/stripe.ts +9 -0
  336. package/template/saas-starter/src/lib/toast-message.ts +101 -0
  337. package/template/saas-starter/src/lib/validations.ts +95 -0
  338. package/template/saas-starter/src/proxy.ts +48 -0
  339. package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
  340. package/template/saas-starter/src/scripts/reset-db.ts +22 -0
  341. package/template/saas-starter/src/scripts/seed.ts +85 -0
  342. package/template/saas-starter/src/scripts/setup-db.ts +72 -0
  343. package/template/saas-starter/src/scripts/setup-stripe-plans.ts +206 -0
  344. package/template/saas-starter/src/scripts/test-mail.ts +43 -0
  345. package/template/saas-starter/tests/admin/users.test.ts +64 -0
  346. package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
  347. package/template/saas-starter/tests/db/seed.test.ts +57 -0
  348. package/template/saas-starter/tests/helpers/action.ts +71 -0
  349. package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
  350. package/template/saas-starter/tests/helpers/index.ts +2 -0
  351. package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
  352. package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
  353. package/template/saas-starter/tests/lib/errors.test.ts +10 -0
  354. package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
  355. package/template/saas-starter/tests/lib/search.test.ts +23 -0
  356. package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
  357. package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
  358. package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
  359. package/template/saas-starter/tests/setup/preload.ts +108 -0
  360. package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
  361. package/template/saas-starter/tsconfig.json +52 -0
  362. package/cli.js +0 -143
  363. package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
  364. package/template/nextjs/src/app/(app)/layout.tsx +0 -20
  365. package/template/nextjs/terraform/artifact-registry.tf +0 -5
  366. package/template/nextjs/terraform/backend-service.tf +0 -13
  367. package/template/nextjs/terraform/cleanup.sh +0 -9
  368. package/template/nextjs/terraform/cloudbuild.tf +0 -70
  369. package/template/nextjs/terraform/cloudrun.tf +0 -68
  370. package/template/nextjs/terraform/database.tf +0 -60
  371. package/template/nextjs/terraform/forwarding-rule.tf +0 -43
  372. package/template/nextjs/terraform/gitignore +0 -4
  373. package/template/nextjs/terraform/ip-address.tf +0 -11
  374. package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
  375. package/template/nextjs/terraform/network.tf +0 -21
  376. package/template/nextjs/terraform/pre.sh +0 -3
  377. package/template/nextjs/terraform/providers.tf +0 -18
  378. package/template/nextjs/terraform/secret.txt +0 -11
  379. package/template/nextjs/terraform/setup.sh +0 -25
  380. package/template/nextjs/terraform/ssl-certs.tf +0 -32
  381. package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
  382. package/template/nextjs/terraform/target-https-proxy.tf +0 -14
  383. package/template/nextjs/terraform/terraform.tfvars +0 -4
  384. package/template/nextjs/terraform/url-map.tf +0 -23
  385. package/template/nextjs/terraform/variables.tf +0 -21
@@ -0,0 +1,241 @@
1
+ 'use client'
2
+
3
+ import { ChevronRight } from 'lucide-react'
4
+ import Link from 'next/link'
5
+ import { usePathname } from 'next/navigation'
6
+ import { ComponentType, ReactNode } from 'react'
7
+ import {
8
+ Collapsible,
9
+ CollapsibleContent,
10
+ CollapsibleTrigger
11
+ } from '~/components/ui/collapsible'
12
+ import {
13
+ DropdownMenu,
14
+ DropdownMenuContent,
15
+ DropdownMenuItem,
16
+ DropdownMenuLabel,
17
+ DropdownMenuSeparator,
18
+ DropdownMenuTrigger
19
+ } from '~/components/ui/dropdown-menu'
20
+ import {
21
+ Sidebar,
22
+ SidebarContent,
23
+ SidebarGroup,
24
+ SidebarGroupContent,
25
+ SidebarGroupLabel,
26
+ SidebarHeader,
27
+ SidebarMenu,
28
+ SidebarMenuButton,
29
+ SidebarMenuItem,
30
+ SidebarMenuSub,
31
+ SidebarMenuSubButton,
32
+ SidebarMenuSubItem,
33
+ SidebarRail,
34
+ useSidebar
35
+ } from '~/components/ui/sidebar'
36
+
37
+ type NavigationSidebarProps = {
38
+ header: ReactNode
39
+ groups: SidebarNavGroup[]
40
+ }
41
+
42
+ export type SidebarNavIcon = ComponentType<{ className?: string }>
43
+
44
+ export type SidebarNavSubItem = {
45
+ title: string
46
+ url: string
47
+ exact?: boolean
48
+ }
49
+
50
+ export type SidebarNavItem = {
51
+ title: string
52
+ url: string
53
+ icon?: SidebarNavIcon
54
+ exact?: boolean
55
+ items?: SidebarNavSubItem[]
56
+ }
57
+
58
+ export type SidebarNavGroup = {
59
+ label: string
60
+ items: SidebarNavItem[]
61
+ /** When true, the group is only shown to superadmins. */
62
+ superadminOnly?: boolean
63
+ }
64
+
65
+ export function NavigationSidebar({ header, groups }: NavigationSidebarProps) {
66
+ const pathname = usePathname()
67
+ const { state, isMobile, setOpenMobile } = useSidebar()
68
+ const isCollapsed = state === 'collapsed' && !isMobile
69
+
70
+ const handleLinkClick = () => {
71
+ if (isMobile) {
72
+ setOpenMobile(false)
73
+ }
74
+ }
75
+
76
+ const allItems = groups.flatMap(g => g.items)
77
+
78
+ const isItemActive = (item: { url: string; exact?: boolean }) => {
79
+ if (item.exact) {
80
+ return pathname === item.url
81
+ }
82
+
83
+ if (pathname !== item.url && !pathname.startsWith(`${item.url}/`)) {
84
+ return false
85
+ }
86
+
87
+ // Yield to a more specific sibling that also matches
88
+ const moreSpecificExists = allItems.some(
89
+ other =>
90
+ other.url !== item.url &&
91
+ other.url.length > item.url.length &&
92
+ other.url.startsWith(item.url) &&
93
+ (pathname === other.url || pathname.startsWith(`${other.url}/`))
94
+ )
95
+
96
+ return !moreSpecificExists
97
+ }
98
+
99
+ const isSubItemActive = (
100
+ item: SidebarNavSubItem,
101
+ siblings: SidebarNavSubItem[]
102
+ ) => {
103
+ if (!isItemActive(item)) {
104
+ return false
105
+ }
106
+
107
+ return !siblings.some(
108
+ sibling =>
109
+ sibling.url !== item.url &&
110
+ isItemActive(sibling) &&
111
+ sibling.url.length > item.url.length
112
+ )
113
+ }
114
+
115
+ const isGroupActive = (item: SidebarNavItem) => {
116
+ if (isItemActive(item)) {
117
+ return true
118
+ }
119
+
120
+ return item.items?.some(subItem => isItemActive(subItem)) ?? false
121
+ }
122
+
123
+ return (
124
+ <Sidebar collapsible="icon" variant="sidebar">
125
+ <SidebarHeader>{header}</SidebarHeader>
126
+ <SidebarContent>
127
+ {groups.map(group => (
128
+ <SidebarGroup key={group.label}>
129
+ <SidebarGroupLabel>{group.label}</SidebarGroupLabel>
130
+ <SidebarGroupContent>
131
+ <SidebarMenu>
132
+ {group.items.map(item =>
133
+ item.items ? (
134
+ isCollapsed ? (
135
+ <SidebarMenuItem key={item.title}>
136
+ <DropdownMenu>
137
+ <DropdownMenuTrigger asChild>
138
+ <SidebarMenuButton
139
+ tooltip={item.title}
140
+ isActive={isGroupActive(item)}
141
+ >
142
+ {item.icon ? <item.icon /> : null}
143
+ <span>{item.title}</span>
144
+ <ChevronRight className="ml-auto transition-transform duration-200" />
145
+ </SidebarMenuButton>
146
+ </DropdownMenuTrigger>
147
+ <DropdownMenuContent
148
+ side="right"
149
+ align="start"
150
+ sideOffset={4}
151
+ >
152
+ <DropdownMenuLabel>{item.title}</DropdownMenuLabel>
153
+ <DropdownMenuSeparator />
154
+ {item.items.map(subItem => (
155
+ <DropdownMenuItem
156
+ key={subItem.url}
157
+ asChild
158
+ className={
159
+ isSubItemActive(subItem, item.items!)
160
+ ? 'bg-accent font-medium'
161
+ : ''
162
+ }
163
+ >
164
+ <Link
165
+ href={subItem.url}
166
+ onClick={handleLinkClick}
167
+ >
168
+ {subItem.title}
169
+ </Link>
170
+ </DropdownMenuItem>
171
+ ))}
172
+ </DropdownMenuContent>
173
+ </DropdownMenu>
174
+ </SidebarMenuItem>
175
+ ) : (
176
+ <Collapsible
177
+ key={item.title}
178
+ asChild
179
+ defaultOpen={isGroupActive(item)}
180
+ className="group/collapsible"
181
+ >
182
+ <SidebarMenuItem>
183
+ <CollapsibleTrigger asChild>
184
+ <SidebarMenuButton
185
+ tooltip={item.title}
186
+ isActive={isGroupActive(item)}
187
+ >
188
+ {item.icon ? <item.icon /> : null}
189
+ <span>{item.title}</span>
190
+ <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
191
+ </SidebarMenuButton>
192
+ </CollapsibleTrigger>
193
+ <CollapsibleContent>
194
+ <SidebarMenuSub>
195
+ {item.items.map(subItem => (
196
+ <SidebarMenuSubItem key={subItem.url}>
197
+ <SidebarMenuSubButton
198
+ asChild
199
+ isActive={isSubItemActive(
200
+ subItem,
201
+ item.items!
202
+ )}
203
+ >
204
+ <Link
205
+ href={subItem.url}
206
+ onClick={handleLinkClick}
207
+ >
208
+ <span>{subItem.title}</span>
209
+ </Link>
210
+ </SidebarMenuSubButton>
211
+ </SidebarMenuSubItem>
212
+ ))}
213
+ </SidebarMenuSub>
214
+ </CollapsibleContent>
215
+ </SidebarMenuItem>
216
+ </Collapsible>
217
+ )
218
+ ) : (
219
+ <SidebarMenuItem key={item.title}>
220
+ <SidebarMenuButton
221
+ asChild
222
+ tooltip={item.title}
223
+ isActive={isItemActive(item)}
224
+ >
225
+ <Link href={item.url} onClick={handleLinkClick}>
226
+ {item.icon ? <item.icon /> : null}
227
+ <span>{item.title}</span>
228
+ </Link>
229
+ </SidebarMenuButton>
230
+ </SidebarMenuItem>
231
+ )
232
+ )}
233
+ </SidebarMenu>
234
+ </SidebarGroupContent>
235
+ </SidebarGroup>
236
+ ))}
237
+ </SidebarContent>
238
+ <SidebarRail />
239
+ </Sidebar>
240
+ )
241
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Reusable page heading component for admin panel pages.
3
+ * Renders a consistent icon, title and description layout at the top of each page.
4
+ * The `icon` prop is optional; when omitted only the text block is rendered.
5
+ * The `children` prop allows for additional elements (e.g. action buttons) to be placed on the right side of the heading.
6
+ */
7
+ import { FC, ReactNode } from 'react'
8
+
9
+ export const PageHeading = ({
10
+ title,
11
+ description,
12
+ icon: Icon,
13
+ children
14
+ }: {
15
+ title: string
16
+ description: string
17
+ icon?: FC<{ className?: string }>
18
+ children?: ReactNode
19
+ }) => {
20
+ return (
21
+ <div className="flex items-center justify-between gap-3">
22
+ <div className="flex items-center gap-3">
23
+ {Icon && (
24
+ <div className="bg-primary/10 text-primary flex h-10 w-10 shrink-0 items-center justify-center rounded-lg">
25
+ <Icon className="h-5 w-5" />
26
+ </div>
27
+ )}
28
+ <div>
29
+ <h1 className="text-2xl font-bold tracking-tight">{title}</h1>
30
+ <p className="text-muted-foreground">{description}</p>
31
+ </div>
32
+ </div>
33
+ {children ? (
34
+ <div className="flex items-center gap-2">{children}</div>
35
+ ) : null}
36
+ </div>
37
+ )
38
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @fileoverview React Query Provider Component
3
+ *
4
+ * Wraps the application with TanStack Query's QueryClientProvider.
5
+ * Enables data fetching, caching, and server state management.
6
+ *
7
+ * **Features Enabled:**
8
+ * - Automatic background refetching
9
+ * - Request deduplication
10
+ * - Cache management
11
+ * - Optimistic updates
12
+ * - useSafeActionQuery/useSafeActionMutation hooks
13
+ *
14
+ * @module components/react-query-provider
15
+ * @see https://tanstack.com/query/latest
16
+ */
17
+
18
+ 'use client'
19
+
20
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
21
+ import { FC, ReactNode, useState } from 'react'
22
+
23
+ /** Default: treat list/admin data as fresh for 30s so focus does not re-hit every action. */
24
+ const DEFAULT_STALE_TIME_MS = 30_000
25
+
26
+ function createQueryClient() {
27
+ return new QueryClient({
28
+ defaultOptions: {
29
+ queries: {
30
+ staleTime: DEFAULT_STALE_TIME_MS
31
+ }
32
+ }
33
+ })
34
+ }
35
+
36
+ /**
37
+ * React Query Provider Component
38
+ *
39
+ * Creates one QueryClient for the provider lifetime and wraps children.
40
+ * Place near the root of the client tree.
41
+ *
42
+ * Global `staleTime` is 30s. Live surfaces should pass `staleTime: 0`;
43
+ * heavy dashboards and static options can pass a longer value.
44
+ */
45
+ export const ReactQueryProvider: FC<{
46
+ children?: ReactNode
47
+ }> = ({ children }) => {
48
+ const [client] = useState(createQueryClient)
49
+
50
+ return <QueryClientProvider client={client}>{children}</QueryClientProvider>
51
+ }
@@ -0,0 +1,55 @@
1
+ 'use client'
2
+
3
+ import { AlertTriangle } from 'lucide-react'
4
+ import Link from 'next/link'
5
+ import { Button } from '~/components/ui/button'
6
+
7
+ /**
8
+ * Shared UI for App Router error boundaries (`error.tsx`, `global-error.tsx`).
9
+ * Keeps recovery actions and support reference copy consistent.
10
+ */
11
+ export function RouteError({
12
+ error,
13
+ onRetry,
14
+ homeHref = '/',
15
+ homeLabel = 'Go Home'
16
+ }: {
17
+ error: Error & { digest?: string }
18
+ onRetry: () => void
19
+ homeHref?: string
20
+ homeLabel?: string
21
+ }) {
22
+ return (
23
+ <main className="bg-background text-foreground flex min-h-screen flex-col">
24
+ <section className="flex min-h-[60vh] flex-col items-center justify-center px-4 py-16">
25
+ <div className="max-w-md text-center">
26
+ <AlertTriangle
27
+ className="text-muted-foreground/50 mx-auto size-16"
28
+ aria-hidden
29
+ />
30
+ <h1 className="mt-4 text-2xl font-semibold tracking-tight">
31
+ This Page Failed to Load
32
+ </h1>
33
+ <p className="text-muted-foreground mt-3 text-base">
34
+ Refresh the page or go home and try again. If it keeps happening,
35
+ contact support
36
+ {error.digest ? ' with the reference below' : ''}.
37
+ </p>
38
+ {error.digest ? (
39
+ <p className="text-muted-foreground mt-4 font-mono text-xs break-all">
40
+ Reference: {error.digest}
41
+ </p>
42
+ ) : null}
43
+ <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
44
+ <Button type="button" onClick={onRetry}>
45
+ Try Again
46
+ </Button>
47
+ <Button type="button" variant="outline" asChild>
48
+ <Link href={homeHref}>{homeLabel}</Link>
49
+ </Button>
50
+ </div>
51
+ </div>
52
+ </section>
53
+ </main>
54
+ )
55
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @fileoverview Screen Size Indicator (Development Tool)
3
+ *
4
+ * Displays current viewport dimensions and Tailwind breakpoint.
5
+ * Useful during development for responsive design debugging.
6
+ *
7
+ * **Displayed Info:**
8
+ * - Viewport width x height in pixels
9
+ * - Current Tailwind breakpoint (XS, SM, MD, LG, XL, 2XL)
10
+ *
11
+ * **Positioning:**
12
+ * - Fixed to bottom-right corner
13
+ * - Always visible overlay
14
+ *
15
+ * Render this component conditionally.
16
+ * only in development environment.
17
+ *
18
+ * @module components/screen-size
19
+ */
20
+
21
+ 'use client'
22
+
23
+ import { FC, useEffect, useState } from 'react'
24
+
25
+ /**
26
+ * Screen Size Indicator Component
27
+ *
28
+ * Fixed position badge showing viewport dimensions and breakpoint.
29
+ * Updates on window resize.
30
+ *
31
+ * **Breakpoint Display:**
32
+ * - XS: < 640px (default, no sm: prefix)
33
+ * - SM: 640px - 767px
34
+ * - MD: 768px - 1023px
35
+ * - LG: 1024px - 1279px
36
+ * - XL: 1280px - 1535px
37
+ * - 2XL: ≥ 1536px
38
+ *
39
+ * @returns {JSX.Element} Fixed position size indicator
40
+ */
41
+ export const ScreenSize: FC = () => {
42
+ const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
43
+
44
+ useEffect(() => {
45
+ function updateDimensions() {
46
+ setDimensions({
47
+ width: window.innerWidth,
48
+ height: window.innerHeight
49
+ })
50
+ }
51
+
52
+ updateDimensions()
53
+ window.addEventListener('resize', updateDimensions)
54
+
55
+ return () => {
56
+ window.removeEventListener('resize', updateDimensions)
57
+ }
58
+ }, [])
59
+
60
+ const { width, height } = dimensions
61
+
62
+ return (
63
+ <div className="bg-foreground text-background fixed right-4 bottom-4 flex items-center gap-2 rounded-full p-2 text-xs font-medium">
64
+ <span>
65
+ {width.toLocaleString()} x {height.toLocaleString()}
66
+ </span>
67
+ <div className="bg-muted-foreground h-4 w-px" />
68
+ <span className="sm:hidden">XS</span>
69
+ <span className="hidden sm:inline md:hidden">SM</span>
70
+ <span className="hidden md:inline lg:hidden">MD</span>
71
+ <span className="hidden lg:inline xl:hidden">LG</span>
72
+ <span className="hidden xl:inline 2xl:hidden">XL</span>
73
+ <span className="hidden 2xl:inline">2XL</span>
74
+ </div>
75
+ )
76
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @fileoverview Site Footer Component
3
+ *
4
+ * Global footer displayed on all pages.
5
+ * Contains copyright notice and links to legal pages.
6
+ *
7
+ * @module components/site-footer
8
+ */
9
+
10
+ import Link from 'next/link'
11
+
12
+ /**
13
+ * Site Footer Component
14
+ *
15
+ * Displays copyright and legal navigation links.
16
+ *
17
+ * @param {Object} props - Component props
18
+ * @param {string} props.name - Company name for copyright
19
+ * @returns {JSX.Element} Footer element with copyright and links
20
+ */
21
+ export const SiteFooter = ({ name }: { name: string }) => {
22
+ return (
23
+ <footer className="border-t">
24
+ <div className="text-muted-foreground container flex flex-col items-center justify-between gap-4 py-6 text-sm sm:flex-row">
25
+ <p>
26
+ &copy; {new Date().getUTCFullYear()} {name}. All rights reserved.
27
+ </p>
28
+ <div className="flex gap-4">
29
+ <Link href="/terms" className="hover:text-foreground">
30
+ Terms
31
+ </Link>
32
+ <Link href="/privacy" className="hover:text-foreground">
33
+ Privacy
34
+ </Link>
35
+ </div>
36
+ </div>
37
+ </footer>
38
+ )
39
+ }
@@ -0,0 +1,122 @@
1
+ 'use client'
2
+
3
+ import { useAction } from 'next-safe-action/hooks'
4
+ import { useRouter } from 'next/navigation'
5
+ import { useState } from 'react'
6
+ import { createSupportTicketAction } from '~/app/actions/support-tickets'
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 { Input } from '~/components/ui/input'
18
+ import {
19
+ Select,
20
+ SelectContent,
21
+ SelectItem,
22
+ SelectTrigger,
23
+ SelectValue
24
+ } from '~/components/ui/select'
25
+ import { Textarea } from '~/components/ui/textarea'
26
+ import { toastActionError, toastSuccessMessage } from '~/lib/toast-message'
27
+ import {
28
+ supportPriorityOptions,
29
+ type SupportTicketPriority
30
+ } from './support-ticket-shared'
31
+
32
+ export function CreateSupportTicketDialog({
33
+ children,
34
+ organizationId,
35
+ redirectBasePath
36
+ }: {
37
+ children: React.ReactNode
38
+ /** Omit to raise an account ticket rather than an organization one. */
39
+ organizationId?: string
40
+ redirectBasePath: string
41
+ }) {
42
+ const router = useRouter()
43
+ const [open, setOpen] = useState(false)
44
+ const [subject, setSubject] = useState('')
45
+ const [priority, setPriority] = useState<SupportTicketPriority>('medium')
46
+ const [message, setMessage] = useState('')
47
+
48
+ const { execute, isExecuting } = useAction(createSupportTicketAction, {
49
+ onSuccess: ({ data }) => {
50
+ toastSuccessMessage('Support ticket created')
51
+ setOpen(false)
52
+ setSubject('')
53
+ setMessage('')
54
+ if (data?.id) {
55
+ router.push(`${redirectBasePath}/${data.id}`)
56
+ } else {
57
+ router.refresh()
58
+ }
59
+ },
60
+ onError: ({ error }) => {
61
+ toastActionError(error, 'The support ticket was not created. Try again.')
62
+ }
63
+ })
64
+
65
+ const handleSubmit = () => {
66
+ execute({
67
+ organizationId: organizationId ?? null,
68
+ subject,
69
+ priority,
70
+ message
71
+ })
72
+ }
73
+
74
+ return (
75
+ <Dialog open={open} onOpenChange={setOpen}>
76
+ <DialogTrigger asChild>{children}</DialogTrigger>
77
+ <DialogContent>
78
+ <DialogHeader>
79
+ <DialogTitle>New Support Ticket</DialogTitle>
80
+ <DialogDescription>
81
+ Describe the problem, including what you expected and what happened.
82
+ </DialogDescription>
83
+ </DialogHeader>
84
+
85
+ <div className="space-y-4">
86
+ <Input
87
+ value={subject}
88
+ onChange={event => setSubject(event.target.value)}
89
+ placeholder="Subject"
90
+ />
91
+ <Select
92
+ value={priority}
93
+ onValueChange={value => setPriority(value as SupportTicketPriority)}
94
+ >
95
+ <SelectTrigger className="w-full">
96
+ <SelectValue placeholder="Priority" />
97
+ </SelectTrigger>
98
+ <SelectContent>
99
+ {supportPriorityOptions.map(option => (
100
+ <SelectItem key={option.value} value={option.value}>
101
+ {option.label}
102
+ </SelectItem>
103
+ ))}
104
+ </SelectContent>
105
+ </Select>
106
+ <Textarea
107
+ value={message}
108
+ onChange={event => setMessage(event.target.value)}
109
+ placeholder="What can we help with?"
110
+ className="min-h-32"
111
+ />
112
+ </div>
113
+
114
+ <DialogFooter>
115
+ <Button onClick={handleSubmit} disabled={isExecuting}>
116
+ Create ticket
117
+ </Button>
118
+ </DialogFooter>
119
+ </DialogContent>
120
+ </Dialog>
121
+ )
122
+ }