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.
- package/README.md +125 -226
- package/cli.ts +79 -29
- package/package.json +2 -2
- package/template/api/.vscode/settings.json +1 -1
- package/template/api/{.github/copilot-instructions.md → AGENTS.md} +0 -2
- package/template/api/CLAUDE.md +1 -0
- package/template/api/package.json +10 -10
- package/template/nextjs/.dockerignore +1 -0
- package/template/nextjs/AGENTS.md +260 -0
- package/template/nextjs/CLAUDE.md +1 -0
- package/template/nextjs/README.md +109 -0
- package/template/nextjs/bunfig.toml +3 -0
- package/template/nextjs/eslint.config.mjs +6 -5
- package/template/nextjs/gitignore +6 -1
- package/template/nextjs/next.config.ts +8 -1
- package/template/nextjs/package.json +47 -39
- package/template/nextjs/public/logo-dark.svg +4 -0
- package/template/nextjs/public/logo.svg +4 -0
- package/template/nextjs/src/app/(admin)/actions/users.ts +183 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
- package/template/nextjs/src/app/(admin)/admin/layout.tsx +53 -0
- package/template/nextjs/src/app/(admin)/admin/page.tsx +11 -2
- package/template/nextjs/src/app/(admin)/admin/settings/layout.tsx +24 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/page.tsx +377 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/validation.ts +23 -0
- package/template/nextjs/src/app/(app)/app/layout.tsx +44 -0
- package/template/nextjs/src/app/(app)/app/page.tsx +14 -2
- package/template/nextjs/src/app/(auth)/(account)/layout.tsx +64 -0
- package/template/nextjs/src/app/(auth)/(account)/profile/page.tsx +16 -0
- package/template/nextjs/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
- package/template/nextjs/src/app/(auth)/login/email-login-form.tsx +56 -29
- package/template/nextjs/src/app/(auth)/login/page.tsx +45 -12
- package/template/nextjs/src/app/(auth)/login/social-login-buttons.tsx +85 -17
- package/template/nextjs/src/app/(marketing)/layout.tsx +35 -4
- package/template/nextjs/src/app/(marketing)/legal.tsx +56 -0
- package/template/nextjs/src/app/(marketing)/page.tsx +20 -2
- package/template/nextjs/src/app/(marketing)/privacy/page.tsx +43 -2
- package/template/nextjs/src/app/(marketing)/terms/page.tsx +33 -2
- package/template/nextjs/src/app/error.tsx +37 -0
- package/template/nextjs/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
- package/template/nextjs/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
- package/template/nextjs/src/app/fonts/index.ts +60 -0
- package/template/nextjs/src/app/global-error.tsx +38 -0
- package/template/nextjs/src/app/globals.css +29 -0
- package/template/nextjs/src/app/layout.tsx +45 -18
- package/template/nextjs/src/app/not-found.tsx +25 -3
- package/template/nextjs/src/app/robots.ts +29 -0
- package/template/nextjs/src/app/sitemap.ts +21 -0
- package/template/nextjs/src/components/confirm-dialog.tsx +79 -0
- package/template/nextjs/src/components/dashboard-header.tsx +70 -0
- package/template/nextjs/src/components/data-table/enum.ts +5 -0
- package/template/nextjs/src/components/data-table/features.ts +49 -0
- package/template/nextjs/src/components/data-table/index.tsx +961 -0
- package/template/nextjs/src/components/data-table/types.ts +76 -0
- package/template/nextjs/src/components/error.tsx +71 -0
- package/template/nextjs/src/components/loader.tsx +18 -2
- package/template/nextjs/src/components/logo.tsx +49 -0
- package/template/nextjs/src/components/navigation-search.tsx +140 -0
- package/template/nextjs/src/components/navigation-sidebar.tsx +241 -0
- package/template/nextjs/src/components/page-heading.tsx +38 -0
- package/template/nextjs/src/components/react-query-provider.tsx +41 -2
- package/template/nextjs/src/components/route-error.tsx +55 -0
- package/template/nextjs/src/components/screen-size.tsx +36 -0
- package/template/nextjs/src/components/site-footer.tsx +20 -4
- package/template/nextjs/src/components/theme-provider.tsx +48 -0
- package/template/nextjs/src/components/theme-toggle.tsx +58 -5
- package/template/nextjs/src/components/ui/alert-dialog.tsx +199 -0
- package/template/nextjs/src/components/ui/avatar.tsx +112 -0
- package/template/nextjs/src/components/ui/badge.tsx +54 -0
- package/template/nextjs/src/components/ui/button.tsx +3 -1
- package/template/nextjs/src/components/ui/card.tsx +4 -4
- package/template/nextjs/src/components/ui/collapsible.tsx +33 -0
- package/template/nextjs/src/components/ui/command.tsx +192 -0
- package/template/nextjs/src/components/ui/dialog.tsx +164 -0
- package/template/nextjs/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/nextjs/src/components/ui/input-group.tsx +156 -0
- package/template/nextjs/src/components/ui/select.tsx +195 -0
- package/template/nextjs/src/components/ui/sheet.tsx +143 -0
- package/template/nextjs/src/components/ui/sidebar.tsx +702 -0
- package/template/nextjs/src/components/ui/skeleton.tsx +13 -0
- package/template/nextjs/src/components/ui/table.tsx +116 -0
- package/template/nextjs/src/components/ui/textarea.tsx +18 -0
- package/template/nextjs/src/components/ui/tooltip.tsx +57 -0
- package/template/nextjs/src/components/user-menu.tsx +206 -0
- package/template/nextjs/src/db/common.ts +10 -0
- package/template/nextjs/src/db/index.ts +16 -6
- package/template/nextjs/src/db/relations.ts +21 -0
- package/template/nextjs/src/db/schema.ts +85 -46
- package/template/nextjs/src/emails/components/email-layout.tsx +27 -11
- package/template/nextjs/src/emails/magic-link.tsx +15 -13
- package/template/nextjs/src/emails/test.tsx +11 -9
- package/template/nextjs/src/emails/welcome.tsx +12 -9
- package/template/nextjs/src/env.ts +1 -1
- package/template/nextjs/src/hooks/use-mobile.ts +14 -12
- package/template/nextjs/src/instrumentation.ts +20 -2
- package/template/nextjs/src/lib/actions.ts +55 -0
- package/template/nextjs/src/lib/admin-navigation.ts +43 -0
- package/template/nextjs/src/lib/auth-client.ts +20 -9
- package/template/nextjs/src/lib/auth-permissions.ts +61 -0
- package/template/nextjs/src/lib/auth.ts +33 -3
- package/template/nextjs/src/lib/callback-url.ts +28 -0
- package/template/nextjs/src/lib/constants.ts +21 -0
- package/template/nextjs/src/lib/delay.ts +10 -0
- package/template/nextjs/src/lib/email-service.ts +21 -0
- package/template/nextjs/src/lib/errors.ts +74 -0
- package/template/nextjs/src/lib/format-currency.ts +22 -24
- package/template/nextjs/src/lib/format-date.ts +69 -6
- package/template/nextjs/src/lib/nodemailer.ts +27 -3
- package/template/nextjs/src/lib/otel.ts +92 -2
- package/template/nextjs/src/lib/safe-action-client.ts +60 -9
- package/template/nextjs/src/lib/safe-action.ts +152 -16
- package/template/nextjs/src/lib/search.ts +16 -0
- package/template/nextjs/src/lib/seo.ts +7 -0
- package/template/nextjs/src/lib/siteConfig.ts +17 -1
- package/template/nextjs/src/lib/toast-message.ts +87 -1
- package/template/nextjs/src/lib/validations.ts +30 -0
- package/template/nextjs/src/proxy.ts +48 -0
- package/template/nextjs/src/scripts/reset-db.ts +22 -0
- package/template/nextjs/src/scripts/seed.ts +59 -8
- package/template/nextjs/src/scripts/test-mail.ts +28 -20
- package/template/nextjs/tests/admin/users.test.ts +64 -0
- package/template/nextjs/tests/db/fk-indexes.test.ts +62 -0
- package/template/nextjs/tests/db/seed.test.ts +49 -0
- package/template/nextjs/tests/helpers/action.ts +71 -0
- package/template/nextjs/tests/helpers/fixtures.ts +78 -0
- package/template/nextjs/tests/helpers/index.ts +2 -0
- package/template/nextjs/tests/lib/callback-url.test.ts +48 -0
- package/template/nextjs/tests/lib/client-server-boundary.test.ts +57 -0
- package/template/nextjs/tests/lib/errors.test.ts +10 -0
- package/template/nextjs/tests/lib/search.test.ts +23 -0
- package/template/nextjs/tests/setup/pglite-db.ts +63 -0
- package/template/nextjs/tests/setup/preload.ts +95 -0
- package/template/nextjs/tsconfig.json +1 -0
- package/template/saas-starter/.dockerignore +1 -0
- package/template/saas-starter/.env.example +15 -15
- package/template/saas-starter/AGENTS.md +302 -0
- package/template/saas-starter/CLAUDE.md +1 -0
- package/template/saas-starter/README.md +124 -2
- package/template/saas-starter/bunfig.toml +3 -0
- package/template/saas-starter/eslint.config.mjs +6 -5
- package/template/saas-starter/gitignore +6 -1
- package/template/saas-starter/next.config.ts +19 -1
- package/template/saas-starter/package.json +50 -44
- package/template/saas-starter/public/logo-dark.svg +4 -0
- package/template/saas-starter/public/logo.svg +4 -0
- package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
- package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
- package/template/saas-starter/src/app/(admin)/actions/users.ts +145 -59
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +4 -1
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +12 -10
- package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
- package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
- package/template/saas-starter/src/app/(admin)/admin/layout.tsx +24 -28
- package/template/saas-starter/src/app/(admin)/admin/page.tsx +59 -2
- package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +372 -10
- package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
- package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
- package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
- package/template/saas-starter/src/app/(app)/actions/billing.ts +171 -107
- package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
- package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
- package/template/saas-starter/src/app/(app)/actions/members.ts +149 -123
- package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
- package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +6 -9
- package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +6 -9
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +45 -53
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +40 -47
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +80 -105
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +72 -82
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +51 -52
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +15 -16
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +77 -44
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +11 -18
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +14 -28
- package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +15 -13
- package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +15 -6
- package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +6 -3
- package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +5 -9
- package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +68 -8
- package/template/saas-starter/src/app/(app)/app/layout.tsx +44 -32
- package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +75 -87
- package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +10 -12
- package/template/saas-starter/src/app/(app)/app/page.tsx +16 -21
- package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
- package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
- package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
- package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
- package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
- package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +49 -27
- package/template/saas-starter/src/app/(auth)/login/page.tsx +39 -21
- package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +80 -18
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +16 -10
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +28 -42
- package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
- package/template/saas-starter/src/app/(marketing)/layout.tsx +38 -4
- package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
- package/template/saas-starter/src/app/(marketing)/page.tsx +23 -2
- package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +62 -2
- package/template/saas-starter/src/app/(marketing)/terms/page.tsx +40 -2
- package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
- package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
- package/template/saas-starter/src/app/actions/uploads.ts +36 -0
- package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
- package/template/saas-starter/src/app/error.tsx +37 -0
- package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
- package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
- package/template/saas-starter/src/app/fonts/index.ts +60 -0
- package/template/saas-starter/src/app/global-error.tsx +38 -0
- package/template/saas-starter/src/app/globals.css +29 -0
- package/template/saas-starter/src/app/layout.tsx +43 -19
- package/template/saas-starter/src/app/not-found.tsx +25 -3
- package/template/saas-starter/src/app/robots.ts +14 -10
- package/template/saas-starter/src/app/sitemap.ts +2 -1
- package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
- package/template/saas-starter/src/components/dashboard-header.tsx +7 -1
- package/template/saas-starter/src/components/data-table/features.ts +49 -0
- package/template/saas-starter/src/components/data-table/index.tsx +65 -64
- package/template/saas-starter/src/components/data-table/types.ts +5 -4
- package/template/saas-starter/src/components/error.tsx +45 -0
- package/template/saas-starter/src/components/file-upload.tsx +65 -26
- package/template/saas-starter/src/components/loader.tsx +18 -2
- package/template/saas-starter/src/components/logo.tsx +49 -0
- package/template/saas-starter/src/components/navigation-search.tsx +2 -2
- package/template/saas-starter/src/components/page-heading.tsx +6 -0
- package/template/saas-starter/src/components/react-query-provider.tsx +41 -2
- package/template/saas-starter/src/components/route-error.tsx +55 -0
- package/template/saas-starter/src/components/screen-size.tsx +36 -0
- package/template/saas-starter/src/components/site-footer.tsx +20 -4
- package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
- package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
- package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
- package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
- package/template/saas-starter/src/components/theme-provider.tsx +48 -0
- package/template/saas-starter/src/components/theme-toggle.tsx +58 -5
- package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
- package/template/saas-starter/src/components/ui/badge.tsx +6 -1
- package/template/saas-starter/src/components/ui/button.tsx +2 -0
- package/template/saas-starter/src/components/ui/command.tsx +1 -1
- package/template/saas-starter/src/components/user-menu.tsx +147 -24
- package/template/saas-starter/src/db/common.ts +33 -1
- package/template/saas-starter/src/db/enums.ts +24 -0
- package/template/saas-starter/src/db/index.ts +14 -5
- package/template/saas-starter/src/db/relations.ts +40 -3
- package/template/saas-starter/src/db/schema.ts +186 -35
- package/template/saas-starter/src/emails/components/email-layout.tsx +27 -11
- package/template/saas-starter/src/emails/invitation.tsx +34 -21
- package/template/saas-starter/src/emails/magic-link.tsx +15 -13
- package/template/saas-starter/src/emails/test.tsx +11 -9
- package/template/saas-starter/src/emails/welcome.tsx +12 -9
- package/template/saas-starter/src/env.ts +1 -1
- package/template/saas-starter/src/hooks/use-mobile.ts +14 -12
- package/template/saas-starter/src/instrumentation.ts +20 -2
- package/template/saas-starter/src/lib/actions.ts +34 -0
- package/template/saas-starter/src/lib/admin-navigation.ts +33 -1
- package/template/saas-starter/src/lib/app-navigation.ts +45 -41
- package/template/saas-starter/src/lib/auth-client.ts +8 -3
- package/template/saas-starter/src/lib/auth-permissions.ts +50 -11
- package/template/saas-starter/src/lib/auth.ts +122 -21
- package/template/saas-starter/src/lib/billing.ts +59 -24
- package/template/saas-starter/src/lib/callback-url.ts +28 -0
- package/template/saas-starter/src/lib/constants.ts +78 -9
- package/template/saas-starter/src/lib/delay.ts +10 -0
- package/template/saas-starter/src/lib/email-service.ts +30 -1
- package/template/saas-starter/src/lib/errors.ts +74 -0
- package/template/saas-starter/src/lib/format-currency.ts +22 -24
- package/template/saas-starter/src/lib/format-date.ts +69 -6
- package/template/saas-starter/src/lib/image-url.ts +54 -0
- package/template/saas-starter/src/lib/nodemailer.ts +27 -3
- package/template/saas-starter/src/lib/organization-access.ts +56 -76
- package/template/saas-starter/src/lib/otel.ts +92 -2
- package/template/saas-starter/src/lib/plan-limits.ts +37 -0
- package/template/saas-starter/src/lib/plans.ts +40 -13
- package/template/saas-starter/src/lib/rbac.ts +44 -0
- package/template/saas-starter/src/lib/safe-action-client.ts +60 -9
- package/template/saas-starter/src/lib/safe-action.ts +137 -17
- package/template/saas-starter/src/lib/search.ts +16 -0
- package/template/saas-starter/src/lib/seo.ts +7 -0
- package/template/saas-starter/src/lib/siteConfig.ts +17 -1
- package/template/saas-starter/src/lib/storage.ts +176 -46
- package/template/saas-starter/src/lib/stripe.ts +4 -1
- package/template/saas-starter/src/lib/toast-message.ts +87 -1
- package/template/saas-starter/src/lib/validations.ts +85 -2
- package/template/saas-starter/src/proxy.ts +48 -0
- package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
- package/template/saas-starter/src/scripts/seed.ts +82 -8
- package/template/saas-starter/src/scripts/setup-db.ts +72 -0
- package/template/saas-starter/src/scripts/setup-stripe-plans.ts +13 -7
- package/template/saas-starter/src/scripts/test-mail.ts +28 -20
- package/template/saas-starter/tests/admin/users.test.ts +64 -0
- package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
- package/template/saas-starter/tests/db/seed.test.ts +57 -0
- package/template/saas-starter/tests/helpers/action.ts +71 -0
- package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
- package/template/saas-starter/tests/helpers/index.ts +2 -0
- package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
- package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
- package/template/saas-starter/tests/lib/errors.test.ts +10 -0
- package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
- package/template/saas-starter/tests/lib/search.test.ts +23 -0
- package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
- package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
- package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
- package/template/saas-starter/tests/setup/preload.ts +108 -0
- package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
- package/template/saas-starter/tsconfig.json +1 -0
- package/cli.js +0 -147
- package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
- package/template/nextjs/src/app/(app)/layout.tsx +0 -20
- package/template/nextjs/terraform/artifact-registry.tf +0 -5
- package/template/nextjs/terraform/backend-service.tf +0 -13
- package/template/nextjs/terraform/cleanup.sh +0 -9
- package/template/nextjs/terraform/cloudbuild.tf +0 -70
- package/template/nextjs/terraform/cloudrun.tf +0 -68
- package/template/nextjs/terraform/database.tf +0 -60
- package/template/nextjs/terraform/forwarding-rule.tf +0 -43
- package/template/nextjs/terraform/gitignore +0 -4
- package/template/nextjs/terraform/ip-address.tf +0 -11
- package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
- package/template/nextjs/terraform/network.tf +0 -21
- package/template/nextjs/terraform/pre.sh +0 -3
- package/template/nextjs/terraform/providers.tf +0 -18
- package/template/nextjs/terraform/secret.txt +0 -11
- package/template/nextjs/terraform/setup.sh +0 -25
- package/template/nextjs/terraform/ssl-certs.tf +0 -32
- package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
- package/template/nextjs/terraform/target-https-proxy.tf +0 -14
- package/template/nextjs/terraform/terraform.tfvars +0 -4
- package/template/nextjs/terraform/url-map.tf +0 -23
- package/template/nextjs/terraform/variables.tf +0 -21
- package/template/saas-starter/next-env.d.ts +0 -6
- package/template/saas-starter/src/actions/uploads.ts +0 -28
- package/template/saas-starter/src/actions/uploads.validation.ts +0 -14
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +0 -183
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +0 -129
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +0 -415
- package/template/saas-starter/src/app/(app)/actions/organization.ts +0 -92
- package/template/saas-starter/src/app/proxy.ts +0 -28
- package/template/saas-starter/src/components/impersonation-banner.tsx +0 -44
- package/template/saas-starter/src/lib/organization-plan-limits.ts +0 -38
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
1
3
|
import { render, toPlainText } from '@react-email/render'
|
|
2
4
|
import MagicLinkEmail, { MagicLinkEmailProps } from '~/emails/magic-link'
|
|
3
5
|
import WelcomeEmail, { WelcomeEmailProps } from '~/emails/welcome'
|
|
@@ -22,6 +24,9 @@ const componentMap: Record<
|
|
|
22
24
|
magicLink: props => MagicLinkEmail(props as MagicLinkEmailProps)
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Generic email template renderer with type-safe overloads
|
|
29
|
+
*/
|
|
25
30
|
async function renderEmailTemplate<T extends EmailTemplateType>(
|
|
26
31
|
templateType: T,
|
|
27
32
|
props: EmailTemplateMap[T]
|
|
@@ -40,6 +45,14 @@ async function renderEmailTemplate(
|
|
|
40
45
|
return { html, text }
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Sends welcome email to newly registered users
|
|
50
|
+
*
|
|
51
|
+
* @param to - Recipient email address
|
|
52
|
+
* @param props - Welcome email template props
|
|
53
|
+
* @returns Promise resolving to SentMessageInfo from nodemailer
|
|
54
|
+
* @throws Error if sending the email fails
|
|
55
|
+
*/
|
|
43
56
|
export async function sendWelcomeEmail(to: string, props: WelcomeEmailProps) {
|
|
44
57
|
'use workflow'
|
|
45
58
|
|
|
@@ -53,6 +66,14 @@ export async function sendWelcomeEmail(to: string, props: WelcomeEmailProps) {
|
|
|
53
66
|
})
|
|
54
67
|
}
|
|
55
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Sends magic link email for passwordless authentication
|
|
71
|
+
*
|
|
72
|
+
* @param to - Recipient email address
|
|
73
|
+
* @param props - Magic link email template props
|
|
74
|
+
* @returns Promise resolving to SentMessageInfo from nodemailer
|
|
75
|
+
* @throws Error if sending the email fails
|
|
76
|
+
*/
|
|
56
77
|
export async function sendMagicLinkEmail(
|
|
57
78
|
to: string,
|
|
58
79
|
props: MagicLinkEmailProps
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expected, user-safe action failures.
|
|
3
|
+
*
|
|
4
|
+
* Throw these for domain and authorization failures. The safe-action
|
|
5
|
+
* `handleServerError` returns {@link AppError.message} to the client and
|
|
6
|
+
* records the code on the active span. Unexpected errors stay masked.
|
|
7
|
+
*
|
|
8
|
+
* Do not put stack traces, SQL, or internal IDs in `message`.
|
|
9
|
+
*/
|
|
10
|
+
export const AppErrorCode = {
|
|
11
|
+
UNAUTHORIZED: 'UNAUTHORIZED',
|
|
12
|
+
FORBIDDEN: 'FORBIDDEN',
|
|
13
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
14
|
+
CONFLICT: 'CONFLICT',
|
|
15
|
+
QUOTA_EXCEEDED: 'QUOTA_EXCEEDED',
|
|
16
|
+
FEATURE_NOT_INCLUDED: 'FEATURE_NOT_INCLUDED',
|
|
17
|
+
BAD_REQUEST: 'BAD_REQUEST',
|
|
18
|
+
INTERNAL: 'INTERNAL'
|
|
19
|
+
} as const
|
|
20
|
+
|
|
21
|
+
export type AppErrorCode = (typeof AppErrorCode)[keyof typeof AppErrorCode]
|
|
22
|
+
|
|
23
|
+
export class AppError extends Error {
|
|
24
|
+
readonly code: AppErrorCode
|
|
25
|
+
readonly meta?: Record<string, unknown>
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
code: AppErrorCode,
|
|
29
|
+
message: string,
|
|
30
|
+
meta?: Record<string, unknown>
|
|
31
|
+
) {
|
|
32
|
+
super(message)
|
|
33
|
+
this.name = 'AppError'
|
|
34
|
+
this.code = code
|
|
35
|
+
this.meta = meta
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isAppError(error: unknown): error is AppError {
|
|
40
|
+
return error instanceof AppError
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The Postgres SQLSTATE of a failed query. Drizzle wraps driver errors and
|
|
45
|
+
* keeps the original on `cause`, so read both.
|
|
46
|
+
*/
|
|
47
|
+
function postgresCode(error: unknown, depth = 0): unknown {
|
|
48
|
+
// Drizzle adds one layer; the cap stops an error whose cause points back at
|
|
49
|
+
// itself from looping forever.
|
|
50
|
+
if (depth > 5 || typeof error !== 'object' || error === null) return undefined
|
|
51
|
+
if ('code' in error && typeof error.code === 'string') return error.code
|
|
52
|
+
return 'cause' in error ? postgresCode(error.cause, depth + 1) : undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isUniqueViolation(error: unknown): boolean {
|
|
56
|
+
return postgresCode(error) === '23505'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* True when a delete or update is refused because another row still points at
|
|
61
|
+
* the row: a foreign key violation, or a `restrict` violation for keys declared
|
|
62
|
+
* `onDelete: 'restrict'`.
|
|
63
|
+
*/
|
|
64
|
+
export function isForeignKeyViolation(error: unknown): boolean {
|
|
65
|
+
const code = postgresCode(error)
|
|
66
|
+
return code === '23503' || code === '23001'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function rethrowUniqueViolation(error: unknown, message: string): never {
|
|
70
|
+
if (isUniqueViolation(error)) {
|
|
71
|
+
throw new AppError(AppErrorCode.CONFLICT, message)
|
|
72
|
+
}
|
|
73
|
+
throw error
|
|
74
|
+
}
|
|
@@ -1,27 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
currencyCode: string,
|
|
3
|
-
num: number | string,
|
|
4
|
-
precision = 2
|
|
5
|
-
) => {
|
|
6
|
-
const numCopy = Number(num)
|
|
1
|
+
const formatters = new Map<string, Intl.NumberFormat>()
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Formats an amount in its currency, such as `$20.00` or `€20.00`.
|
|
5
|
+
* `Intl` picks the symbol and the minor digits from the ISO currency code.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* formatCurrency('USD', 20) // '$20.00'
|
|
9
|
+
*/
|
|
10
|
+
export function formatCurrency(
|
|
11
|
+
currencyCode: string,
|
|
12
|
+
value: number | string
|
|
13
|
+
): string {
|
|
14
|
+
let formatter = formatters.get(currencyCode)
|
|
15
|
+
if (!formatter) {
|
|
16
|
+
formatter = new Intl.NumberFormat('en-US', {
|
|
17
|
+
style: 'currency',
|
|
18
|
+
currency: currencyCode,
|
|
19
|
+
currencyDisplay: 'narrowSymbol',
|
|
20
|
+
signDisplay: 'negative'
|
|
21
|
+
})
|
|
22
|
+
formatters.set(currencyCode, formatter)
|
|
26
23
|
}
|
|
24
|
+
return formatter.format(Number(value))
|
|
27
25
|
}
|
|
@@ -1,12 +1,75 @@
|
|
|
1
|
+
export type DateFormatOptions = {
|
|
2
|
+
/** Abbreviated month ("Jan") instead of the full name. */
|
|
3
|
+
short?: boolean
|
|
4
|
+
/** Append the time of day. */
|
|
5
|
+
time?: boolean
|
|
6
|
+
/** Drop the year. */
|
|
7
|
+
noYear?: boolean
|
|
8
|
+
/** Render in this IANA zone instead of the viewer's. */
|
|
9
|
+
timeZone?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function asValidDate(
|
|
13
|
+
date: Date | string | number | null | undefined
|
|
14
|
+
): Date | null {
|
|
15
|
+
if (date == null) return null
|
|
16
|
+
const instant = new Date(date)
|
|
17
|
+
return Number.isFinite(instant.getTime()) ? instant : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Date and time are formatted separately and joined with a fixed " at ",
|
|
22
|
+
* because the connector ICU inserts between them changed across versions
|
|
23
|
+
* (", " in ICU 72 and later). Bun, Node, and each browser ship different ICU
|
|
24
|
+
* data, so one `toLocaleString` call renders differently per viewer and
|
|
25
|
+
* breaks hydration.
|
|
26
|
+
*/
|
|
27
|
+
function joinDateAndTime(
|
|
28
|
+
instant: Date,
|
|
29
|
+
dateOptions: Intl.DateTimeFormatOptions,
|
|
30
|
+
timeOptions: Intl.DateTimeFormatOptions,
|
|
31
|
+
timeZone: string | undefined
|
|
32
|
+
): string {
|
|
33
|
+
const zone = timeZone ? { timeZone } : {}
|
|
34
|
+
const datePart = instant.toLocaleDateString('en-US', {
|
|
35
|
+
...dateOptions,
|
|
36
|
+
...zone
|
|
37
|
+
})
|
|
38
|
+
const timePart = instant.toLocaleTimeString('en-US', {
|
|
39
|
+
...timeOptions,
|
|
40
|
+
...zone
|
|
41
|
+
})
|
|
42
|
+
return `${datePart} at ${timePart}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Formats a date as an `en-US` string, such as "March 3, 2026".
|
|
47
|
+
* Returns `-` for a missing or invalid date.
|
|
48
|
+
*/
|
|
1
49
|
export const formatDate = (
|
|
2
50
|
date: Date | string | number | null | undefined,
|
|
3
|
-
options?:
|
|
51
|
+
options?: DateFormatOptions
|
|
4
52
|
): string => {
|
|
5
|
-
|
|
6
|
-
|
|
53
|
+
const instant = asValidDate(date)
|
|
54
|
+
if (!instant) return '-'
|
|
55
|
+
|
|
56
|
+
const dateOptions: Intl.DateTimeFormatOptions = {
|
|
7
57
|
...(options?.noYear ? {} : { year: 'numeric' }),
|
|
8
58
|
month: options?.short ? 'short' : 'long',
|
|
9
|
-
day: 'numeric'
|
|
10
|
-
|
|
11
|
-
|
|
59
|
+
day: 'numeric'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!options?.time) {
|
|
63
|
+
return instant.toLocaleDateString('en-US', {
|
|
64
|
+
...dateOptions,
|
|
65
|
+
...(options?.timeZone ? { timeZone: options.timeZone } : {})
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return joinDateAndTime(
|
|
70
|
+
instant,
|
|
71
|
+
dateOptions,
|
|
72
|
+
{ hour: '2-digit', minute: '2-digit' },
|
|
73
|
+
options.timeZone
|
|
74
|
+
)
|
|
12
75
|
}
|
|
@@ -1,16 +1,26 @@
|
|
|
1
|
+
import 'server-only'
|
|
2
|
+
|
|
1
3
|
import nodemailer, { SentMessageInfo } from 'nodemailer'
|
|
2
4
|
import { env } from '~/env'
|
|
5
|
+
import { AppError, AppErrorCode } from './errors'
|
|
3
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Configure the email transporter using environment variables.
|
|
9
|
+
* Uses nodemailer for sending emails via SMTP.
|
|
10
|
+
*/
|
|
4
11
|
const transporter = nodemailer.createTransport({
|
|
5
12
|
host: env.EMAIL_SERVER_HOST,
|
|
6
|
-
port:
|
|
7
|
-
secure:
|
|
13
|
+
port: env.EMAIL_SERVER_PORT,
|
|
14
|
+
secure: env.EMAIL_SERVER_PORT === 465, // Use secure for port 465, false for other ports
|
|
8
15
|
auth: {
|
|
9
16
|
user: env.EMAIL_SERVER_USER,
|
|
10
17
|
pass: env.EMAIL_SERVER_PASSWORD
|
|
11
18
|
}
|
|
12
19
|
})
|
|
13
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Interface for the email payload
|
|
23
|
+
*/
|
|
14
24
|
interface SendEmailParams {
|
|
15
25
|
to: string
|
|
16
26
|
subject: string
|
|
@@ -18,6 +28,17 @@ interface SendEmailParams {
|
|
|
18
28
|
html?: string
|
|
19
29
|
}
|
|
20
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Sends an email using the configured transporter.
|
|
33
|
+
*
|
|
34
|
+
* @param {SendEmailParams} params - The email parameters.
|
|
35
|
+
* @param {string} params.to - The recipient's email address.
|
|
36
|
+
* @param {string} params.subject - The subject of the email.
|
|
37
|
+
* @param {string} [params.text] - The plain text content of the email.
|
|
38
|
+
* @param {string} [params.html] - The HTML content of the email (optional).
|
|
39
|
+
* @returns {Promise<any>} Resolves when the email is sent successfully.
|
|
40
|
+
* @throws {Error} If sending the email fails.
|
|
41
|
+
*/
|
|
21
42
|
export const sendEmail = async ({
|
|
22
43
|
to,
|
|
23
44
|
subject,
|
|
@@ -38,6 +59,9 @@ export const sendEmail = async ({
|
|
|
38
59
|
return info
|
|
39
60
|
} catch (error) {
|
|
40
61
|
console.error('Error sending email:', error)
|
|
41
|
-
throw new
|
|
62
|
+
throw new AppError(
|
|
63
|
+
AppErrorCode.INTERNAL,
|
|
64
|
+
'The email was not sent. Try again.'
|
|
65
|
+
)
|
|
42
66
|
}
|
|
43
67
|
}
|
|
@@ -1,17 +1,107 @@
|
|
|
1
|
-
import { SpanStatusCode, trace } from '@opentelemetry/api'
|
|
1
|
+
import { SpanStatusCode, trace, type Attributes } from '@opentelemetry/api'
|
|
2
2
|
|
|
3
|
+
const MAX_STRING_ATTR_LENGTH = 256
|
|
4
|
+
const MAX_ARRAY_ATTR_ITEMS = 20
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Helper function to retrieve the currently active span from the OpenTelemetry context.
|
|
8
|
+
* Useful for attaching additional information to the current trace.
|
|
9
|
+
*/
|
|
3
10
|
function getSpan() {
|
|
4
11
|
const span = trace.getActiveSpan()
|
|
5
12
|
return span
|
|
6
13
|
}
|
|
7
14
|
|
|
8
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Records a "wide event" with arbitrary attributes to the current span.
|
|
17
|
+
* This allows adding high-cardinality data or domain-specific context
|
|
18
|
+
* to the trace without starting a new child span.
|
|
19
|
+
*
|
|
20
|
+
* @param attributes - Key-value pairs of attributes to add to the span
|
|
21
|
+
*/
|
|
22
|
+
export function recordWideEvent(attributes: Attributes) {
|
|
9
23
|
const span = getSpan()
|
|
10
24
|
if (!span) return
|
|
11
25
|
|
|
12
26
|
span.setAttributes(attributes)
|
|
13
27
|
}
|
|
14
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Flatten a value tree into OTEL-safe attributes under `prefix`.
|
|
31
|
+
* Nested objects use dotted keys; long strings are truncated; large
|
|
32
|
+
* arrays are capped. Use for action `parsedInput` on wide events.
|
|
33
|
+
*/
|
|
34
|
+
export function flattenForWideEvent(
|
|
35
|
+
value: unknown,
|
|
36
|
+
prefix = 'input'
|
|
37
|
+
): Attributes {
|
|
38
|
+
const out: Attributes = {}
|
|
39
|
+
|
|
40
|
+
const visit = (current: unknown, path: string) => {
|
|
41
|
+
if (current === null || current === undefined) return
|
|
42
|
+
|
|
43
|
+
if (typeof current === 'string') {
|
|
44
|
+
out[path] =
|
|
45
|
+
current.length > MAX_STRING_ATTR_LENGTH
|
|
46
|
+
? `${current.slice(0, MAX_STRING_ATTR_LENGTH)}…`
|
|
47
|
+
: current
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof current === 'number' || typeof current === 'boolean') {
|
|
52
|
+
out[path] = current
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof current === 'bigint') {
|
|
57
|
+
out[path] = current.toString()
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (Array.isArray(current)) {
|
|
62
|
+
const limited = current.slice(0, MAX_ARRAY_ATTR_ITEMS)
|
|
63
|
+
const allPrimitive = limited.every(
|
|
64
|
+
item =>
|
|
65
|
+
item === null ||
|
|
66
|
+
item === undefined ||
|
|
67
|
+
typeof item === 'string' ||
|
|
68
|
+
typeof item === 'number' ||
|
|
69
|
+
typeof item === 'boolean'
|
|
70
|
+
)
|
|
71
|
+
if (allPrimitive) {
|
|
72
|
+
out[path] = limited.map(item =>
|
|
73
|
+
item === null || item === undefined ? '' : String(item)
|
|
74
|
+
)
|
|
75
|
+
if (current.length > MAX_ARRAY_ATTR_ITEMS) {
|
|
76
|
+
out[`${path}.length`] = current.length
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
out[`${path}.length`] = current.length
|
|
81
|
+
limited.forEach((item, index) => visit(item, `${path}.${index}`))
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof current === 'object') {
|
|
86
|
+
for (const [key, child] of Object.entries(
|
|
87
|
+
current as Record<string, unknown>
|
|
88
|
+
)) {
|
|
89
|
+
visit(child, path ? `${path}.${key}` : key)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
visit(value, prefix)
|
|
95
|
+
return out
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Records an exception to the current span and sets the span status to ERROR.
|
|
100
|
+
* captures the error message and stack trace if available.
|
|
101
|
+
*
|
|
102
|
+
* @param err - The error to record (can be an Error object or unknown)
|
|
103
|
+
* @returns The trace ID associated with the error, useful for correlation
|
|
104
|
+
*/
|
|
15
105
|
export function recordError(err: unknown) {
|
|
16
106
|
const span = getSpan()
|
|
17
107
|
if (!span) return
|
|
@@ -8,25 +8,52 @@ import {
|
|
|
8
8
|
} from '@tanstack/react-query'
|
|
9
9
|
import type { SafeActionResult } from 'next-safe-action'
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/** Type helper for any server action function */
|
|
12
|
+
export type AnyAction = (
|
|
12
13
|
...args: any[]
|
|
13
14
|
) => Promise<SafeActionResult<any, any, any, any, any>>
|
|
15
|
+
|
|
16
|
+
/** Extracts the input type from a server action */
|
|
14
17
|
type ActionInput<T extends AnyAction> = Parameters<T>[0]
|
|
18
|
+
|
|
19
|
+
/** Extracts the data type from a server action result */
|
|
15
20
|
type ActionData<T extends AnyAction> = Awaited<ReturnType<T>>['data']
|
|
16
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Client-side error for server action failures.
|
|
24
|
+
* Carries the user-facing `serverError` string (including unmasked AppError
|
|
25
|
+
* messages) and optional Zod validation errors.
|
|
26
|
+
*/
|
|
17
27
|
class SafeActionError extends Error {
|
|
18
28
|
constructor(
|
|
19
29
|
public message: string,
|
|
20
30
|
public validationErrors?: Record<string, string[] | undefined>
|
|
21
31
|
) {
|
|
22
32
|
super(message)
|
|
33
|
+
this.name = 'SafeActionError'
|
|
23
34
|
}
|
|
24
35
|
}
|
|
25
36
|
|
|
37
|
+
/**
|
|
38
|
+
* React Query hook for server actions that fetch data
|
|
39
|
+
* Wraps server actions with proper error handling and caching support
|
|
40
|
+
*
|
|
41
|
+
* @param key - Unique query key for caching
|
|
42
|
+
* @param action - Server action function to execute
|
|
43
|
+
* @param input - Input parameters for the server action (optional)
|
|
44
|
+
* @param options - React Query options (staleTime, refetchInterval, etc.)
|
|
45
|
+
*
|
|
46
|
+
* @returns React Query result with data, loading, and error states
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* const { data, isLoading } = useSafeActionQuery('cart-items', getCartItems)
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
26
53
|
export function useSafeActionQuery<TAction extends AnyAction>(
|
|
27
54
|
key: string,
|
|
28
55
|
action: TAction,
|
|
29
|
-
input
|
|
56
|
+
input?: ActionInput<TAction>,
|
|
30
57
|
options?: Omit<UseQueryOptions<ActionData<TAction>>, 'queryKey' | 'queryFn'>
|
|
31
58
|
) {
|
|
32
59
|
return useQuery({
|
|
@@ -39,7 +66,10 @@ export function useSafeActionQuery<TAction extends AnyAction>(
|
|
|
39
66
|
}
|
|
40
67
|
|
|
41
68
|
if (result?.validationErrors) {
|
|
42
|
-
throw new SafeActionError(
|
|
69
|
+
throw new SafeActionError(
|
|
70
|
+
'Check the highlighted fields and try again.',
|
|
71
|
+
result.validationErrors
|
|
72
|
+
)
|
|
43
73
|
}
|
|
44
74
|
|
|
45
75
|
return result?.data
|
|
@@ -48,13 +78,31 @@ export function useSafeActionQuery<TAction extends AnyAction>(
|
|
|
48
78
|
})
|
|
49
79
|
}
|
|
50
80
|
|
|
81
|
+
/**
|
|
82
|
+
* React Query mutation hook for server actions that modify data
|
|
83
|
+
* Wraps server actions with proper error handling and optimistic updates support
|
|
84
|
+
*
|
|
85
|
+
* @param action - Server action function to execute
|
|
86
|
+
* @param options - React Query mutation options (onSuccess, onError, etc.)
|
|
87
|
+
*
|
|
88
|
+
* @returns React Query mutation result with mutate function and states
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```tsx
|
|
92
|
+
* const { mutate, isPending } = useSafeActionMutation(addToCart, {
|
|
93
|
+
* onSuccess: (data) => {
|
|
94
|
+
* toastSuccessMessage('Added to cart!')
|
|
95
|
+
* queryClient.invalidateQueries({ queryKey: ['cart-items'] })
|
|
96
|
+
* },
|
|
97
|
+
* onError: (error) => {
|
|
98
|
+
* toastActionError(error, 'Failed to add to cart')
|
|
99
|
+
* }
|
|
100
|
+
* })
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
51
103
|
export function useSafeActionMutation<TAction extends AnyAction>(
|
|
52
104
|
action: TAction,
|
|
53
|
-
options?: UseMutationOptions<
|
|
54
|
-
ActionData<TAction>,
|
|
55
|
-
SafeActionError,
|
|
56
|
-
ActionInput<TAction>
|
|
57
|
-
>
|
|
105
|
+
options?: UseMutationOptions<ActionData<TAction>, Error, ActionInput<TAction>>
|
|
58
106
|
) {
|
|
59
107
|
return useMutation({
|
|
60
108
|
mutationFn: async (input: ActionInput<TAction>) => {
|
|
@@ -65,7 +113,10 @@ export function useSafeActionMutation<TAction extends AnyAction>(
|
|
|
65
113
|
}
|
|
66
114
|
|
|
67
115
|
if (result?.validationErrors) {
|
|
68
|
-
throw new SafeActionError(
|
|
116
|
+
throw new SafeActionError(
|
|
117
|
+
'Check the highlighted fields and try again.',
|
|
118
|
+
result.validationErrors
|
|
119
|
+
)
|
|
69
120
|
}
|
|
70
121
|
|
|
71
122
|
return result?.data
|