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.
- package/README.md +125 -226
- package/cli.ts +83 -28
- 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 -0
- package/template/nextjs/next.config.ts +8 -1
- package/template/nextjs/package.json +47 -40
- 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 +8 -0
- package/template/saas-starter/.env.example +25 -0
- package/template/saas-starter/.vscode/extensions.json +11 -0
- package/template/saas-starter/.vscode/settings.json +3 -0
- package/template/saas-starter/AGENTS.md +302 -0
- package/template/saas-starter/CLAUDE.md +1 -0
- package/template/saas-starter/Dockerfile +15 -0
- package/template/saas-starter/README.md +125 -0
- package/template/saas-starter/bunfig.toml +3 -0
- package/template/saas-starter/components.json +25 -0
- package/template/saas-starter/drizzle.config.ts +10 -0
- package/template/saas-starter/eslint.config.mjs +24 -0
- package/template/saas-starter/gitignore +14 -0
- package/template/saas-starter/next.config.ts +32 -0
- package/template/saas-starter/package.json +97 -0
- package/template/saas-starter/postcss.config.mjs +7 -0
- package/template/saas-starter/prettier.config.mjs +11 -0
- package/template/saas-starter/public/logo-dark.svg +4 -0
- package/template/saas-starter/public/logo.svg +4 -0
- package/template/saas-starter/public/placeholder.svg +1 -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 +183 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
- 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 +53 -0
- package/template/saas-starter/src/app/(admin)/admin/page.tsx +60 -0
- 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 +377 -0
- 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 +216 -0
- 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 +189 -0
- 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 +17 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +31 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +92 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +357 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +130 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +222 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +115 -0
- 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 +168 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +604 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +33 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +55 -0
- 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 +31 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +26 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +30 -0
- 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 +37 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +200 -0
- package/template/saas-starter/src/app/(app)/app/layout.tsx +64 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +117 -0
- package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +40 -0
- package/template/saas-starter/src/app/(app)/app/page.tsx +76 -0
- 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 +131 -0
- package/template/saas-starter/src/app/(auth)/login/page.tsx +105 -0
- package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +137 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +94 -0
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +153 -0
- 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 +48 -0
- package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
- package/template/saas-starter/src/app/(marketing)/page.tsx +24 -0
- package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +63 -0
- package/template/saas-starter/src/app/(marketing)/terms/page.tsx +41 -0
- 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/api/auth/[...all]/route.ts +4 -0
- package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
- package/template/saas-starter/src/app/error.tsx +37 -0
- package/template/saas-starter/src/app/favicon.ico +0 -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 +269 -0
- package/template/saas-starter/src/app/layout.tsx +77 -0
- package/template/saas-starter/src/app/not-found.tsx +49 -0
- package/template/saas-starter/src/app/robots.ts +36 -0
- package/template/saas-starter/src/app/sitemap.ts +21 -0
- package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
- package/template/saas-starter/src/components/dashboard-header.tsx +70 -0
- package/template/saas-starter/src/components/data-table/enum.ts +5 -0
- package/template/saas-starter/src/components/data-table/features.ts +49 -0
- package/template/saas-starter/src/components/data-table/index.tsx +961 -0
- package/template/saas-starter/src/components/data-table/types.ts +76 -0
- package/template/saas-starter/src/components/error.tsx +71 -0
- package/template/saas-starter/src/components/file-upload.tsx +254 -0
- package/template/saas-starter/src/components/loader.tsx +41 -0
- package/template/saas-starter/src/components/logo.tsx +49 -0
- package/template/saas-starter/src/components/navigation-search.tsx +140 -0
- package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
- package/template/saas-starter/src/components/page-heading.tsx +38 -0
- package/template/saas-starter/src/components/react-query-provider.tsx +51 -0
- package/template/saas-starter/src/components/route-error.tsx +55 -0
- package/template/saas-starter/src/components/screen-size.tsx +76 -0
- package/template/saas-starter/src/components/site-footer.tsx +39 -0
- 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 +115 -0
- package/template/saas-starter/src/components/theme-toggle.tsx +75 -0
- package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
- package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
- package/template/saas-starter/src/components/ui/badge.tsx +54 -0
- package/template/saas-starter/src/components/ui/button.tsx +69 -0
- package/template/saas-starter/src/components/ui/card.tsx +103 -0
- package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
- package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
- package/template/saas-starter/src/components/ui/command.tsx +192 -0
- package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
- package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/saas-starter/src/components/ui/field.tsx +238 -0
- package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
- package/template/saas-starter/src/components/ui/input.tsx +19 -0
- package/template/saas-starter/src/components/ui/label.tsx +24 -0
- package/template/saas-starter/src/components/ui/popover.tsx +89 -0
- package/template/saas-starter/src/components/ui/select.tsx +195 -0
- package/template/saas-starter/src/components/ui/separator.tsx +28 -0
- package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
- package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
- package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
- package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
- package/template/saas-starter/src/components/ui/switch.tsx +33 -0
- package/template/saas-starter/src/components/ui/table.tsx +116 -0
- package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
- package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
- package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
- package/template/saas-starter/src/components/user-menu.tsx +213 -0
- package/template/saas-starter/src/db/common.ts +63 -0
- package/template/saas-starter/src/db/enums.ts +50 -0
- package/template/saas-starter/src/db/index.ts +60 -0
- package/template/saas-starter/src/db/relations.ts +112 -0
- package/template/saas-starter/src/db/schema.ts +342 -0
- package/template/saas-starter/src/emails/components/email-layout.tsx +79 -0
- package/template/saas-starter/src/emails/invitation.tsx +69 -0
- package/template/saas-starter/src/emails/magic-link.tsx +51 -0
- package/template/saas-starter/src/emails/test.tsx +34 -0
- package/template/saas-starter/src/emails/welcome.tsx +37 -0
- package/template/saas-starter/src/env.ts +66 -0
- package/template/saas-starter/src/hooks/use-mobile.ts +21 -0
- package/template/saas-starter/src/instrumentation.ts +44 -0
- package/template/saas-starter/src/lib/actions.ts +55 -0
- package/template/saas-starter/src/lib/admin-navigation.ts +63 -0
- package/template/saas-starter/src/lib/app-navigation.ts +63 -0
- package/template/saas-starter/src/lib/auth-client.ts +34 -0
- package/template/saas-starter/src/lib/auth-permissions.ts +61 -0
- package/template/saas-starter/src/lib/auth.ts +312 -0
- package/template/saas-starter/src/lib/billing.ts +152 -0
- package/template/saas-starter/src/lib/callback-url.ts +28 -0
- package/template/saas-starter/src/lib/cn.ts +4 -0
- package/template/saas-starter/src/lib/constants.ts +91 -0
- package/template/saas-starter/src/lib/delay.ts +12 -0
- package/template/saas-starter/src/lib/email-service.ts +118 -0
- package/template/saas-starter/src/lib/errors.ts +74 -0
- package/template/saas-starter/src/lib/format-currency.ts +25 -0
- package/template/saas-starter/src/lib/format-date.ts +75 -0
- package/template/saas-starter/src/lib/image-url.ts +54 -0
- package/template/saas-starter/src/lib/nodemailer.ts +67 -0
- package/template/saas-starter/src/lib/organization-access.ts +73 -0
- package/template/saas-starter/src/lib/otel.ts +117 -0
- package/template/saas-starter/src/lib/plan-limits.ts +37 -0
- package/template/saas-starter/src/lib/plans.ts +90 -0
- package/template/saas-starter/src/lib/rbac.ts +44 -0
- package/template/saas-starter/src/lib/safe-action-client.ts +126 -0
- package/template/saas-starter/src/lib/safe-action.ts +187 -0
- 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 +20 -0
- package/template/saas-starter/src/lib/storage.ts +311 -0
- package/template/saas-starter/src/lib/stripe.ts +9 -0
- package/template/saas-starter/src/lib/toast-message.ts +101 -0
- package/template/saas-starter/src/lib/validations.ts +95 -0
- 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/reset-db.ts +22 -0
- package/template/saas-starter/src/scripts/seed.ts +85 -0
- package/template/saas-starter/src/scripts/setup-db.ts +72 -0
- package/template/saas-starter/src/scripts/setup-stripe-plans.ts +206 -0
- package/template/saas-starter/src/scripts/test-mail.ts +43 -0
- 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 +52 -0
- package/cli.js +0 -143
- 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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ArrowLeft } from 'lucide-react'
|
|
2
|
+
import Link from 'next/link'
|
|
3
|
+
import { Button } from '~/components/ui/button'
|
|
4
|
+
import { getOrganizationsCached } from '../../actions/organizations'
|
|
5
|
+
import { CreateOrganizationForm } from './_components/create-organization-form'
|
|
6
|
+
|
|
7
|
+
export default async function OnboardingPage() {
|
|
8
|
+
const organizations = (await getOrganizationsCached())?.data ?? []
|
|
9
|
+
const isFirstOrg = organizations.length === 0
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="mx-auto flex w-full max-w-xl flex-col gap-8 px-2 py-8">
|
|
13
|
+
<div className="flex flex-col gap-2">
|
|
14
|
+
{isFirstOrg ? null : (
|
|
15
|
+
<Button
|
|
16
|
+
asChild
|
|
17
|
+
variant="ghost"
|
|
18
|
+
size="sm"
|
|
19
|
+
className="text-muted-foreground -ml-2 w-fit"
|
|
20
|
+
>
|
|
21
|
+
<Link href="/app">
|
|
22
|
+
<ArrowLeft />
|
|
23
|
+
Back to Organizations
|
|
24
|
+
</Link>
|
|
25
|
+
</Button>
|
|
26
|
+
)}
|
|
27
|
+
<h1 className="text-3xl font-bold tracking-tight">
|
|
28
|
+
{isFirstOrg ? 'Create Your Organization' : 'Create an Organization'}
|
|
29
|
+
</h1>
|
|
30
|
+
<p className="text-muted-foreground">
|
|
31
|
+
{isFirstOrg
|
|
32
|
+
? 'Set up an organization to get started. You can invite teammates next.'
|
|
33
|
+
: 'Each organization has its own members, settings, and billing.'}
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<CreateOrganizationForm />
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Building2, Plus } from 'lucide-react'
|
|
2
|
+
import Image from 'next/image'
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { redirect } from 'next/navigation'
|
|
5
|
+
import { Card, CardHeader, CardTitle } from '~/components/ui/card'
|
|
6
|
+
import { siteConfig } from '~/lib/siteConfig'
|
|
7
|
+
import { getOrganizationsCached } from '../actions/organizations'
|
|
8
|
+
|
|
9
|
+
export default async function AppPage() {
|
|
10
|
+
const organizations = (await getOrganizationsCached())?.data ?? []
|
|
11
|
+
|
|
12
|
+
if (organizations.length === 0) {
|
|
13
|
+
return redirect('/app/onboarding')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="mx-auto flex w-full max-w-4xl flex-col gap-8 px-4 py-12">
|
|
18
|
+
<div className="flex flex-col gap-2">
|
|
19
|
+
<h1 className="text-4xl font-bold tracking-tight">
|
|
20
|
+
Welcome to {siteConfig.name}
|
|
21
|
+
</h1>
|
|
22
|
+
<p className="text-muted-foreground text-lg">
|
|
23
|
+
Choose an organization to open, or create a new one.
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
28
|
+
{organizations.map(org => (
|
|
29
|
+
<Link
|
|
30
|
+
key={org.id}
|
|
31
|
+
href={`/app/${org.id}/dashboard`}
|
|
32
|
+
className="group"
|
|
33
|
+
>
|
|
34
|
+
<Card className="hover:border-primary h-full cursor-pointer transition-all duration-200 hover:shadow-md">
|
|
35
|
+
<CardHeader className="flex flex-row items-center gap-4">
|
|
36
|
+
{org.logoUrl ? (
|
|
37
|
+
<div className="relative size-10 shrink-0 overflow-hidden rounded-lg">
|
|
38
|
+
<Image
|
|
39
|
+
src={org.logoUrl}
|
|
40
|
+
alt={org.name}
|
|
41
|
+
fill
|
|
42
|
+
sizes="40px"
|
|
43
|
+
className="object-cover"
|
|
44
|
+
unoptimized={org.logoUrl.startsWith('data:')}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
) : (
|
|
48
|
+
<div className="bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground flex size-10 shrink-0 items-center justify-center rounded-lg transition-colors">
|
|
49
|
+
<Building2 className="size-5" />
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
<div className="min-w-0 flex-1">
|
|
53
|
+
<CardTitle className="truncate text-lg">{org.name}</CardTitle>
|
|
54
|
+
<p className="text-muted-foreground truncate text-xs capitalize">
|
|
55
|
+
{org.role}
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
</CardHeader>
|
|
59
|
+
</Card>
|
|
60
|
+
</Link>
|
|
61
|
+
))}
|
|
62
|
+
|
|
63
|
+
<Link href="/app/onboarding">
|
|
64
|
+
<Card className="hover:border-primary hover:bg-muted/50 flex h-full cursor-pointer flex-col items-center justify-center border-dashed py-8 transition-all">
|
|
65
|
+
<div className="bg-muted mb-2 flex size-10 items-center justify-center rounded-full">
|
|
66
|
+
<Plus className="size-5" />
|
|
67
|
+
</div>
|
|
68
|
+
<div className="text-muted-foreground font-semibold">
|
|
69
|
+
Create Organization
|
|
70
|
+
</div>
|
|
71
|
+
</Card>
|
|
72
|
+
</Link>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ArrowLeft } from 'lucide-react'
|
|
2
|
+
import type { Metadata } from 'next'
|
|
3
|
+
import { headers } from 'next/headers'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { redirect } from 'next/navigation'
|
|
6
|
+
import { NuqsAdapter } from 'nuqs/adapters/next/app'
|
|
7
|
+
import { Logo } from '~/components/logo'
|
|
8
|
+
import { SiteFooter } from '~/components/site-footer'
|
|
9
|
+
import { Button } from '~/components/ui/button'
|
|
10
|
+
import { UserMenu } from '~/components/user-menu'
|
|
11
|
+
import { getAuthSession } from '~/lib/auth'
|
|
12
|
+
import { sanitizeCallbackUrl } from '~/lib/callback-url'
|
|
13
|
+
import { privatePageRobots } from '~/lib/seo'
|
|
14
|
+
import { siteConfig } from '~/lib/siteConfig'
|
|
15
|
+
|
|
16
|
+
export const metadata: Metadata = {
|
|
17
|
+
robots: privatePageRobots
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default async function AccountLayout({
|
|
21
|
+
children
|
|
22
|
+
}: {
|
|
23
|
+
children: React.ReactNode
|
|
24
|
+
}) {
|
|
25
|
+
const authSession = await getAuthSession()
|
|
26
|
+
|
|
27
|
+
if (!authSession) {
|
|
28
|
+
const headerStore = await headers()
|
|
29
|
+
const callbackUrl =
|
|
30
|
+
sanitizeCallbackUrl(headerStore.get('x-pathname') ?? undefined) ??
|
|
31
|
+
'/profile'
|
|
32
|
+
redirect(`/login?callbackUrl=${encodeURIComponent(callbackUrl)}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<NuqsAdapter>
|
|
37
|
+
<div className="flex min-h-screen flex-col">
|
|
38
|
+
<header className="bg-background sticky top-0 z-50 flex h-14 shrink-0 items-center gap-2 border-b px-4">
|
|
39
|
+
<Link href="/" className="flex shrink-0 items-center">
|
|
40
|
+
<Logo size={32} priority className="text-lg" />
|
|
41
|
+
</Link>
|
|
42
|
+
|
|
43
|
+
<div className="flex flex-1 items-center justify-end gap-2">
|
|
44
|
+
<Button asChild variant="ghost" size="sm">
|
|
45
|
+
<Link href="/app">
|
|
46
|
+
<ArrowLeft className="size-4" />
|
|
47
|
+
Back to App
|
|
48
|
+
</Link>
|
|
49
|
+
</Button>
|
|
50
|
+
<UserMenu
|
|
51
|
+
user={authSession.user}
|
|
52
|
+
isSuperAdmin={authSession.isSuperAdmin}
|
|
53
|
+
isImpersonating={!!authSession.session.impersonatedBy}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
</header>
|
|
57
|
+
|
|
58
|
+
<main className="flex-1">{children}</main>
|
|
59
|
+
|
|
60
|
+
<SiteFooter name={siteConfig.name} />
|
|
61
|
+
</div>
|
|
62
|
+
</NuqsAdapter>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { UserCog } from 'lucide-react'
|
|
2
|
+
import { PageHeading } from '~/components/page-heading'
|
|
3
|
+
import { ProfileForm } from './profile-form'
|
|
4
|
+
|
|
5
|
+
export default function ProfilePage() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="mx-auto w-full max-w-3xl space-y-6 p-4 sm:p-6">
|
|
8
|
+
<PageHeading
|
|
9
|
+
title="Your Profile"
|
|
10
|
+
description="Update your name and review your account details."
|
|
11
|
+
icon={UserCog}
|
|
12
|
+
/>
|
|
13
|
+
<ProfileForm />
|
|
14
|
+
</div>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
4
|
+
import { Mail, ShieldCheck } from 'lucide-react'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { Controller, useForm } from 'react-hook-form'
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'
|
|
9
|
+
import { Badge } from '~/components/ui/badge'
|
|
10
|
+
import { Button } from '~/components/ui/button'
|
|
11
|
+
import {
|
|
12
|
+
Card,
|
|
13
|
+
CardContent,
|
|
14
|
+
CardDescription,
|
|
15
|
+
CardHeader,
|
|
16
|
+
CardTitle
|
|
17
|
+
} from '~/components/ui/card'
|
|
18
|
+
import { Field, FieldError, FieldLabel } from '~/components/ui/field'
|
|
19
|
+
import { Input } from '~/components/ui/input'
|
|
20
|
+
import { Skeleton } from '~/components/ui/skeleton'
|
|
21
|
+
import { updateUser, useSession } from '~/lib/auth-client'
|
|
22
|
+
import {
|
|
23
|
+
toastActionError,
|
|
24
|
+
toastErrorMessage,
|
|
25
|
+
toastSuccessMessage
|
|
26
|
+
} from '~/lib/toast-message'
|
|
27
|
+
import { stringValidation } from '~/lib/validations'
|
|
28
|
+
|
|
29
|
+
const profileFormSchema = z.object({
|
|
30
|
+
name: stringValidation
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
type ProfileFormValues = z.infer<typeof profileFormSchema>
|
|
34
|
+
|
|
35
|
+
export function ProfileForm() {
|
|
36
|
+
const { data: session, isPending, refetch } = useSession()
|
|
37
|
+
|
|
38
|
+
if (isPending) {
|
|
39
|
+
return <ProfileFormSkeleton />
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!session?.user) {
|
|
43
|
+
return (
|
|
44
|
+
<Card>
|
|
45
|
+
<CardContent className="py-8 text-center">
|
|
46
|
+
<p className="text-muted-foreground text-sm">
|
|
47
|
+
You are not signed in.
|
|
48
|
+
</p>
|
|
49
|
+
<Button asChild className="mt-4">
|
|
50
|
+
<Link href="/login?callbackUrl=/profile">Sign In</Link>
|
|
51
|
+
</Button>
|
|
52
|
+
</CardContent>
|
|
53
|
+
</Card>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return <ProfileFormInner user={session.user} onUpdated={refetch} />
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function ProfileFormInner({
|
|
61
|
+
user,
|
|
62
|
+
onUpdated
|
|
63
|
+
}: {
|
|
64
|
+
user: {
|
|
65
|
+
name: string
|
|
66
|
+
email: string
|
|
67
|
+
image?: string | null
|
|
68
|
+
emailVerified?: boolean
|
|
69
|
+
role?: string | null
|
|
70
|
+
}
|
|
71
|
+
onUpdated: () => Promise<void>
|
|
72
|
+
}) {
|
|
73
|
+
const form = useForm<ProfileFormValues>({
|
|
74
|
+
resolver: zodResolver(profileFormSchema),
|
|
75
|
+
defaultValues: {
|
|
76
|
+
name: user.name ?? ''
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const isSubmitting = form.formState.isSubmitting
|
|
81
|
+
const isDirty = form.formState.isDirty
|
|
82
|
+
|
|
83
|
+
const initials =
|
|
84
|
+
user.name
|
|
85
|
+
?.split(' ')
|
|
86
|
+
.slice(0, 2)
|
|
87
|
+
.map(part => part[0])
|
|
88
|
+
.join('')
|
|
89
|
+
.toUpperCase() || 'NA'
|
|
90
|
+
|
|
91
|
+
async function onSubmit(values: ProfileFormValues) {
|
|
92
|
+
if (values.name.trim() === user.name) return
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const res = await updateUser({ name: values.name.trim() })
|
|
96
|
+
if (res?.error) throw res.error
|
|
97
|
+
await onUpdated()
|
|
98
|
+
form.reset({ name: values.name.trim() })
|
|
99
|
+
toastSuccessMessage('Profile updated')
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error('Failed to update profile:', err)
|
|
102
|
+
if (
|
|
103
|
+
typeof err === 'object' &&
|
|
104
|
+
err &&
|
|
105
|
+
'message' in err &&
|
|
106
|
+
typeof err.message === 'string'
|
|
107
|
+
)
|
|
108
|
+
toastActionError(err)
|
|
109
|
+
else toastErrorMessage('Your profile was not saved. Try again.')
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="space-y-6">
|
|
115
|
+
<Card>
|
|
116
|
+
<CardHeader>
|
|
117
|
+
<CardTitle>Account</CardTitle>
|
|
118
|
+
<CardDescription>
|
|
119
|
+
Details from your account. Only your name can change here.
|
|
120
|
+
</CardDescription>
|
|
121
|
+
</CardHeader>
|
|
122
|
+
<CardContent className="space-y-6">
|
|
123
|
+
<div className="flex items-center gap-4">
|
|
124
|
+
<Avatar className="size-16">
|
|
125
|
+
<AvatarFallback className="text-foreground text-lg">
|
|
126
|
+
{initials}
|
|
127
|
+
</AvatarFallback>
|
|
128
|
+
{user.image ? (
|
|
129
|
+
<AvatarImage src={user.image} alt={user.name} />
|
|
130
|
+
) : null}
|
|
131
|
+
</Avatar>
|
|
132
|
+
<div className="space-y-1">
|
|
133
|
+
<p className="text-base font-medium">{user.name}</p>
|
|
134
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
135
|
+
{user.emailVerified ? (
|
|
136
|
+
<Badge variant="success" className="gap-1">
|
|
137
|
+
<ShieldCheck className="size-3" />
|
|
138
|
+
Email verified
|
|
139
|
+
</Badge>
|
|
140
|
+
) : null}
|
|
141
|
+
{user.role ? (
|
|
142
|
+
<Badge variant="outline" className="capitalize">
|
|
143
|
+
{user.role}
|
|
144
|
+
</Badge>
|
|
145
|
+
) : null}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<DetailRow icon={Mail} label="Email" value={user.email} />
|
|
151
|
+
</CardContent>
|
|
152
|
+
</Card>
|
|
153
|
+
|
|
154
|
+
<Card>
|
|
155
|
+
<CardHeader>
|
|
156
|
+
<CardTitle>Edit Profile</CardTitle>
|
|
157
|
+
<CardDescription>Update your display name.</CardDescription>
|
|
158
|
+
</CardHeader>
|
|
159
|
+
<CardContent>
|
|
160
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
161
|
+
<Controller
|
|
162
|
+
control={form.control}
|
|
163
|
+
name="name"
|
|
164
|
+
render={({ field, fieldState }) => (
|
|
165
|
+
<Field data-invalid={fieldState.invalid}>
|
|
166
|
+
<FieldLabel htmlFor={field.name}>Name</FieldLabel>
|
|
167
|
+
<Input
|
|
168
|
+
id={field.name}
|
|
169
|
+
placeholder="Your full name"
|
|
170
|
+
autoComplete="name"
|
|
171
|
+
aria-invalid={fieldState.invalid}
|
|
172
|
+
{...field}
|
|
173
|
+
/>
|
|
174
|
+
<FieldError
|
|
175
|
+
errors={fieldState.error ? [fieldState.error] : undefined}
|
|
176
|
+
/>
|
|
177
|
+
</Field>
|
|
178
|
+
)}
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
<div className="flex justify-end">
|
|
182
|
+
<Button type="submit" disabled={isSubmitting || !isDirty}>
|
|
183
|
+
{isSubmitting ? 'Saving…' : 'Save'}
|
|
184
|
+
</Button>
|
|
185
|
+
</div>
|
|
186
|
+
</form>
|
|
187
|
+
</CardContent>
|
|
188
|
+
</Card>
|
|
189
|
+
</div>
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function DetailRow({
|
|
194
|
+
icon: Icon,
|
|
195
|
+
label,
|
|
196
|
+
value
|
|
197
|
+
}: {
|
|
198
|
+
icon: React.FC<{ className?: string }>
|
|
199
|
+
label: string
|
|
200
|
+
value: string
|
|
201
|
+
}) {
|
|
202
|
+
return (
|
|
203
|
+
<div className="flex items-start gap-3 rounded-lg border p-3">
|
|
204
|
+
<div className="bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-md">
|
|
205
|
+
<Icon className="size-4" />
|
|
206
|
+
</div>
|
|
207
|
+
<div className="min-w-0 space-y-0.5">
|
|
208
|
+
<p className="text-muted-foreground text-xs tracking-wide uppercase">
|
|
209
|
+
{label}
|
|
210
|
+
</p>
|
|
211
|
+
<p className="text-sm wrap-break-word">{value}</p>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function ProfileFormSkeleton() {
|
|
218
|
+
return (
|
|
219
|
+
<div className="space-y-6">
|
|
220
|
+
<Card>
|
|
221
|
+
<CardHeader>
|
|
222
|
+
<Skeleton className="h-5 w-32" />
|
|
223
|
+
<Skeleton className="mt-2 h-4 w-64" />
|
|
224
|
+
</CardHeader>
|
|
225
|
+
<CardContent className="space-y-6">
|
|
226
|
+
<div className="flex items-center gap-4">
|
|
227
|
+
<Skeleton className="size-16 rounded-full" />
|
|
228
|
+
<div className="space-y-2">
|
|
229
|
+
<Skeleton className="h-4 w-40" />
|
|
230
|
+
<Skeleton className="h-4 w-24" />
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
<Skeleton className="h-16 w-full" />
|
|
234
|
+
</CardContent>
|
|
235
|
+
</Card>
|
|
236
|
+
<Card>
|
|
237
|
+
<CardHeader>
|
|
238
|
+
<Skeleton className="h-5 w-32" />
|
|
239
|
+
</CardHeader>
|
|
240
|
+
<CardContent className="space-y-4">
|
|
241
|
+
<Skeleton className="h-10 w-full" />
|
|
242
|
+
<Skeleton className="h-10 w-32 self-end" />
|
|
243
|
+
</CardContent>
|
|
244
|
+
</Card>
|
|
245
|
+
</div>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SupportTicketConversation } from '~/components/support/support-ticket-conversation'
|
|
2
|
+
|
|
3
|
+
export default async function AccountSupportTicketPage({
|
|
4
|
+
params
|
|
5
|
+
}: {
|
|
6
|
+
params: Promise<{ ticketId: string }>
|
|
7
|
+
}) {
|
|
8
|
+
const { ticketId } = await params
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="mx-auto w-full max-w-5xl p-4 sm:p-6">
|
|
12
|
+
<SupportTicketConversation
|
|
13
|
+
mode="app"
|
|
14
|
+
ticketId={ticketId}
|
|
15
|
+
backHref="/support"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SupportTicketsTable } from '~/components/support/support-tickets-table'
|
|
2
|
+
|
|
3
|
+
export default function AccountSupportPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="mx-auto w-full max-w-5xl p-4 sm:p-6">
|
|
6
|
+
<SupportTicketsTable mode="account" basePath="/support" />
|
|
7
|
+
</div>
|
|
8
|
+
)
|
|
9
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email-based login form with magic link
|
|
3
|
+
*
|
|
4
|
+
* Sends a passwordless authentication link to the user's email.
|
|
5
|
+
* Shows success message after link is sent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client'
|
|
9
|
+
|
|
10
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
11
|
+
import { Mail } from 'lucide-react'
|
|
12
|
+
import { useState } from 'react'
|
|
13
|
+
import { Controller, useForm } from 'react-hook-form'
|
|
14
|
+
import { z } from 'zod'
|
|
15
|
+
import { Button } from '~/components/ui/button'
|
|
16
|
+
import { Field, FieldError, FieldLabel } from '~/components/ui/field'
|
|
17
|
+
import { Input } from '~/components/ui/input'
|
|
18
|
+
import { signIn } from '~/lib/auth-client'
|
|
19
|
+
import {
|
|
20
|
+
toastActionError,
|
|
21
|
+
toastErrorMessage,
|
|
22
|
+
toastSuccessMessage
|
|
23
|
+
} from '~/lib/toast-message'
|
|
24
|
+
import { emailValidation } from '~/lib/validations'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Email login form schema
|
|
28
|
+
* Validates email format
|
|
29
|
+
*/
|
|
30
|
+
const emailLoginFormSchema = z.object({
|
|
31
|
+
email: emailValidation
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Email Login Form Component
|
|
36
|
+
*
|
|
37
|
+
* Allows users to sign in via magic link sent to their email.
|
|
38
|
+
* Shows confirmation message after sending the link.
|
|
39
|
+
*
|
|
40
|
+
* **States:**
|
|
41
|
+
* - Form: Ready for email input
|
|
42
|
+
* - Submitted: Shows confirmation after sending link
|
|
43
|
+
*
|
|
44
|
+
* @returns {JSX.Element} Email input form or confirmation message
|
|
45
|
+
*/
|
|
46
|
+
export const EmailLoginForm = ({
|
|
47
|
+
callbackUrl
|
|
48
|
+
}: {
|
|
49
|
+
callbackUrl?: string | null
|
|
50
|
+
}) => {
|
|
51
|
+
const form = useForm<z.infer<typeof emailLoginFormSchema>>({
|
|
52
|
+
resolver: zodResolver(emailLoginFormSchema),
|
|
53
|
+
defaultValues: {
|
|
54
|
+
email: ''
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
59
|
+
const [isSent, setIsSent] = useState('')
|
|
60
|
+
|
|
61
|
+
async function onSubmit(values: z.infer<typeof emailLoginFormSchema>) {
|
|
62
|
+
const email = values.email
|
|
63
|
+
|
|
64
|
+
if (!email) return
|
|
65
|
+
|
|
66
|
+
setIsLoading(true)
|
|
67
|
+
try {
|
|
68
|
+
const res = await signIn.magicLink({
|
|
69
|
+
email,
|
|
70
|
+
name: '',
|
|
71
|
+
callbackURL: callbackUrl || '/login'
|
|
72
|
+
})
|
|
73
|
+
if (res?.error) throw res.error
|
|
74
|
+
setIsSent(email)
|
|
75
|
+
toastSuccessMessage('Magic link sent. Check your email.')
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error('Error sending magic link:', err)
|
|
78
|
+
if (
|
|
79
|
+
typeof err === 'object' &&
|
|
80
|
+
err &&
|
|
81
|
+
'message' in err &&
|
|
82
|
+
typeof err.message === 'string'
|
|
83
|
+
)
|
|
84
|
+
toastActionError(err)
|
|
85
|
+
else toastErrorMessage('The magic link was not sent. Try again.')
|
|
86
|
+
} finally {
|
|
87
|
+
setIsLoading(false)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isSent) {
|
|
92
|
+
return (
|
|
93
|
+
<div className="rounded-lg border p-4 text-center">
|
|
94
|
+
<Mail className="text-primary mx-auto mb-2 size-6" />
|
|
95
|
+
<h3 className="mb-1 text-sm font-medium">Check Your Email</h3>
|
|
96
|
+
<p className="text-muted-foreground text-xs">
|
|
97
|
+
We've sent a magic link to <strong>{isSent}</strong>
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
105
|
+
<Controller
|
|
106
|
+
control={form.control}
|
|
107
|
+
name="email"
|
|
108
|
+
render={({ field, fieldState }) => (
|
|
109
|
+
<Field data-invalid={fieldState.invalid}>
|
|
110
|
+
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
|
111
|
+
<Input
|
|
112
|
+
id={field.name}
|
|
113
|
+
placeholder="name@example.com"
|
|
114
|
+
autoCapitalize="off"
|
|
115
|
+
autoComplete="email"
|
|
116
|
+
autoFocus
|
|
117
|
+
aria-invalid={fieldState.invalid}
|
|
118
|
+
{...field}
|
|
119
|
+
/>
|
|
120
|
+
<FieldError
|
|
121
|
+
errors={fieldState.error ? [fieldState.error] : undefined}
|
|
122
|
+
/>
|
|
123
|
+
</Field>
|
|
124
|
+
)}
|
|
125
|
+
/>
|
|
126
|
+
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
127
|
+
{isLoading ? 'Sending…' : 'Send Magic Link'}
|
|
128
|
+
</Button>
|
|
129
|
+
</form>
|
|
130
|
+
)
|
|
131
|
+
}
|