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,241 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ChevronRight } from 'lucide-react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { usePathname } from 'next/navigation'
|
|
6
|
+
import { ComponentType, ReactNode } from 'react'
|
|
7
|
+
import {
|
|
8
|
+
Collapsible,
|
|
9
|
+
CollapsibleContent,
|
|
10
|
+
CollapsibleTrigger
|
|
11
|
+
} from '~/components/ui/collapsible'
|
|
12
|
+
import {
|
|
13
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuContent,
|
|
15
|
+
DropdownMenuItem,
|
|
16
|
+
DropdownMenuLabel,
|
|
17
|
+
DropdownMenuSeparator,
|
|
18
|
+
DropdownMenuTrigger
|
|
19
|
+
} from '~/components/ui/dropdown-menu'
|
|
20
|
+
import {
|
|
21
|
+
Sidebar,
|
|
22
|
+
SidebarContent,
|
|
23
|
+
SidebarGroup,
|
|
24
|
+
SidebarGroupContent,
|
|
25
|
+
SidebarGroupLabel,
|
|
26
|
+
SidebarHeader,
|
|
27
|
+
SidebarMenu,
|
|
28
|
+
SidebarMenuButton,
|
|
29
|
+
SidebarMenuItem,
|
|
30
|
+
SidebarMenuSub,
|
|
31
|
+
SidebarMenuSubButton,
|
|
32
|
+
SidebarMenuSubItem,
|
|
33
|
+
SidebarRail,
|
|
34
|
+
useSidebar
|
|
35
|
+
} from '~/components/ui/sidebar'
|
|
36
|
+
|
|
37
|
+
type NavigationSidebarProps = {
|
|
38
|
+
header: ReactNode
|
|
39
|
+
groups: SidebarNavGroup[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type SidebarNavIcon = ComponentType<{ className?: string }>
|
|
43
|
+
|
|
44
|
+
export type SidebarNavSubItem = {
|
|
45
|
+
title: string
|
|
46
|
+
url: string
|
|
47
|
+
exact?: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type SidebarNavItem = {
|
|
51
|
+
title: string
|
|
52
|
+
url: string
|
|
53
|
+
icon?: SidebarNavIcon
|
|
54
|
+
exact?: boolean
|
|
55
|
+
items?: SidebarNavSubItem[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type SidebarNavGroup = {
|
|
59
|
+
label: string
|
|
60
|
+
items: SidebarNavItem[]
|
|
61
|
+
/** When true, the group is only shown to superadmins. */
|
|
62
|
+
superadminOnly?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function NavigationSidebar({ header, groups }: NavigationSidebarProps) {
|
|
66
|
+
const pathname = usePathname()
|
|
67
|
+
const { state, isMobile, setOpenMobile } = useSidebar()
|
|
68
|
+
const isCollapsed = state === 'collapsed' && !isMobile
|
|
69
|
+
|
|
70
|
+
const handleLinkClick = () => {
|
|
71
|
+
if (isMobile) {
|
|
72
|
+
setOpenMobile(false)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const allItems = groups.flatMap(g => g.items)
|
|
77
|
+
|
|
78
|
+
const isItemActive = (item: { url: string; exact?: boolean }) => {
|
|
79
|
+
if (item.exact) {
|
|
80
|
+
return pathname === item.url
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (pathname !== item.url && !pathname.startsWith(`${item.url}/`)) {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Yield to a more specific sibling that also matches
|
|
88
|
+
const moreSpecificExists = allItems.some(
|
|
89
|
+
other =>
|
|
90
|
+
other.url !== item.url &&
|
|
91
|
+
other.url.length > item.url.length &&
|
|
92
|
+
other.url.startsWith(item.url) &&
|
|
93
|
+
(pathname === other.url || pathname.startsWith(`${other.url}/`))
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return !moreSpecificExists
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isSubItemActive = (
|
|
100
|
+
item: SidebarNavSubItem,
|
|
101
|
+
siblings: SidebarNavSubItem[]
|
|
102
|
+
) => {
|
|
103
|
+
if (!isItemActive(item)) {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return !siblings.some(
|
|
108
|
+
sibling =>
|
|
109
|
+
sibling.url !== item.url &&
|
|
110
|
+
isItemActive(sibling) &&
|
|
111
|
+
sibling.url.length > item.url.length
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const isGroupActive = (item: SidebarNavItem) => {
|
|
116
|
+
if (isItemActive(item)) {
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return item.items?.some(subItem => isItemActive(subItem)) ?? false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Sidebar collapsible="icon" variant="sidebar">
|
|
125
|
+
<SidebarHeader>{header}</SidebarHeader>
|
|
126
|
+
<SidebarContent>
|
|
127
|
+
{groups.map(group => (
|
|
128
|
+
<SidebarGroup key={group.label}>
|
|
129
|
+
<SidebarGroupLabel>{group.label}</SidebarGroupLabel>
|
|
130
|
+
<SidebarGroupContent>
|
|
131
|
+
<SidebarMenu>
|
|
132
|
+
{group.items.map(item =>
|
|
133
|
+
item.items ? (
|
|
134
|
+
isCollapsed ? (
|
|
135
|
+
<SidebarMenuItem key={item.title}>
|
|
136
|
+
<DropdownMenu>
|
|
137
|
+
<DropdownMenuTrigger asChild>
|
|
138
|
+
<SidebarMenuButton
|
|
139
|
+
tooltip={item.title}
|
|
140
|
+
isActive={isGroupActive(item)}
|
|
141
|
+
>
|
|
142
|
+
{item.icon ? <item.icon /> : null}
|
|
143
|
+
<span>{item.title}</span>
|
|
144
|
+
<ChevronRight className="ml-auto transition-transform duration-200" />
|
|
145
|
+
</SidebarMenuButton>
|
|
146
|
+
</DropdownMenuTrigger>
|
|
147
|
+
<DropdownMenuContent
|
|
148
|
+
side="right"
|
|
149
|
+
align="start"
|
|
150
|
+
sideOffset={4}
|
|
151
|
+
>
|
|
152
|
+
<DropdownMenuLabel>{item.title}</DropdownMenuLabel>
|
|
153
|
+
<DropdownMenuSeparator />
|
|
154
|
+
{item.items.map(subItem => (
|
|
155
|
+
<DropdownMenuItem
|
|
156
|
+
key={subItem.url}
|
|
157
|
+
asChild
|
|
158
|
+
className={
|
|
159
|
+
isSubItemActive(subItem, item.items!)
|
|
160
|
+
? 'bg-accent font-medium'
|
|
161
|
+
: ''
|
|
162
|
+
}
|
|
163
|
+
>
|
|
164
|
+
<Link
|
|
165
|
+
href={subItem.url}
|
|
166
|
+
onClick={handleLinkClick}
|
|
167
|
+
>
|
|
168
|
+
{subItem.title}
|
|
169
|
+
</Link>
|
|
170
|
+
</DropdownMenuItem>
|
|
171
|
+
))}
|
|
172
|
+
</DropdownMenuContent>
|
|
173
|
+
</DropdownMenu>
|
|
174
|
+
</SidebarMenuItem>
|
|
175
|
+
) : (
|
|
176
|
+
<Collapsible
|
|
177
|
+
key={item.title}
|
|
178
|
+
asChild
|
|
179
|
+
defaultOpen={isGroupActive(item)}
|
|
180
|
+
className="group/collapsible"
|
|
181
|
+
>
|
|
182
|
+
<SidebarMenuItem>
|
|
183
|
+
<CollapsibleTrigger asChild>
|
|
184
|
+
<SidebarMenuButton
|
|
185
|
+
tooltip={item.title}
|
|
186
|
+
isActive={isGroupActive(item)}
|
|
187
|
+
>
|
|
188
|
+
{item.icon ? <item.icon /> : null}
|
|
189
|
+
<span>{item.title}</span>
|
|
190
|
+
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
|
191
|
+
</SidebarMenuButton>
|
|
192
|
+
</CollapsibleTrigger>
|
|
193
|
+
<CollapsibleContent>
|
|
194
|
+
<SidebarMenuSub>
|
|
195
|
+
{item.items.map(subItem => (
|
|
196
|
+
<SidebarMenuSubItem key={subItem.url}>
|
|
197
|
+
<SidebarMenuSubButton
|
|
198
|
+
asChild
|
|
199
|
+
isActive={isSubItemActive(
|
|
200
|
+
subItem,
|
|
201
|
+
item.items!
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
204
|
+
<Link
|
|
205
|
+
href={subItem.url}
|
|
206
|
+
onClick={handleLinkClick}
|
|
207
|
+
>
|
|
208
|
+
<span>{subItem.title}</span>
|
|
209
|
+
</Link>
|
|
210
|
+
</SidebarMenuSubButton>
|
|
211
|
+
</SidebarMenuSubItem>
|
|
212
|
+
))}
|
|
213
|
+
</SidebarMenuSub>
|
|
214
|
+
</CollapsibleContent>
|
|
215
|
+
</SidebarMenuItem>
|
|
216
|
+
</Collapsible>
|
|
217
|
+
)
|
|
218
|
+
) : (
|
|
219
|
+
<SidebarMenuItem key={item.title}>
|
|
220
|
+
<SidebarMenuButton
|
|
221
|
+
asChild
|
|
222
|
+
tooltip={item.title}
|
|
223
|
+
isActive={isItemActive(item)}
|
|
224
|
+
>
|
|
225
|
+
<Link href={item.url} onClick={handleLinkClick}>
|
|
226
|
+
{item.icon ? <item.icon /> : null}
|
|
227
|
+
<span>{item.title}</span>
|
|
228
|
+
</Link>
|
|
229
|
+
</SidebarMenuButton>
|
|
230
|
+
</SidebarMenuItem>
|
|
231
|
+
)
|
|
232
|
+
)}
|
|
233
|
+
</SidebarMenu>
|
|
234
|
+
</SidebarGroupContent>
|
|
235
|
+
</SidebarGroup>
|
|
236
|
+
))}
|
|
237
|
+
</SidebarContent>
|
|
238
|
+
<SidebarRail />
|
|
239
|
+
</Sidebar>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable page heading component for admin panel pages.
|
|
3
|
+
* Renders a consistent icon, title and description layout at the top of each page.
|
|
4
|
+
* The `icon` prop is optional; when omitted only the text block is rendered.
|
|
5
|
+
* The `children` prop allows for additional elements (e.g. action buttons) to be placed on the right side of the heading.
|
|
6
|
+
*/
|
|
7
|
+
import { FC, ReactNode } from 'react'
|
|
8
|
+
|
|
9
|
+
export const PageHeading = ({
|
|
10
|
+
title,
|
|
11
|
+
description,
|
|
12
|
+
icon: Icon,
|
|
13
|
+
children
|
|
14
|
+
}: {
|
|
15
|
+
title: string
|
|
16
|
+
description: string
|
|
17
|
+
icon?: FC<{ className?: string }>
|
|
18
|
+
children?: ReactNode
|
|
19
|
+
}) => {
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex items-center justify-between gap-3">
|
|
22
|
+
<div className="flex items-center gap-3">
|
|
23
|
+
{Icon && (
|
|
24
|
+
<div className="bg-primary/10 text-primary flex h-10 w-10 shrink-0 items-center justify-center rounded-lg">
|
|
25
|
+
<Icon className="h-5 w-5" />
|
|
26
|
+
</div>
|
|
27
|
+
)}
|
|
28
|
+
<div>
|
|
29
|
+
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
|
|
30
|
+
<p className="text-muted-foreground">{description}</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
{children ? (
|
|
34
|
+
<div className="flex items-center gap-2">{children}</div>
|
|
35
|
+
) : null}
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview React Query Provider Component
|
|
3
|
+
*
|
|
4
|
+
* Wraps the application with TanStack Query's QueryClientProvider.
|
|
5
|
+
* Enables data fetching, caching, and server state management.
|
|
6
|
+
*
|
|
7
|
+
* **Features Enabled:**
|
|
8
|
+
* - Automatic background refetching
|
|
9
|
+
* - Request deduplication
|
|
10
|
+
* - Cache management
|
|
11
|
+
* - Optimistic updates
|
|
12
|
+
* - useSafeActionQuery/useSafeActionMutation hooks
|
|
13
|
+
*
|
|
14
|
+
* @module components/react-query-provider
|
|
15
|
+
* @see https://tanstack.com/query/latest
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use client'
|
|
19
|
+
|
|
20
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
21
|
+
import { FC, ReactNode, useState } from 'react'
|
|
22
|
+
|
|
23
|
+
/** Default: treat list/admin data as fresh for 30s so focus does not re-hit every action. */
|
|
24
|
+
const DEFAULT_STALE_TIME_MS = 30_000
|
|
25
|
+
|
|
26
|
+
function createQueryClient() {
|
|
27
|
+
return new QueryClient({
|
|
28
|
+
defaultOptions: {
|
|
29
|
+
queries: {
|
|
30
|
+
staleTime: DEFAULT_STALE_TIME_MS
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* React Query Provider Component
|
|
38
|
+
*
|
|
39
|
+
* Creates one QueryClient for the provider lifetime and wraps children.
|
|
40
|
+
* Place near the root of the client tree.
|
|
41
|
+
*
|
|
42
|
+
* Global `staleTime` is 30s. Live surfaces should pass `staleTime: 0`;
|
|
43
|
+
* heavy dashboards and static options can pass a longer value.
|
|
44
|
+
*/
|
|
45
|
+
export const ReactQueryProvider: FC<{
|
|
46
|
+
children?: ReactNode
|
|
47
|
+
}> = ({ children }) => {
|
|
48
|
+
const [client] = useState(createQueryClient)
|
|
49
|
+
|
|
50
|
+
return <QueryClientProvider client={client}>{children}</QueryClientProvider>
|
|
51
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { AlertTriangle } from 'lucide-react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { Button } from '~/components/ui/button'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shared UI for App Router error boundaries (`error.tsx`, `global-error.tsx`).
|
|
9
|
+
* Keeps recovery actions and support reference copy consistent.
|
|
10
|
+
*/
|
|
11
|
+
export function RouteError({
|
|
12
|
+
error,
|
|
13
|
+
onRetry,
|
|
14
|
+
homeHref = '/',
|
|
15
|
+
homeLabel = 'Go Home'
|
|
16
|
+
}: {
|
|
17
|
+
error: Error & { digest?: string }
|
|
18
|
+
onRetry: () => void
|
|
19
|
+
homeHref?: string
|
|
20
|
+
homeLabel?: string
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<main className="bg-background text-foreground flex min-h-screen flex-col">
|
|
24
|
+
<section className="flex min-h-[60vh] flex-col items-center justify-center px-4 py-16">
|
|
25
|
+
<div className="max-w-md text-center">
|
|
26
|
+
<AlertTriangle
|
|
27
|
+
className="text-muted-foreground/50 mx-auto size-16"
|
|
28
|
+
aria-hidden
|
|
29
|
+
/>
|
|
30
|
+
<h1 className="mt-4 text-2xl font-semibold tracking-tight">
|
|
31
|
+
This Page Failed to Load
|
|
32
|
+
</h1>
|
|
33
|
+
<p className="text-muted-foreground mt-3 text-base">
|
|
34
|
+
Refresh the page or go home and try again. If it keeps happening,
|
|
35
|
+
contact support
|
|
36
|
+
{error.digest ? ' with the reference below' : ''}.
|
|
37
|
+
</p>
|
|
38
|
+
{error.digest ? (
|
|
39
|
+
<p className="text-muted-foreground mt-4 font-mono text-xs break-all">
|
|
40
|
+
Reference: {error.digest}
|
|
41
|
+
</p>
|
|
42
|
+
) : null}
|
|
43
|
+
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
|
|
44
|
+
<Button type="button" onClick={onRetry}>
|
|
45
|
+
Try Again
|
|
46
|
+
</Button>
|
|
47
|
+
<Button type="button" variant="outline" asChild>
|
|
48
|
+
<Link href={homeHref}>{homeLabel}</Link>
|
|
49
|
+
</Button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
</main>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Screen Size Indicator (Development Tool)
|
|
3
|
+
*
|
|
4
|
+
* Displays current viewport dimensions and Tailwind breakpoint.
|
|
5
|
+
* Useful during development for responsive design debugging.
|
|
6
|
+
*
|
|
7
|
+
* **Displayed Info:**
|
|
8
|
+
* - Viewport width x height in pixels
|
|
9
|
+
* - Current Tailwind breakpoint (XS, SM, MD, LG, XL, 2XL)
|
|
10
|
+
*
|
|
11
|
+
* **Positioning:**
|
|
12
|
+
* - Fixed to bottom-right corner
|
|
13
|
+
* - Always visible overlay
|
|
14
|
+
*
|
|
15
|
+
* Render this component conditionally.
|
|
16
|
+
* only in development environment.
|
|
17
|
+
*
|
|
18
|
+
* @module components/screen-size
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use client'
|
|
22
|
+
|
|
23
|
+
import { FC, useEffect, useState } from 'react'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Screen Size Indicator Component
|
|
27
|
+
*
|
|
28
|
+
* Fixed position badge showing viewport dimensions and breakpoint.
|
|
29
|
+
* Updates on window resize.
|
|
30
|
+
*
|
|
31
|
+
* **Breakpoint Display:**
|
|
32
|
+
* - XS: < 640px (default, no sm: prefix)
|
|
33
|
+
* - SM: 640px - 767px
|
|
34
|
+
* - MD: 768px - 1023px
|
|
35
|
+
* - LG: 1024px - 1279px
|
|
36
|
+
* - XL: 1280px - 1535px
|
|
37
|
+
* - 2XL: ≥ 1536px
|
|
38
|
+
*
|
|
39
|
+
* @returns {JSX.Element} Fixed position size indicator
|
|
40
|
+
*/
|
|
41
|
+
export const ScreenSize: FC = () => {
|
|
42
|
+
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
function updateDimensions() {
|
|
46
|
+
setDimensions({
|
|
47
|
+
width: window.innerWidth,
|
|
48
|
+
height: window.innerHeight
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
updateDimensions()
|
|
53
|
+
window.addEventListener('resize', updateDimensions)
|
|
54
|
+
|
|
55
|
+
return () => {
|
|
56
|
+
window.removeEventListener('resize', updateDimensions)
|
|
57
|
+
}
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
const { width, height } = dimensions
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="bg-foreground text-background fixed right-4 bottom-4 flex items-center gap-2 rounded-full p-2 text-xs font-medium">
|
|
64
|
+
<span>
|
|
65
|
+
{width.toLocaleString()} x {height.toLocaleString()}
|
|
66
|
+
</span>
|
|
67
|
+
<div className="bg-muted-foreground h-4 w-px" />
|
|
68
|
+
<span className="sm:hidden">XS</span>
|
|
69
|
+
<span className="hidden sm:inline md:hidden">SM</span>
|
|
70
|
+
<span className="hidden md:inline lg:hidden">MD</span>
|
|
71
|
+
<span className="hidden lg:inline xl:hidden">LG</span>
|
|
72
|
+
<span className="hidden xl:inline 2xl:hidden">XL</span>
|
|
73
|
+
<span className="hidden 2xl:inline">2XL</span>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Site Footer Component
|
|
3
|
+
*
|
|
4
|
+
* Global footer displayed on all pages.
|
|
5
|
+
* Contains copyright notice and links to legal pages.
|
|
6
|
+
*
|
|
7
|
+
* @module components/site-footer
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Link from 'next/link'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Site Footer Component
|
|
14
|
+
*
|
|
15
|
+
* Displays copyright and legal navigation links.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} props - Component props
|
|
18
|
+
* @param {string} props.name - Company name for copyright
|
|
19
|
+
* @returns {JSX.Element} Footer element with copyright and links
|
|
20
|
+
*/
|
|
21
|
+
export const SiteFooter = ({ name }: { name: string }) => {
|
|
22
|
+
return (
|
|
23
|
+
<footer className="border-t">
|
|
24
|
+
<div className="text-muted-foreground container flex flex-col items-center justify-between gap-4 py-6 text-sm sm:flex-row">
|
|
25
|
+
<p>
|
|
26
|
+
© {new Date().getUTCFullYear()} {name}. All rights reserved.
|
|
27
|
+
</p>
|
|
28
|
+
<div className="flex gap-4">
|
|
29
|
+
<Link href="/terms" className="hover:text-foreground">
|
|
30
|
+
Terms
|
|
31
|
+
</Link>
|
|
32
|
+
<Link href="/privacy" className="hover:text-foreground">
|
|
33
|
+
Privacy
|
|
34
|
+
</Link>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</footer>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useAction } from 'next-safe-action/hooks'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import { createSupportTicketAction } from '~/app/actions/support-tickets'
|
|
7
|
+
import { Button } from '~/components/ui/button'
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
DialogFooter,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
DialogTrigger
|
|
16
|
+
} from '~/components/ui/dialog'
|
|
17
|
+
import { Input } from '~/components/ui/input'
|
|
18
|
+
import {
|
|
19
|
+
Select,
|
|
20
|
+
SelectContent,
|
|
21
|
+
SelectItem,
|
|
22
|
+
SelectTrigger,
|
|
23
|
+
SelectValue
|
|
24
|
+
} from '~/components/ui/select'
|
|
25
|
+
import { Textarea } from '~/components/ui/textarea'
|
|
26
|
+
import { toastActionError, toastSuccessMessage } from '~/lib/toast-message'
|
|
27
|
+
import {
|
|
28
|
+
supportPriorityOptions,
|
|
29
|
+
type SupportTicketPriority
|
|
30
|
+
} from './support-ticket-shared'
|
|
31
|
+
|
|
32
|
+
export function CreateSupportTicketDialog({
|
|
33
|
+
children,
|
|
34
|
+
organizationId,
|
|
35
|
+
redirectBasePath
|
|
36
|
+
}: {
|
|
37
|
+
children: React.ReactNode
|
|
38
|
+
/** Omit to raise an account ticket rather than an organization one. */
|
|
39
|
+
organizationId?: string
|
|
40
|
+
redirectBasePath: string
|
|
41
|
+
}) {
|
|
42
|
+
const router = useRouter()
|
|
43
|
+
const [open, setOpen] = useState(false)
|
|
44
|
+
const [subject, setSubject] = useState('')
|
|
45
|
+
const [priority, setPriority] = useState<SupportTicketPriority>('medium')
|
|
46
|
+
const [message, setMessage] = useState('')
|
|
47
|
+
|
|
48
|
+
const { execute, isExecuting } = useAction(createSupportTicketAction, {
|
|
49
|
+
onSuccess: ({ data }) => {
|
|
50
|
+
toastSuccessMessage('Support ticket created')
|
|
51
|
+
setOpen(false)
|
|
52
|
+
setSubject('')
|
|
53
|
+
setMessage('')
|
|
54
|
+
if (data?.id) {
|
|
55
|
+
router.push(`${redirectBasePath}/${data.id}`)
|
|
56
|
+
} else {
|
|
57
|
+
router.refresh()
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
onError: ({ error }) => {
|
|
61
|
+
toastActionError(error, 'The support ticket was not created. Try again.')
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const handleSubmit = () => {
|
|
66
|
+
execute({
|
|
67
|
+
organizationId: organizationId ?? null,
|
|
68
|
+
subject,
|
|
69
|
+
priority,
|
|
70
|
+
message
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
76
|
+
<DialogTrigger asChild>{children}</DialogTrigger>
|
|
77
|
+
<DialogContent>
|
|
78
|
+
<DialogHeader>
|
|
79
|
+
<DialogTitle>New Support Ticket</DialogTitle>
|
|
80
|
+
<DialogDescription>
|
|
81
|
+
Describe the problem, including what you expected and what happened.
|
|
82
|
+
</DialogDescription>
|
|
83
|
+
</DialogHeader>
|
|
84
|
+
|
|
85
|
+
<div className="space-y-4">
|
|
86
|
+
<Input
|
|
87
|
+
value={subject}
|
|
88
|
+
onChange={event => setSubject(event.target.value)}
|
|
89
|
+
placeholder="Subject"
|
|
90
|
+
/>
|
|
91
|
+
<Select
|
|
92
|
+
value={priority}
|
|
93
|
+
onValueChange={value => setPriority(value as SupportTicketPriority)}
|
|
94
|
+
>
|
|
95
|
+
<SelectTrigger className="w-full">
|
|
96
|
+
<SelectValue placeholder="Priority" />
|
|
97
|
+
</SelectTrigger>
|
|
98
|
+
<SelectContent>
|
|
99
|
+
{supportPriorityOptions.map(option => (
|
|
100
|
+
<SelectItem key={option.value} value={option.value}>
|
|
101
|
+
{option.label}
|
|
102
|
+
</SelectItem>
|
|
103
|
+
))}
|
|
104
|
+
</SelectContent>
|
|
105
|
+
</Select>
|
|
106
|
+
<Textarea
|
|
107
|
+
value={message}
|
|
108
|
+
onChange={event => setMessage(event.target.value)}
|
|
109
|
+
placeholder="What can we help with?"
|
|
110
|
+
className="min-h-32"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<DialogFooter>
|
|
115
|
+
<Button onClick={handleSubmit} disabled={isExecuting}>
|
|
116
|
+
Create ticket
|
|
117
|
+
</Button>
|
|
118
|
+
</DialogFooter>
|
|
119
|
+
</DialogContent>
|
|
120
|
+
</Dialog>
|
|
121
|
+
)
|
|
122
|
+
}
|