create-ej-app 26.0.0 → 28.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -226
- package/cli.ts +79 -29
- package/package.json +2 -2
- package/template/api/.vscode/settings.json +1 -1
- package/template/api/{.github/copilot-instructions.md → AGENTS.md} +0 -2
- package/template/api/CLAUDE.md +1 -0
- package/template/api/package.json +10 -10
- package/template/nextjs/.dockerignore +1 -0
- package/template/nextjs/AGENTS.md +260 -0
- package/template/nextjs/CLAUDE.md +1 -0
- package/template/nextjs/README.md +109 -0
- package/template/nextjs/bunfig.toml +3 -0
- package/template/nextjs/eslint.config.mjs +6 -5
- package/template/nextjs/gitignore +6 -1
- package/template/nextjs/next.config.ts +8 -1
- package/template/nextjs/package.json +47 -39
- package/template/nextjs/public/logo-dark.svg +4 -0
- package/template/nextjs/public/logo.svg +4 -0
- package/template/nextjs/src/app/(admin)/actions/users.ts +183 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
- package/template/nextjs/src/app/(admin)/admin/layout.tsx +53 -0
- package/template/nextjs/src/app/(admin)/admin/page.tsx +11 -2
- package/template/nextjs/src/app/(admin)/admin/settings/layout.tsx +24 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/page.tsx +377 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/validation.ts +23 -0
- package/template/nextjs/src/app/(app)/app/layout.tsx +44 -0
- package/template/nextjs/src/app/(app)/app/page.tsx +14 -2
- package/template/nextjs/src/app/(auth)/(account)/layout.tsx +64 -0
- package/template/nextjs/src/app/(auth)/(account)/profile/page.tsx +16 -0
- package/template/nextjs/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
- package/template/nextjs/src/app/(auth)/login/email-login-form.tsx +56 -29
- package/template/nextjs/src/app/(auth)/login/page.tsx +45 -12
- package/template/nextjs/src/app/(auth)/login/social-login-buttons.tsx +85 -17
- package/template/nextjs/src/app/(marketing)/layout.tsx +35 -4
- package/template/nextjs/src/app/(marketing)/legal.tsx +56 -0
- package/template/nextjs/src/app/(marketing)/page.tsx +20 -2
- package/template/nextjs/src/app/(marketing)/privacy/page.tsx +43 -2
- package/template/nextjs/src/app/(marketing)/terms/page.tsx +33 -2
- package/template/nextjs/src/app/error.tsx +37 -0
- package/template/nextjs/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
- package/template/nextjs/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
- package/template/nextjs/src/app/fonts/index.ts +60 -0
- package/template/nextjs/src/app/global-error.tsx +38 -0
- package/template/nextjs/src/app/globals.css +29 -0
- package/template/nextjs/src/app/layout.tsx +45 -18
- package/template/nextjs/src/app/not-found.tsx +25 -3
- package/template/nextjs/src/app/robots.ts +29 -0
- package/template/nextjs/src/app/sitemap.ts +21 -0
- package/template/nextjs/src/components/confirm-dialog.tsx +79 -0
- package/template/nextjs/src/components/dashboard-header.tsx +70 -0
- package/template/nextjs/src/components/data-table/enum.ts +5 -0
- package/template/nextjs/src/components/data-table/features.ts +49 -0
- package/template/nextjs/src/components/data-table/index.tsx +961 -0
- package/template/nextjs/src/components/data-table/types.ts +76 -0
- package/template/nextjs/src/components/error.tsx +71 -0
- package/template/nextjs/src/components/loader.tsx +18 -2
- package/template/nextjs/src/components/logo.tsx +49 -0
- package/template/nextjs/src/components/navigation-search.tsx +140 -0
- package/template/nextjs/src/components/navigation-sidebar.tsx +241 -0
- package/template/nextjs/src/components/page-heading.tsx +38 -0
- package/template/nextjs/src/components/react-query-provider.tsx +41 -2
- package/template/nextjs/src/components/route-error.tsx +55 -0
- package/template/nextjs/src/components/screen-size.tsx +36 -0
- package/template/nextjs/src/components/site-footer.tsx +20 -4
- package/template/nextjs/src/components/theme-provider.tsx +48 -0
- package/template/nextjs/src/components/theme-toggle.tsx +58 -5
- package/template/nextjs/src/components/ui/alert-dialog.tsx +199 -0
- package/template/nextjs/src/components/ui/avatar.tsx +112 -0
- package/template/nextjs/src/components/ui/badge.tsx +54 -0
- package/template/nextjs/src/components/ui/button.tsx +3 -1
- package/template/nextjs/src/components/ui/card.tsx +4 -4
- package/template/nextjs/src/components/ui/collapsible.tsx +33 -0
- package/template/nextjs/src/components/ui/command.tsx +192 -0
- package/template/nextjs/src/components/ui/dialog.tsx +164 -0
- package/template/nextjs/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/nextjs/src/components/ui/input-group.tsx +156 -0
- package/template/nextjs/src/components/ui/select.tsx +195 -0
- package/template/nextjs/src/components/ui/sheet.tsx +143 -0
- package/template/nextjs/src/components/ui/sidebar.tsx +702 -0
- package/template/nextjs/src/components/ui/skeleton.tsx +13 -0
- package/template/nextjs/src/components/ui/table.tsx +116 -0
- package/template/nextjs/src/components/ui/textarea.tsx +18 -0
- package/template/nextjs/src/components/ui/tooltip.tsx +57 -0
- package/template/nextjs/src/components/user-menu.tsx +206 -0
- package/template/nextjs/src/db/common.ts +10 -0
- package/template/nextjs/src/db/index.ts +16 -6
- package/template/nextjs/src/db/relations.ts +21 -0
- package/template/nextjs/src/db/schema.ts +85 -46
- package/template/nextjs/src/emails/components/email-layout.tsx +27 -11
- package/template/nextjs/src/emails/magic-link.tsx +15 -13
- package/template/nextjs/src/emails/test.tsx +11 -9
- package/template/nextjs/src/emails/welcome.tsx +12 -9
- package/template/nextjs/src/env.ts +1 -1
- package/template/nextjs/src/hooks/use-mobile.ts +14 -12
- package/template/nextjs/src/instrumentation.ts +20 -2
- package/template/nextjs/src/lib/actions.ts +55 -0
- package/template/nextjs/src/lib/admin-navigation.ts +43 -0
- package/template/nextjs/src/lib/auth-client.ts +20 -9
- package/template/nextjs/src/lib/auth-permissions.ts +61 -0
- package/template/nextjs/src/lib/auth.ts +33 -3
- package/template/nextjs/src/lib/callback-url.ts +28 -0
- package/template/nextjs/src/lib/constants.ts +21 -0
- package/template/nextjs/src/lib/delay.ts +10 -0
- package/template/nextjs/src/lib/email-service.ts +21 -0
- package/template/nextjs/src/lib/errors.ts +74 -0
- package/template/nextjs/src/lib/format-currency.ts +22 -24
- package/template/nextjs/src/lib/format-date.ts +69 -6
- package/template/nextjs/src/lib/nodemailer.ts +27 -3
- package/template/nextjs/src/lib/otel.ts +92 -2
- package/template/nextjs/src/lib/safe-action-client.ts +60 -9
- package/template/nextjs/src/lib/safe-action.ts +152 -16
- package/template/nextjs/src/lib/search.ts +16 -0
- package/template/nextjs/src/lib/seo.ts +7 -0
- package/template/nextjs/src/lib/siteConfig.ts +17 -1
- package/template/nextjs/src/lib/toast-message.ts +87 -1
- package/template/nextjs/src/lib/validations.ts +30 -0
- package/template/nextjs/src/proxy.ts +48 -0
- package/template/nextjs/src/scripts/reset-db.ts +22 -0
- package/template/nextjs/src/scripts/seed.ts +59 -8
- package/template/nextjs/src/scripts/test-mail.ts +28 -20
- package/template/nextjs/tests/admin/users.test.ts +64 -0
- package/template/nextjs/tests/db/fk-indexes.test.ts +62 -0
- package/template/nextjs/tests/db/seed.test.ts +49 -0
- package/template/nextjs/tests/helpers/action.ts +71 -0
- package/template/nextjs/tests/helpers/fixtures.ts +78 -0
- package/template/nextjs/tests/helpers/index.ts +2 -0
- package/template/nextjs/tests/lib/callback-url.test.ts +48 -0
- package/template/nextjs/tests/lib/client-server-boundary.test.ts +57 -0
- package/template/nextjs/tests/lib/errors.test.ts +10 -0
- package/template/nextjs/tests/lib/search.test.ts +23 -0
- package/template/nextjs/tests/setup/pglite-db.ts +63 -0
- package/template/nextjs/tests/setup/preload.ts +95 -0
- package/template/nextjs/tsconfig.json +1 -0
- package/template/saas-starter/.dockerignore +1 -0
- package/template/saas-starter/.env.example +15 -15
- package/template/saas-starter/AGENTS.md +302 -0
- package/template/saas-starter/CLAUDE.md +1 -0
- package/template/saas-starter/README.md +124 -2
- package/template/saas-starter/bunfig.toml +3 -0
- package/template/saas-starter/eslint.config.mjs +6 -5
- package/template/saas-starter/gitignore +6 -1
- package/template/saas-starter/next.config.ts +19 -1
- package/template/saas-starter/package.json +50 -44
- package/template/saas-starter/public/logo-dark.svg +4 -0
- package/template/saas-starter/public/logo.svg +4 -0
- package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
- package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
- package/template/saas-starter/src/app/(admin)/actions/users.ts +145 -59
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +4 -1
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +12 -10
- package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
- package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
- package/template/saas-starter/src/app/(admin)/admin/layout.tsx +24 -28
- package/template/saas-starter/src/app/(admin)/admin/page.tsx +59 -2
- package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +372 -10
- package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
- package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
- package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
- package/template/saas-starter/src/app/(app)/actions/billing.ts +171 -107
- package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
- package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
- package/template/saas-starter/src/app/(app)/actions/members.ts +149 -123
- package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
- package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +6 -9
- package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +6 -9
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +45 -53
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +40 -47
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +80 -105
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +72 -82
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +51 -52
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +15 -16
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +77 -44
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +11 -18
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +14 -28
- package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +15 -13
- package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +15 -6
- package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +6 -3
- package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +5 -9
- package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +68 -8
- package/template/saas-starter/src/app/(app)/app/layout.tsx +44 -32
- package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +75 -87
- package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +10 -12
- package/template/saas-starter/src/app/(app)/app/page.tsx +16 -21
- package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
- package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
- package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
- package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
- package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
- package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +49 -27
- package/template/saas-starter/src/app/(auth)/login/page.tsx +39 -21
- package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +80 -18
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +16 -10
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +28 -42
- package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
- package/template/saas-starter/src/app/(marketing)/layout.tsx +38 -4
- package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
- package/template/saas-starter/src/app/(marketing)/page.tsx +23 -2
- package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +62 -2
- package/template/saas-starter/src/app/(marketing)/terms/page.tsx +40 -2
- package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
- package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
- package/template/saas-starter/src/app/actions/uploads.ts +36 -0
- package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
- package/template/saas-starter/src/app/error.tsx +37 -0
- package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
- package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
- package/template/saas-starter/src/app/fonts/index.ts +60 -0
- package/template/saas-starter/src/app/global-error.tsx +38 -0
- package/template/saas-starter/src/app/globals.css +29 -0
- package/template/saas-starter/src/app/layout.tsx +43 -19
- package/template/saas-starter/src/app/not-found.tsx +25 -3
- package/template/saas-starter/src/app/robots.ts +14 -10
- package/template/saas-starter/src/app/sitemap.ts +2 -1
- package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
- package/template/saas-starter/src/components/dashboard-header.tsx +7 -1
- package/template/saas-starter/src/components/data-table/features.ts +49 -0
- package/template/saas-starter/src/components/data-table/index.tsx +65 -64
- package/template/saas-starter/src/components/data-table/types.ts +5 -4
- package/template/saas-starter/src/components/error.tsx +45 -0
- package/template/saas-starter/src/components/file-upload.tsx +65 -26
- package/template/saas-starter/src/components/loader.tsx +18 -2
- package/template/saas-starter/src/components/logo.tsx +49 -0
- package/template/saas-starter/src/components/navigation-search.tsx +2 -2
- package/template/saas-starter/src/components/page-heading.tsx +6 -0
- package/template/saas-starter/src/components/react-query-provider.tsx +41 -2
- package/template/saas-starter/src/components/route-error.tsx +55 -0
- package/template/saas-starter/src/components/screen-size.tsx +36 -0
- package/template/saas-starter/src/components/site-footer.tsx +20 -4
- package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
- package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
- package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
- package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
- package/template/saas-starter/src/components/theme-provider.tsx +48 -0
- package/template/saas-starter/src/components/theme-toggle.tsx +58 -5
- package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
- package/template/saas-starter/src/components/ui/badge.tsx +6 -1
- package/template/saas-starter/src/components/ui/button.tsx +2 -0
- package/template/saas-starter/src/components/ui/command.tsx +1 -1
- package/template/saas-starter/src/components/user-menu.tsx +147 -24
- package/template/saas-starter/src/db/common.ts +33 -1
- package/template/saas-starter/src/db/enums.ts +24 -0
- package/template/saas-starter/src/db/index.ts +14 -5
- package/template/saas-starter/src/db/relations.ts +40 -3
- package/template/saas-starter/src/db/schema.ts +186 -35
- package/template/saas-starter/src/emails/components/email-layout.tsx +27 -11
- package/template/saas-starter/src/emails/invitation.tsx +34 -21
- package/template/saas-starter/src/emails/magic-link.tsx +15 -13
- package/template/saas-starter/src/emails/test.tsx +11 -9
- package/template/saas-starter/src/emails/welcome.tsx +12 -9
- package/template/saas-starter/src/env.ts +1 -1
- package/template/saas-starter/src/hooks/use-mobile.ts +14 -12
- package/template/saas-starter/src/instrumentation.ts +20 -2
- package/template/saas-starter/src/lib/actions.ts +34 -0
- package/template/saas-starter/src/lib/admin-navigation.ts +33 -1
- package/template/saas-starter/src/lib/app-navigation.ts +45 -41
- package/template/saas-starter/src/lib/auth-client.ts +8 -3
- package/template/saas-starter/src/lib/auth-permissions.ts +50 -11
- package/template/saas-starter/src/lib/auth.ts +122 -21
- package/template/saas-starter/src/lib/billing.ts +59 -24
- package/template/saas-starter/src/lib/callback-url.ts +28 -0
- package/template/saas-starter/src/lib/constants.ts +78 -9
- package/template/saas-starter/src/lib/delay.ts +10 -0
- package/template/saas-starter/src/lib/email-service.ts +30 -1
- package/template/saas-starter/src/lib/errors.ts +74 -0
- package/template/saas-starter/src/lib/format-currency.ts +22 -24
- package/template/saas-starter/src/lib/format-date.ts +69 -6
- package/template/saas-starter/src/lib/image-url.ts +54 -0
- package/template/saas-starter/src/lib/nodemailer.ts +27 -3
- package/template/saas-starter/src/lib/organization-access.ts +56 -76
- package/template/saas-starter/src/lib/otel.ts +92 -2
- package/template/saas-starter/src/lib/plan-limits.ts +37 -0
- package/template/saas-starter/src/lib/plans.ts +40 -13
- package/template/saas-starter/src/lib/rbac.ts +44 -0
- package/template/saas-starter/src/lib/safe-action-client.ts +60 -9
- package/template/saas-starter/src/lib/safe-action.ts +137 -17
- package/template/saas-starter/src/lib/search.ts +16 -0
- package/template/saas-starter/src/lib/seo.ts +7 -0
- package/template/saas-starter/src/lib/siteConfig.ts +17 -1
- package/template/saas-starter/src/lib/storage.ts +176 -46
- package/template/saas-starter/src/lib/stripe.ts +4 -1
- package/template/saas-starter/src/lib/toast-message.ts +87 -1
- package/template/saas-starter/src/lib/validations.ts +85 -2
- package/template/saas-starter/src/proxy.ts +48 -0
- package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
- package/template/saas-starter/src/scripts/seed.ts +82 -8
- package/template/saas-starter/src/scripts/setup-db.ts +72 -0
- package/template/saas-starter/src/scripts/setup-stripe-plans.ts +13 -7
- package/template/saas-starter/src/scripts/test-mail.ts +28 -20
- package/template/saas-starter/tests/admin/users.test.ts +64 -0
- package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
- package/template/saas-starter/tests/db/seed.test.ts +57 -0
- package/template/saas-starter/tests/helpers/action.ts +71 -0
- package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
- package/template/saas-starter/tests/helpers/index.ts +2 -0
- package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
- package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
- package/template/saas-starter/tests/lib/errors.test.ts +10 -0
- package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
- package/template/saas-starter/tests/lib/search.test.ts +23 -0
- package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
- package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
- package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
- package/template/saas-starter/tests/setup/preload.ts +108 -0
- package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
- package/template/saas-starter/tsconfig.json +1 -0
- package/cli.js +0 -147
- package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
- package/template/nextjs/src/app/(app)/layout.tsx +0 -20
- package/template/nextjs/terraform/artifact-registry.tf +0 -5
- package/template/nextjs/terraform/backend-service.tf +0 -13
- package/template/nextjs/terraform/cleanup.sh +0 -9
- package/template/nextjs/terraform/cloudbuild.tf +0 -70
- package/template/nextjs/terraform/cloudrun.tf +0 -68
- package/template/nextjs/terraform/database.tf +0 -60
- package/template/nextjs/terraform/forwarding-rule.tf +0 -43
- package/template/nextjs/terraform/gitignore +0 -4
- package/template/nextjs/terraform/ip-address.tf +0 -11
- package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
- package/template/nextjs/terraform/network.tf +0 -21
- package/template/nextjs/terraform/pre.sh +0 -3
- package/template/nextjs/terraform/providers.tf +0 -18
- package/template/nextjs/terraform/secret.txt +0 -11
- package/template/nextjs/terraform/setup.sh +0 -25
- package/template/nextjs/terraform/ssl-certs.tf +0 -32
- package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
- package/template/nextjs/terraform/target-https-proxy.tf +0 -14
- package/template/nextjs/terraform/terraform.tfvars +0 -4
- package/template/nextjs/terraform/url-map.tf +0 -23
- package/template/nextjs/terraform/variables.tf +0 -21
- package/template/saas-starter/next-env.d.ts +0 -6
- package/template/saas-starter/src/actions/uploads.ts +0 -28
- package/template/saas-starter/src/actions/uploads.validation.ts +0 -14
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +0 -183
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +0 -129
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +0 -415
- package/template/saas-starter/src/app/(app)/actions/organization.ts +0 -92
- package/template/saas-starter/src/app/proxy.ts +0 -28
- package/template/saas-starter/src/components/impersonation-banner.tsx +0 -44
- package/template/saas-starter/src/lib/organization-plan-limits.ts +0 -38
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Generic
|
|
2
|
+
* @fileoverview Generic DataTable built on TanStack Table.
|
|
3
3
|
*
|
|
4
4
|
* Supports:
|
|
5
5
|
* - Server-side (manual) or client-side pagination & sorting
|
|
@@ -14,13 +14,10 @@
|
|
|
14
14
|
'use client'
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
getPaginationRowModel,
|
|
22
|
-
getSortedRowModel,
|
|
23
|
-
useReactTable
|
|
17
|
+
type ColumnVisibilityState,
|
|
18
|
+
type RowData,
|
|
19
|
+
type SortingState,
|
|
20
|
+
useTable
|
|
24
21
|
} from '@tanstack/react-table'
|
|
25
22
|
import {
|
|
26
23
|
ArrowDown,
|
|
@@ -93,6 +90,7 @@ import { cn } from '~/lib/cn'
|
|
|
93
90
|
import { useSafeActionQuery } from '~/lib/safe-action-client'
|
|
94
91
|
import { Skeleton } from '../ui/skeleton'
|
|
95
92
|
import { SortOrderEnum } from './enum'
|
|
93
|
+
import { dataTableFeatures } from './features'
|
|
96
94
|
import { DataTableFilter, DataTableProps } from './types'
|
|
97
95
|
|
|
98
96
|
/**
|
|
@@ -161,7 +159,7 @@ const StaticFilterSubContent = memo(function StaticFilterSubContent({
|
|
|
161
159
|
{isTruncated ? (
|
|
162
160
|
<Tooltip>
|
|
163
161
|
<TooltipTrigger asChild>
|
|
164
|
-
<span>{option.label.slice(0, 18) + '
|
|
162
|
+
<span>{option.label.slice(0, 18) + '…'}</span>
|
|
165
163
|
</TooltipTrigger>
|
|
166
164
|
<TooltipContent side="right">
|
|
167
165
|
<p className="max-w-75 break-all">{option.label}</p>
|
|
@@ -284,7 +282,7 @@ const AsyncFilterSubContent = memo(function AsyncFilterSubContent({
|
|
|
284
282
|
{isTruncated ? (
|
|
285
283
|
<Tooltip>
|
|
286
284
|
<TooltipTrigger asChild>
|
|
287
|
-
<span>{label.slice(0, 18) + '
|
|
285
|
+
<span>{label.slice(0, 18) + '…'}</span>
|
|
288
286
|
</TooltipTrigger>
|
|
289
287
|
<TooltipContent side="right">
|
|
290
288
|
<p className="max-w-75 break-all">{label}</p>
|
|
@@ -306,14 +304,13 @@ const AsyncFilterSubContent = memo(function AsyncFilterSubContent({
|
|
|
306
304
|
* Generic DataTable component with toolbar, filters, sorting and pagination.
|
|
307
305
|
*
|
|
308
306
|
* @template TData Row data shape
|
|
309
|
-
* @template TValue Cell value shape
|
|
310
307
|
*/
|
|
311
|
-
export function DataTable<TData
|
|
308
|
+
export function DataTable<TData extends RowData>({
|
|
312
309
|
columns,
|
|
313
310
|
data = [],
|
|
314
311
|
isLoading = false,
|
|
315
312
|
enableSearch = false,
|
|
316
|
-
searchPlaceholder = 'Search
|
|
313
|
+
searchPlaceholder = 'Search…',
|
|
317
314
|
initialSearch = '',
|
|
318
315
|
totalCount,
|
|
319
316
|
onParamsChange,
|
|
@@ -324,9 +321,10 @@ export function DataTable<TData, TValue>({
|
|
|
324
321
|
initialSorting = [],
|
|
325
322
|
filters = [],
|
|
326
323
|
activeFilters = {}
|
|
327
|
-
}: DataTableProps<TData
|
|
324
|
+
}: DataTableProps<TData>) {
|
|
328
325
|
const [sorting, setSorting] = useState<SortingState>(initialSorting)
|
|
329
|
-
const [columnVisibility, setColumnVisibility] =
|
|
326
|
+
const [columnVisibility, setColumnVisibility] =
|
|
327
|
+
useState<ColumnVisibilityState>({})
|
|
330
328
|
const [searchValue, setSearchValue] = useState(initialSearch)
|
|
331
329
|
const [pagination, setPagination] = useState({
|
|
332
330
|
pageIndex: controlledPageIndex,
|
|
@@ -335,12 +333,41 @@ export function DataTable<TData, TValue>({
|
|
|
335
333
|
/** Cache for async filter labels so pills display correctly after data refetch. */
|
|
336
334
|
const [labelCache, setLabelCache] = useState<Record<string, string>>({})
|
|
337
335
|
const searchInputId = useId()
|
|
338
|
-
|
|
339
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Last positive totalCount. Used while loading so a transient 0 does not
|
|
338
|
+
* collapse the pagination footer (e.g. "Page 1 of 1" mid-refetch).
|
|
339
|
+
* Adjusted during render when the prop changes (React-approved pattern).
|
|
340
|
+
*/
|
|
341
|
+
const [lastPositiveTotalCount, setLastPositiveTotalCount] = useState<
|
|
342
|
+
number | undefined
|
|
343
|
+
>(totalCount !== undefined && totalCount > 0 ? totalCount : undefined)
|
|
344
|
+
if (
|
|
345
|
+
totalCount !== undefined &&
|
|
346
|
+
totalCount > 0 &&
|
|
347
|
+
totalCount !== lastPositiveTotalCount
|
|
348
|
+
) {
|
|
349
|
+
setLastPositiveTotalCount(totalCount)
|
|
350
|
+
}
|
|
351
|
+
const stableTotalCount =
|
|
352
|
+
isLoading && totalCount === 0 && lastPositiveTotalCount !== undefined
|
|
353
|
+
? lastPositiveTotalCount
|
|
354
|
+
: totalCount
|
|
355
|
+
|
|
356
|
+
/** Latest values for debounced/async callbacks without putting them in effect deps. */
|
|
340
357
|
const activeFiltersRef = useRef(activeFilters)
|
|
341
|
-
|
|
358
|
+
const onParamsChangeRef = useRef(onParamsChange)
|
|
342
359
|
/** Set by the prop-sync effect so the debounced effect can skip redundant fires. */
|
|
343
360
|
const isExternalPaginationSync = useRef(false)
|
|
361
|
+
/** Skip the initial render to avoid firing onParamsChange on mount. */
|
|
362
|
+
const isFirstRender = useRef(true)
|
|
363
|
+
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
activeFiltersRef.current = activeFilters
|
|
366
|
+
}, [activeFilters])
|
|
367
|
+
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
onParamsChangeRef.current = onParamsChange
|
|
370
|
+
}, [onParamsChange])
|
|
344
371
|
|
|
345
372
|
// Keep internal pagination in sync with controlled props from parent.
|
|
346
373
|
// Without this, external resets (e.g. filter change → page 1) are ignored.
|
|
@@ -350,7 +377,7 @@ export function DataTable<TData, TValue>({
|
|
|
350
377
|
prev.pageIndex !== controlledPageIndex ||
|
|
351
378
|
prev.pageSize !== controlledPageSize
|
|
352
379
|
) {
|
|
353
|
-
// Flag so the debounced effect skips its next fire
|
|
380
|
+
// Flag so the debounced effect skips its next fire: the filter/clear
|
|
354
381
|
// handler that triggered this prop change already called onParamsChange.
|
|
355
382
|
isExternalPaginationSync.current = true
|
|
356
383
|
return {
|
|
@@ -362,30 +389,13 @@ export function DataTable<TData, TValue>({
|
|
|
362
389
|
})
|
|
363
390
|
}, [controlledPageIndex, controlledPageSize])
|
|
364
391
|
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
const stableTotalCount = useMemo(
|
|
372
|
-
() =>
|
|
373
|
-
isLoading && totalCount === 0 && lastTotalCountRef.current !== undefined
|
|
374
|
-
? lastTotalCountRef.current
|
|
375
|
-
: totalCount,
|
|
376
|
-
// lastTotalCountRef is a ref - intentionally omitted from deps
|
|
377
|
-
[isLoading, totalCount]
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
// Initialise TanStack Table with manual pagination/sorting when enabled
|
|
381
|
-
const table = useReactTable({
|
|
392
|
+
// TanStack Table v9: features (and row models) live on `dataTableFeatures`.
|
|
393
|
+
// Core row model is always included; sorted/paginated models only run when
|
|
394
|
+
// manual* is false.
|
|
395
|
+
const table = useTable({
|
|
396
|
+
features: dataTableFeatures,
|
|
382
397
|
data,
|
|
383
398
|
columns,
|
|
384
|
-
getCoreRowModel: getCoreRowModel(),
|
|
385
|
-
...(!manualPagination && {
|
|
386
|
-
getPaginationRowModel: getPaginationRowModel()
|
|
387
|
-
}),
|
|
388
|
-
...(!manualSorting && { getSortedRowModel: getSortedRowModel() }),
|
|
389
399
|
manualPagination,
|
|
390
400
|
manualSorting,
|
|
391
401
|
...(manualPagination && {
|
|
@@ -402,16 +412,11 @@ export function DataTable<TData, TValue>({
|
|
|
402
412
|
}
|
|
403
413
|
})
|
|
404
414
|
|
|
405
|
-
/** Skip the initial render to avoid firing onParamsChange on mount. */
|
|
406
|
-
const isFirstRender = useRef(true)
|
|
407
|
-
|
|
408
|
-
// Stable reference for onParamsChange to avoid unnecessary effect re-runs
|
|
409
|
-
const onParamsChangeRef = useRef(onParamsChange)
|
|
410
|
-
onParamsChangeRef.current = onParamsChange
|
|
411
|
-
|
|
412
415
|
/**
|
|
413
416
|
* Build the params object sent to onParamsChange.
|
|
414
|
-
* Uses `activeFiltersRef` so
|
|
417
|
+
* Uses `activeFiltersRef` so debounced callbacks always read the latest filters
|
|
418
|
+
* without putting `activeFilters` in the debounce effect deps (filter handlers
|
|
419
|
+
* already call onParamsChange themselves).
|
|
415
420
|
* Pass `overrides` to set page or filters explicitly (e.g. on filter change).
|
|
416
421
|
*/
|
|
417
422
|
const buildParams = useCallback(
|
|
@@ -446,7 +451,7 @@ export function DataTable<TData, TValue>({
|
|
|
446
451
|
)
|
|
447
452
|
|
|
448
453
|
// Debounce param changes (300ms) to batch rapid user interactions.
|
|
449
|
-
// Only watches pagination, sorting, and searchValue
|
|
454
|
+
// Only watches pagination, sorting, and searchValue, NOT activeFilters,
|
|
450
455
|
// because filter handlers already call onParamsChange directly.
|
|
451
456
|
useEffect(() => {
|
|
452
457
|
if (isFirstRender.current) {
|
|
@@ -604,7 +609,7 @@ export function DataTable<TData, TValue>({
|
|
|
604
609
|
}}
|
|
605
610
|
className="justify-center text-center"
|
|
606
611
|
>
|
|
607
|
-
Clear
|
|
612
|
+
Clear Filters
|
|
608
613
|
</DropdownMenuItem>
|
|
609
614
|
</>
|
|
610
615
|
)}
|
|
@@ -681,7 +686,7 @@ export function DataTable<TData, TValue>({
|
|
|
681
686
|
variant="secondary"
|
|
682
687
|
className="px-1 font-normal"
|
|
683
688
|
>
|
|
684
|
-
{displayLabel.slice(0, 18) + '
|
|
689
|
+
{displayLabel.slice(0, 18) + '…'}
|
|
685
690
|
</Badge>
|
|
686
691
|
</TooltipTrigger>
|
|
687
692
|
<TooltipContent>
|
|
@@ -792,10 +797,7 @@ export function DataTable<TData, TValue>({
|
|
|
792
797
|
: undefined
|
|
793
798
|
}
|
|
794
799
|
>
|
|
795
|
-
{
|
|
796
|
-
header.column.columnDef.header,
|
|
797
|
-
header.getContext()
|
|
798
|
-
)}
|
|
800
|
+
<table.FlexRender header={header} />
|
|
799
801
|
{isSortable && (
|
|
800
802
|
<div className="flex w-4 items-center justify-center">
|
|
801
803
|
{sortStatus === 'asc' ? (
|
|
@@ -827,16 +829,10 @@ export function DataTable<TData, TValue>({
|
|
|
827
829
|
<TableBody>
|
|
828
830
|
{table.getRowModel().rows.length ? (
|
|
829
831
|
table.getRowModel().rows.map(row => (
|
|
830
|
-
<TableRow
|
|
831
|
-
key={row.id}
|
|
832
|
-
data-state={row.getIsSelected() ? 'selected' : undefined}
|
|
833
|
-
>
|
|
832
|
+
<TableRow key={row.id}>
|
|
834
833
|
{row.getVisibleCells().map(cell => (
|
|
835
834
|
<TableCell key={cell.id}>
|
|
836
|
-
{
|
|
837
|
-
cell.column.columnDef.cell,
|
|
838
|
-
cell.getContext()
|
|
839
|
-
)}
|
|
835
|
+
<table.FlexRender cell={cell} />
|
|
840
836
|
</TableCell>
|
|
841
837
|
))}
|
|
842
838
|
</TableRow>
|
|
@@ -958,3 +954,8 @@ export function DataTable<TData, TValue>({
|
|
|
958
954
|
</div>
|
|
959
955
|
)
|
|
960
956
|
}
|
|
957
|
+
|
|
958
|
+
export { SortOrderEnum } from './enum'
|
|
959
|
+
export { dataTableFeatures } from './features'
|
|
960
|
+
export type { DataTableColumnDef } from './features'
|
|
961
|
+
export type { DataTableFilter, DataTableProps } from './types'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { RowData, SortingState } from '@tanstack/react-table'
|
|
2
2
|
import { SortOrderEnum } from './enum'
|
|
3
|
+
import type { DataTableColumnDef } from './features'
|
|
3
4
|
|
|
4
5
|
/** A single selectable option inside a filter dropdown. */
|
|
5
6
|
interface DataTableFilterOption {
|
|
@@ -34,9 +35,9 @@ export interface DataTableFilter {
|
|
|
34
35
|
* When `manualPagination` / `manualSorting` are enabled the parent is
|
|
35
36
|
* responsible for fetching data and passing results back via `data` & `totalCount`.
|
|
36
37
|
*/
|
|
37
|
-
export interface DataTableProps<TData
|
|
38
|
-
/** Column definitions (TanStack Table). */
|
|
39
|
-
columns:
|
|
38
|
+
export interface DataTableProps<TData extends RowData> {
|
|
39
|
+
/** Column definitions (TanStack Table v9, typed to DataTable features). */
|
|
40
|
+
columns: DataTableColumnDef<TData, any>[]
|
|
40
41
|
/** Row data to display. */
|
|
41
42
|
data?: TData[]
|
|
42
43
|
/** Show skeleton loading rows while data is being fetched. */
|
|
@@ -1,8 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Reusable Error Display Component
|
|
3
|
+
*
|
|
4
|
+
* Generic error/empty state component for displaying user-friendly
|
|
5
|
+
* error messages with optional icon and action button.
|
|
6
|
+
*
|
|
7
|
+
* **Use Cases:**
|
|
8
|
+
* - Empty search results
|
|
9
|
+
* - Data fetch failures
|
|
10
|
+
* - Access denied states
|
|
11
|
+
* - 404-like content not found
|
|
12
|
+
*
|
|
13
|
+
* @module components/error
|
|
14
|
+
*/
|
|
15
|
+
|
|
1
16
|
import { type LucideIcon, AlertTriangle } from 'lucide-react'
|
|
2
17
|
import Link from 'next/link'
|
|
3
18
|
import { FC } from 'react'
|
|
4
19
|
import { Button } from '~/components/ui/button'
|
|
5
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Error Display Component
|
|
23
|
+
*
|
|
24
|
+
* Renders a centered error state with icon, message, and action.
|
|
25
|
+
*
|
|
26
|
+
* **Layout:**
|
|
27
|
+
* - Large icon (customizable)
|
|
28
|
+
* - Bold title
|
|
29
|
+
* - Descriptive message
|
|
30
|
+
* - Action button (customizable link)
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} props - Component props
|
|
33
|
+
* @param {string} props.title - Error heading text
|
|
34
|
+
* @param {string} props.message - Descriptive error message
|
|
35
|
+
* @param {LucideIcon} [props.icon] - Custom icon (defaults to AlertTriangle)
|
|
36
|
+
* @param {Object} [props.link] - Optional action button configuration
|
|
37
|
+
* @param {string} [props.link.href] - Button link destination (defaults to '/')
|
|
38
|
+
* @param {string} [props.link.text] - Button text (defaults to 'Browse Products')
|
|
39
|
+
* @returns {JSX.Element} Centered error display
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* <ErrorDisplay
|
|
44
|
+
* title="No Results Found"
|
|
45
|
+
* message="Try adjusting your search or filters"
|
|
46
|
+
* icon={Search}
|
|
47
|
+
* link={{ href: '/search', text: 'Clear Filters' }}
|
|
48
|
+
* />
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
6
51
|
export const ErrorDisplay: FC<{
|
|
7
52
|
title: string
|
|
8
53
|
message: string
|
|
@@ -3,12 +3,40 @@
|
|
|
3
3
|
import { File, FileText, Loader2, Music, Upload, Video, X } from 'lucide-react'
|
|
4
4
|
import Image from 'next/image'
|
|
5
5
|
import { useRef, useState } from 'react'
|
|
6
|
-
import {
|
|
7
|
-
import { generateUploadUrlAction } from '~/actions/uploads'
|
|
6
|
+
import { generateUploadUrlAction } from '~/app/actions/uploads'
|
|
8
7
|
import { Button } from '~/components/ui/button'
|
|
9
8
|
import { cn } from '~/lib/cn'
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
MAX_FILE_SIZE_BYTES,
|
|
11
|
+
MAX_FILE_SIZE_MB,
|
|
12
|
+
R2_UPLOAD_EXTENSION_BY_MIME_TYPE,
|
|
13
|
+
type R2UploadMimeType
|
|
14
|
+
} from '~/lib/constants'
|
|
15
|
+
import { toastErrorMessage, toastIfActionFailed } from '~/lib/toast-message'
|
|
16
|
+
|
|
17
|
+
function isAllowedMimeType(type: string): type is R2UploadMimeType {
|
|
18
|
+
return type in R2_UPLOAD_EXTENSION_BY_MIME_TYPE
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readFileAsDataUrl(file: File): Promise<string> {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const reader = new FileReader()
|
|
24
|
+
reader.onload = () => {
|
|
25
|
+
if (typeof reader.result === 'string') {
|
|
26
|
+
resolve(reader.result)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
reject(new Error('Failed to read file'))
|
|
30
|
+
}
|
|
31
|
+
reader.onerror = () => {
|
|
32
|
+
reject(reader.error ?? new Error('Failed to read file'))
|
|
33
|
+
}
|
|
34
|
+
reader.readAsDataURL(file)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
11
37
|
|
|
38
|
+
// Discriminated union so the preview renderer knows exactly what to show
|
|
39
|
+
// without guessing from a raw URL string.
|
|
12
40
|
type PreviewState =
|
|
13
41
|
| { kind: 'image'; url: string }
|
|
14
42
|
| { kind: 'file'; name: string; mimeType: string }
|
|
@@ -16,6 +44,7 @@ type PreviewState =
|
|
|
16
44
|
|
|
17
45
|
function getInitialPreview(currentUrl?: string | null): PreviewState {
|
|
18
46
|
if (!currentUrl) return null
|
|
47
|
+
// base64 data URLs and image paths/URLs both render as images
|
|
19
48
|
return { kind: 'image', url: currentUrl }
|
|
20
49
|
}
|
|
21
50
|
|
|
@@ -32,7 +61,7 @@ function FileTypeIcon({ mimeType }: { mimeType: string }) {
|
|
|
32
61
|
type Props = {
|
|
33
62
|
/** MIME type filter passed to the file input, e.g. `"image/*"` or `"image/*,application/pdf"`. */
|
|
34
63
|
accept: string
|
|
35
|
-
/**
|
|
64
|
+
/** Organization that owns the upload; the server checks you manage it. */
|
|
36
65
|
organizationId: string
|
|
37
66
|
/**
|
|
38
67
|
* Called with the R2 object key (prod) or base64 data URL (dev mode) once
|
|
@@ -40,13 +69,13 @@ type Props = {
|
|
|
40
69
|
*/
|
|
41
70
|
onClientUploadFinish: (key: string | null) => void
|
|
42
71
|
/**
|
|
43
|
-
* Pre-resolved URL for the current value
|
|
72
|
+
* Pre-resolved URL for the current value. Pass the output of
|
|
44
73
|
* `resolveImageUrl` from the parent server component so the preview renders
|
|
45
74
|
* correctly for existing R2 keys.
|
|
46
75
|
*/
|
|
47
76
|
currentUrl?: string | null
|
|
48
77
|
className?: string
|
|
49
|
-
/** Passed to next/image `sizes
|
|
78
|
+
/** Passed to next/image `sizes`. It should match the rendered widget width. */
|
|
50
79
|
sizes?: string
|
|
51
80
|
}
|
|
52
81
|
|
|
@@ -66,12 +95,18 @@ export function FileUpload({
|
|
|
66
95
|
|
|
67
96
|
const handleFile = async (file: File) => {
|
|
68
97
|
if (file.size > MAX_FILE_SIZE_BYTES) {
|
|
69
|
-
|
|
98
|
+
toastErrorMessage(`Choose a file under ${MAX_FILE_SIZE_MB} MB.`)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!isAllowedMimeType(file.type)) {
|
|
103
|
+
toastErrorMessage('Choose a JPEG, PNG, WebP, or GIF image.')
|
|
70
104
|
return
|
|
71
105
|
}
|
|
72
106
|
|
|
73
107
|
setIsUploading(true)
|
|
74
108
|
|
|
109
|
+
// Set the preview from the file type without a network round trip.
|
|
75
110
|
if (file.type.startsWith('image/')) {
|
|
76
111
|
setPreview({ kind: 'image', url: URL.createObjectURL(file) })
|
|
77
112
|
} else {
|
|
@@ -85,8 +120,13 @@ export function FileUpload({
|
|
|
85
120
|
organizationId
|
|
86
121
|
})
|
|
87
122
|
|
|
123
|
+
if (toastIfActionFailed(result)) {
|
|
124
|
+
setPreview(getInitialPreview(currentUrl))
|
|
125
|
+
setIsUploading(false)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
88
128
|
if (!result?.data) {
|
|
89
|
-
|
|
129
|
+
toastErrorMessage('The upload could not start. Try again.')
|
|
90
130
|
setPreview(getInitialPreview(currentUrl))
|
|
91
131
|
setIsUploading(false)
|
|
92
132
|
return
|
|
@@ -94,8 +134,8 @@ export function FileUpload({
|
|
|
94
134
|
|
|
95
135
|
const { data } = result
|
|
96
136
|
|
|
97
|
-
|
|
98
|
-
|
|
137
|
+
try {
|
|
138
|
+
if (data.mode === 'r2') {
|
|
99
139
|
const res = await fetch(data.uploadUrl, {
|
|
100
140
|
method: 'PUT',
|
|
101
141
|
body: file,
|
|
@@ -103,27 +143,25 @@ export function FileUpload({
|
|
|
103
143
|
})
|
|
104
144
|
if (!res.ok) throw new Error(`R2 upload failed: ${res.status}`)
|
|
105
145
|
onClientUploadFinish(data.key)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} else {
|
|
111
|
-
const reader = new FileReader()
|
|
112
|
-
reader.onload = e => {
|
|
113
|
-
const base64 = e.target?.result as string
|
|
146
|
+
} else {
|
|
147
|
+
// Development mode has no R2 credentials, so store base64 data directly.
|
|
148
|
+
// Await the read so isUploading stays true until onClientUploadFinish runs.
|
|
149
|
+
const base64 = await readFileAsDataUrl(file)
|
|
114
150
|
if (file.type.startsWith('image/')) {
|
|
115
151
|
setPreview({ kind: 'image', url: base64 })
|
|
116
152
|
}
|
|
117
153
|
onClientUploadFinish(base64)
|
|
118
154
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
155
|
+
} catch {
|
|
156
|
+
toastErrorMessage(
|
|
157
|
+
data.mode === 'r2'
|
|
158
|
+
? 'The upload failed. Check your connection and try again.'
|
|
159
|
+
: 'The file could not be read. Choose it again.'
|
|
160
|
+
)
|
|
161
|
+
setPreview(getInitialPreview(currentUrl))
|
|
162
|
+
} finally {
|
|
163
|
+
setIsUploading(false)
|
|
124
164
|
}
|
|
125
|
-
|
|
126
|
-
setIsUploading(false)
|
|
127
165
|
}
|
|
128
166
|
|
|
129
167
|
const handleClear = (e: React.MouseEvent) => {
|
|
@@ -187,7 +225,7 @@ export function FileUpload({
|
|
|
187
225
|
Click to upload
|
|
188
226
|
</span>
|
|
189
227
|
<span className="text-muted-foreground/60 text-[11px]">
|
|
190
|
-
Max {MAX_FILE_SIZE_MB}MB
|
|
228
|
+
Max {MAX_FILE_SIZE_MB} MB
|
|
191
229
|
</span>
|
|
192
230
|
</div>
|
|
193
231
|
)}
|
|
@@ -206,6 +244,7 @@ export function FileUpload({
|
|
|
206
244
|
size="icon"
|
|
207
245
|
className="absolute -top-2 -right-2 h-6 w-6 rounded-full shadow-sm"
|
|
208
246
|
onClick={handleClear}
|
|
247
|
+
aria-label="Remove file"
|
|
209
248
|
>
|
|
210
249
|
<X className="h-3 w-3" />
|
|
211
250
|
</Button>
|
|
@@ -1,13 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Page Transition Loader Component
|
|
3
|
+
*
|
|
4
|
+
* Top progress bar shown during navigation, using the theme primary color.
|
|
5
|
+
*
|
|
6
|
+
* @module components/loader
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
'use client'
|
|
2
10
|
|
|
3
11
|
import NextTopLoader from 'nextjs-toploader'
|
|
4
12
|
|
|
5
13
|
import { FC } from 'react'
|
|
6
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Page Loader Component
|
|
17
|
+
*
|
|
18
|
+
* Renders the NextTopLoader with preset configuration.
|
|
19
|
+
* Should be included once near the root layout.
|
|
20
|
+
*
|
|
21
|
+
* @returns {JSX.Element} Top loading bar component
|
|
22
|
+
*/
|
|
7
23
|
export const Loader: FC = () => {
|
|
8
24
|
return (
|
|
9
25
|
<NextTopLoader
|
|
10
|
-
color="
|
|
26
|
+
color="var(--primary)"
|
|
11
27
|
initialPosition={0.08}
|
|
12
28
|
crawlSpeed={200}
|
|
13
29
|
height={3}
|
|
@@ -15,7 +31,7 @@ export const Loader: FC = () => {
|
|
|
15
31
|
showSpinner={true}
|
|
16
32
|
easing="ease"
|
|
17
33
|
speed={200}
|
|
18
|
-
shadow=
|
|
34
|
+
shadow="0 0 10px var(--primary), 0 0 5px var(--primary)"
|
|
19
35
|
template='<div class="bar" role="bar"><div class="peg"></div></div>
|
|
20
36
|
<div class="spinner" role="spinner"><div class="spinner-icon"></div></div>'
|
|
21
37
|
zIndex={10000}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Image from 'next/image'
|
|
2
|
+
import { cn } from '~/lib/cn'
|
|
3
|
+
import { siteConfig } from '~/lib/siteConfig'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Brand mark, with the wordmark beside it unless `showName` is false.
|
|
7
|
+
*
|
|
8
|
+
* Two SVG files rather than one: the mark uses two colors, so `currentColor`
|
|
9
|
+
* cannot recolor it for both themes.
|
|
10
|
+
*/
|
|
11
|
+
export function Logo({
|
|
12
|
+
className,
|
|
13
|
+
size = 36,
|
|
14
|
+
showName = true,
|
|
15
|
+
priority = false
|
|
16
|
+
}: {
|
|
17
|
+
className?: string
|
|
18
|
+
size?: number
|
|
19
|
+
showName?: boolean
|
|
20
|
+
priority?: boolean
|
|
21
|
+
}) {
|
|
22
|
+
const dimensions = { width: size, height: size }
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<span className={cn('flex items-center gap-2.5', className)}>
|
|
26
|
+
<Image
|
|
27
|
+
src={siteConfig.logo}
|
|
28
|
+
alt=""
|
|
29
|
+
aria-hidden
|
|
30
|
+
{...dimensions}
|
|
31
|
+
style={dimensions}
|
|
32
|
+
className="shrink-0 dark:hidden"
|
|
33
|
+
priority={priority}
|
|
34
|
+
/>
|
|
35
|
+
<Image
|
|
36
|
+
src={siteConfig.logoDark}
|
|
37
|
+
alt=""
|
|
38
|
+
aria-hidden
|
|
39
|
+
{...dimensions}
|
|
40
|
+
style={dimensions}
|
|
41
|
+
className="hidden shrink-0 dark:block"
|
|
42
|
+
priority={priority}
|
|
43
|
+
/>
|
|
44
|
+
<span className={cn('font-bold tracking-tight', !showName && 'sr-only')}>
|
|
45
|
+
{siteConfig.name}
|
|
46
|
+
</span>
|
|
47
|
+
</span>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -32,7 +32,7 @@ export function NavigationSearch({
|
|
|
32
32
|
groups,
|
|
33
33
|
dialogDescription,
|
|
34
34
|
emptyMessage = 'No results found.',
|
|
35
|
-
buttonLabel = 'Search
|
|
35
|
+
buttonLabel = 'Search…',
|
|
36
36
|
buttonClassName = 'text-muted-foreground relative h-9 w-36 justify-start rounded-md text-sm sm:w-64 sm:pr-12'
|
|
37
37
|
}: NavigationSearchProps) {
|
|
38
38
|
const router = useRouter()
|
|
@@ -111,7 +111,7 @@ export function NavigationSearch({
|
|
|
111
111
|
description={dialogDescription}
|
|
112
112
|
>
|
|
113
113
|
<Command>
|
|
114
|
-
<CommandInput placeholder="Type to search
|
|
114
|
+
<CommandInput placeholder="Type to search…" />
|
|
115
115
|
<CommandList>
|
|
116
116
|
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
|
117
117
|
|
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
*/
|
|
1
7
|
import { FC, ReactNode } from 'react'
|
|
2
8
|
|
|
3
9
|
export const PageHeading = ({
|
|
@@ -1,12 +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
|
+
|
|
1
18
|
'use client'
|
|
2
19
|
|
|
3
20
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
|
-
import { FC, ReactNode } from 'react'
|
|
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
|
+
}
|
|
5
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
|
+
*/
|
|
6
45
|
export const ReactQueryProvider: FC<{
|
|
7
46
|
children?: ReactNode
|
|
8
47
|
}> = ({ children }) => {
|
|
9
|
-
const client =
|
|
48
|
+
const [client] = useState(createQueryClient)
|
|
10
49
|
|
|
11
50
|
return <QueryClientProvider client={client}>{children}</QueryClientProvider>
|
|
12
51
|
}
|