create-ej-app 26.0.0 → 28.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 +47 -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 +50 -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,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
+ }
@@ -1,7 +1,43 @@
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
+
1
21
  'use client'
2
22
 
3
23
  import { FC, useEffect, useState } from 'react'
4
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
+ */
5
41
  export const ScreenSize: FC = () => {
6
42
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
7
43
 
@@ -1,13 +1,29 @@
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
+
1
10
  import Link from 'next/link'
2
- import { siteConfig } from '~/lib/siteConfig'
3
11
 
4
- export const SiteFooter = () => {
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 }) => {
5
22
  return (
6
23
  <footer className="border-t">
7
24
  <div className="text-muted-foreground container flex flex-col items-center justify-between gap-4 py-6 text-sm sm:flex-row">
8
25
  <p>
9
- &copy; {new Date().getUTCFullYear()} {siteConfig.name}. All rights
10
- reserved.
26
+ &copy; {new Date().getUTCFullYear()} {name}. All rights reserved.
11
27
  </p>
12
28
  <div className="flex gap-4">
13
29
  <Link href="/terms" className="hover:text-foreground">
@@ -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
+ }
@@ -0,0 +1,236 @@
1
+ 'use client'
2
+
3
+ import { ArrowLeft, Loader2, Send } from 'lucide-react'
4
+ import Link from 'next/link'
5
+ import { useState } from 'react'
6
+ import {
7
+ addSupportTicketMessageAction,
8
+ getSupportTicketDetailsAction,
9
+ updateSupportTicketStatusAction
10
+ } from '~/app/actions/support-tickets'
11
+ import { Button } from '~/components/ui/button'
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue
18
+ } from '~/components/ui/select'
19
+ import { Textarea } from '~/components/ui/textarea'
20
+ import { formatDate } from '~/lib/format-date'
21
+ import {
22
+ useSafeActionMutation,
23
+ useSafeActionQuery
24
+ } from '~/lib/safe-action-client'
25
+ import { toastActionError, toastSuccessMessage } from '~/lib/toast-message'
26
+ import {
27
+ SupportPriorityBadge,
28
+ SupportStatusBadge,
29
+ formatSupportLabel,
30
+ supportStatusOptions,
31
+ type SupportTicketStatus
32
+ } from './support-ticket-shared'
33
+
34
+ export function SupportTicketConversation({
35
+ ticketId,
36
+ backHref,
37
+ mode
38
+ }: {
39
+ ticketId: string
40
+ backHref: string
41
+ /** `admin` answers as the support team and can change the status. */
42
+ mode: 'app' | 'admin'
43
+ }) {
44
+ const [message, setMessage] = useState('')
45
+ const { data, isLoading, refetch } = useSafeActionQuery(
46
+ `support-ticket-${ticketId}`,
47
+ getSupportTicketDetailsAction,
48
+ { ticketId }
49
+ )
50
+
51
+ const addMessage = useSafeActionMutation(addSupportTicketMessageAction, {
52
+ onSuccess: () => {
53
+ setMessage('')
54
+ void refetch()
55
+ toastSuccessMessage('Message sent')
56
+ },
57
+ onError: error =>
58
+ toastActionError(error, 'Your message was not sent. Try again.')
59
+ })
60
+
61
+ const updateStatus = useSafeActionMutation(updateSupportTicketStatusAction, {
62
+ onSuccess: () => {
63
+ void refetch()
64
+ toastSuccessMessage('Ticket status updated')
65
+ },
66
+ onError: error =>
67
+ toastActionError(error, 'The status was not changed. Try again.')
68
+ })
69
+
70
+ if (isLoading) {
71
+ return (
72
+ <div className="flex h-[60vh] items-center justify-center">
73
+ <Loader2 className="text-primary h-8 w-8 animate-spin" />
74
+ </div>
75
+ )
76
+ }
77
+
78
+ if (!data) return null
79
+
80
+ const customerReplyDisabled =
81
+ mode === 'app' && ['closed', 'resolved'].includes(data.ticket.status)
82
+ const replyDisabled =
83
+ customerReplyDisabled || addMessage.isPending || !message.trim()
84
+
85
+ return (
86
+ <div className="space-y-5">
87
+ <div className="flex flex-wrap items-start justify-between gap-3">
88
+ <div className="space-y-3">
89
+ <Button asChild variant="ghost" size="sm" className="-ml-2">
90
+ <Link href={backHref}>
91
+ <ArrowLeft className="mr-2 h-4 w-4" />
92
+ Back
93
+ </Link>
94
+ </Button>
95
+ <div>
96
+ <div className="text-muted-foreground font-mono text-sm">
97
+ {data.ticket.ticketNumber}
98
+ </div>
99
+ <h1 className="text-2xl font-bold tracking-tight">
100
+ {data.ticket.subject}
101
+ </h1>
102
+ <div className="mt-2 flex flex-wrap items-center gap-2">
103
+ <SupportStatusBadge status={data.ticket.status} />
104
+ <SupportPriorityBadge priority={data.ticket.priority} />
105
+ <span className="text-muted-foreground text-sm">
106
+ Created {formatDate(data.ticket.createdAt, { short: true })}
107
+ </span>
108
+ </div>
109
+ </div>
110
+ </div>
111
+
112
+ {mode === 'admin' ? (
113
+ <div className="min-w-52">
114
+ <Select
115
+ value={data.ticket.status}
116
+ onValueChange={value =>
117
+ updateStatus.mutate({
118
+ ticketId,
119
+ status: value as SupportTicketStatus
120
+ })
121
+ }
122
+ disabled={updateStatus.isPending}
123
+ >
124
+ <SelectTrigger className="w-full">
125
+ <SelectValue placeholder="Update status" />
126
+ </SelectTrigger>
127
+ <SelectContent>
128
+ {supportStatusOptions.map(option => (
129
+ <SelectItem key={option.value} value={option.value}>
130
+ {option.label}
131
+ </SelectItem>
132
+ ))}
133
+ </SelectContent>
134
+ </Select>
135
+ </div>
136
+ ) : null}
137
+ </div>
138
+
139
+ <div className="grid gap-5 lg:grid-cols-[1fr_280px]">
140
+ <section className="border-border bg-card overflow-hidden rounded-lg border shadow-sm">
141
+ <div className="border-border bg-muted/30 border-b px-5 py-3">
142
+ <h2 className="font-semibold">Conversation</h2>
143
+ </div>
144
+ <div className="space-y-4 p-5">
145
+ {data.messages.map(item => {
146
+ const mine = mode === 'admin' ? item.isAdmin : !item.isAdmin
147
+ return (
148
+ <div
149
+ key={item.id}
150
+ className={`flex ${mine ? 'justify-end' : 'justify-start'}`}
151
+ >
152
+ <div
153
+ className={`max-w-[82%] rounded-lg border px-4 py-3 ${
154
+ mine
155
+ ? 'bg-primary text-primary-foreground border-primary'
156
+ : 'bg-background'
157
+ }`}
158
+ >
159
+ <div className="mb-1 text-xs opacity-75">
160
+ {item.isAdmin
161
+ ? 'Support Team'
162
+ : item.userName || item.userEmail || 'Customer'}{' '}
163
+ ·{' '}
164
+ {formatDate(item.createdAt, { short: true, time: true })}
165
+ </div>
166
+ <p className="text-sm leading-relaxed whitespace-pre-wrap">
167
+ {item.message}
168
+ </p>
169
+ </div>
170
+ </div>
171
+ )
172
+ })}
173
+ </div>
174
+ <div className="border-border bg-muted/20 space-y-3 border-t p-5">
175
+ <Textarea
176
+ value={message}
177
+ disabled={customerReplyDisabled || addMessage.isPending}
178
+ onChange={event => setMessage(event.target.value)}
179
+ placeholder={
180
+ customerReplyDisabled
181
+ ? 'This ticket is resolved or closed. Open a new one to follow up.'
182
+ : 'Write a reply…'
183
+ }
184
+ className="min-h-28"
185
+ />
186
+ <div className="flex justify-end">
187
+ <Button
188
+ disabled={replyDisabled}
189
+ onClick={() =>
190
+ addMessage.mutate({
191
+ ticketId,
192
+ message,
193
+ asSupport: mode === 'admin'
194
+ })
195
+ }
196
+ >
197
+ <Send className="mr-2 h-4 w-4" />
198
+ Send message
199
+ </Button>
200
+ </div>
201
+ </div>
202
+ </section>
203
+
204
+ <aside className="border-border bg-card h-fit rounded-lg border p-5 shadow-sm">
205
+ <h2 className="font-semibold">Ticket Details</h2>
206
+ <dl className="mt-4 space-y-3 text-sm">
207
+ <div>
208
+ <dt className="text-muted-foreground">Status</dt>
209
+ <dd>{formatSupportLabel(data.ticket.status)}</dd>
210
+ </div>
211
+ <div>
212
+ <dt className="text-muted-foreground">Priority</dt>
213
+ <dd>{formatSupportLabel(data.ticket.priority)}</dd>
214
+ </div>
215
+ <div>
216
+ <dt className="text-muted-foreground">Organization</dt>
217
+ <dd>{data.ticket.organizationName || '-'}</dd>
218
+ </div>
219
+ <div>
220
+ <dt className="text-muted-foreground">Raised by</dt>
221
+ <dd>{data.ticket.userName || data.ticket.userEmail || '-'}</dd>
222
+ </div>
223
+ <div>
224
+ <dt className="text-muted-foreground">Resolved</dt>
225
+ <dd>
226
+ {data.ticket.resolvedAt
227
+ ? formatDate(data.ticket.resolvedAt, { short: true })
228
+ : '-'}
229
+ </dd>
230
+ </div>
231
+ </dl>
232
+ </aside>
233
+ </div>
234
+ </div>
235
+ )
236
+ }
@@ -0,0 +1,58 @@
1
+ import { Badge } from '~/components/ui/badge'
2
+
3
+ export const supportStatusOptions = [
4
+ { label: 'Open', value: 'open' },
5
+ { label: 'In Progress', value: 'in_progress' },
6
+ { label: 'Waiting on Customer', value: 'waiting_customer' },
7
+ { label: 'Resolved', value: 'resolved' },
8
+ { label: 'Closed', value: 'closed' }
9
+ ] as const
10
+
11
+ export const supportPriorityOptions = [
12
+ { label: 'Low', value: 'low' },
13
+ { label: 'Medium', value: 'medium' },
14
+ { label: 'High', value: 'high' },
15
+ { label: 'Urgent', value: 'urgent' }
16
+ ] as const
17
+
18
+ export type SupportTicketStatus = (typeof supportStatusOptions)[number]['value']
19
+ export type SupportTicketPriority =
20
+ (typeof supportPriorityOptions)[number]['value']
21
+
22
+ export function formatSupportLabel(value: string | null) {
23
+ if (!value) return '-'
24
+ return value.replaceAll('_', ' ').replace(/\b\w/g, char => char.toUpperCase())
25
+ }
26
+
27
+ export function SupportStatusBadge({ status }: { status: string }) {
28
+ const variantMap = {
29
+ open: 'info',
30
+ in_progress: 'info',
31
+ waiting_customer: 'warning',
32
+ resolved: 'success',
33
+ closed: 'secondary'
34
+ } as const
35
+
36
+ const variant = variantMap[status as keyof typeof variantMap] ?? 'secondary'
37
+
38
+ return <Badge variant={variant}>{formatSupportLabel(status)}</Badge>
39
+ }
40
+
41
+ export function SupportPriorityBadge({
42
+ priority
43
+ }: {
44
+ priority: string | null
45
+ }) {
46
+ if (!priority) return <span className="text-muted-foreground">-</span>
47
+
48
+ const variantMap = {
49
+ low: 'outline',
50
+ medium: 'secondary',
51
+ high: 'warning',
52
+ urgent: 'destructive'
53
+ } as const
54
+
55
+ const variant = variantMap[priority as keyof typeof variantMap] ?? 'secondary'
56
+
57
+ return <Badge variant={variant}>{formatSupportLabel(priority)}</Badge>
58
+ }