create-ej-app 26.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 +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 +46 -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 +49 -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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { RowData, SortingState } from '@tanstack/react-table'
|
|
2
|
+
import { SortOrderEnum } from './enum'
|
|
3
|
+
import type { DataTableColumnDef } from './features'
|
|
4
|
+
|
|
5
|
+
/** A single selectable option inside a filter dropdown. */
|
|
6
|
+
interface DataTableFilterOption {
|
|
7
|
+
label: string
|
|
8
|
+
value: string | boolean
|
|
9
|
+
/** Optional icon rendered next to the option label. */
|
|
10
|
+
icon?: React.ComponentType<{ className?: string }>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Describes a filter that appears in the DataTable toolbar.
|
|
15
|
+
* Filters can either be static (pre-defined `options`) or async
|
|
16
|
+
* (fetched via a server `action` on search).
|
|
17
|
+
*/
|
|
18
|
+
export interface DataTableFilter {
|
|
19
|
+
/** Unique key used to store the filter's selected values. */
|
|
20
|
+
id: string
|
|
21
|
+
/** Human-readable label shown in the filter dropdown trigger. */
|
|
22
|
+
label: string
|
|
23
|
+
/** Pre-defined options for static filters. Omit for async filters. */
|
|
24
|
+
options?: DataTableFilterOption[]
|
|
25
|
+
/** Server action called to fetch options dynamically (async filters). */
|
|
26
|
+
action?: any
|
|
27
|
+
/** Custom React-Query key for async filter caching. Defaults to `id`. */
|
|
28
|
+
queryKey?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Props accepted by the generic `DataTable` component.
|
|
33
|
+
*
|
|
34
|
+
* Supports both client-side and server-side (manual) pagination & sorting.
|
|
35
|
+
* When `manualPagination` / `manualSorting` are enabled the parent is
|
|
36
|
+
* responsible for fetching data and passing results back via `data` & `totalCount`.
|
|
37
|
+
*/
|
|
38
|
+
export interface DataTableProps<TData extends RowData> {
|
|
39
|
+
/** Column definitions (TanStack Table v9, typed to DataTable features). */
|
|
40
|
+
columns: DataTableColumnDef<TData, any>[]
|
|
41
|
+
/** Row data to display. */
|
|
42
|
+
data?: TData[]
|
|
43
|
+
/** Show skeleton loading rows while data is being fetched. */
|
|
44
|
+
isLoading?: boolean
|
|
45
|
+
/** Render the search input in the toolbar. */
|
|
46
|
+
enableSearch?: boolean
|
|
47
|
+
/** Placeholder text for the search input. */
|
|
48
|
+
searchPlaceholder?: string
|
|
49
|
+
/** Initial value for the search input (e.g. restored from URL query params). */
|
|
50
|
+
initialSearch?: string
|
|
51
|
+
/** Total row count for server-side pagination info. */
|
|
52
|
+
totalCount?: number
|
|
53
|
+
/** Callback fired when pagination, sorting, search or filters change. */
|
|
54
|
+
onParamsChange?: (params: {
|
|
55
|
+
page: number
|
|
56
|
+
limit: number
|
|
57
|
+
sortBy?: string
|
|
58
|
+
sortOrder?: SortOrderEnum
|
|
59
|
+
search?: string
|
|
60
|
+
filters?: Record<string, (string | boolean)[]>
|
|
61
|
+
}) => void
|
|
62
|
+
/** Enable server-side pagination (parent controls data fetching). */
|
|
63
|
+
manualPagination?: boolean
|
|
64
|
+
/** Enable server-side sorting (parent controls data fetching). */
|
|
65
|
+
manualSorting?: boolean
|
|
66
|
+
/** Current 0-based page index (controlled). */
|
|
67
|
+
pageIndex?: number
|
|
68
|
+
/** Number of rows per page (controlled). */
|
|
69
|
+
pageSize?: number
|
|
70
|
+
/** Default sorting state applied on mount. */
|
|
71
|
+
initialSorting?: SortingState
|
|
72
|
+
/** Filter definitions rendered in the toolbar filter dropdown. */
|
|
73
|
+
filters?: DataTableFilter[]
|
|
74
|
+
/** Currently active filter selections keyed by filter id. */
|
|
75
|
+
activeFilters?: Record<string, (string | boolean)[]>
|
|
76
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
|
|
16
|
+
import { type LucideIcon, AlertTriangle } from 'lucide-react'
|
|
17
|
+
import Link from 'next/link'
|
|
18
|
+
import { FC } from 'react'
|
|
19
|
+
import { Button } from '~/components/ui/button'
|
|
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
|
+
*/
|
|
51
|
+
export const ErrorDisplay: FC<{
|
|
52
|
+
title: string
|
|
53
|
+
message: string
|
|
54
|
+
icon?: LucideIcon
|
|
55
|
+
link?: {
|
|
56
|
+
href?: string
|
|
57
|
+
text?: string
|
|
58
|
+
}
|
|
59
|
+
}> = ({ title, message, icon, link }) => {
|
|
60
|
+
const Icon = icon || AlertTriangle
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex w-full flex-col items-center justify-center py-16">
|
|
63
|
+
<Icon className="text-muted-foreground/50 size-16" />
|
|
64
|
+
<h2 className="mt-4 text-xl font-semibold">{title}</h2>
|
|
65
|
+
<p className="text-muted-foreground mt-2">{message}</p>
|
|
66
|
+
<Button className="mt-4" asChild>
|
|
67
|
+
<Link href={link?.href || '/'}>{link?.text || 'Go Home'}</Link>
|
|
68
|
+
</Button>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Search } from 'lucide-react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
6
|
+
import {
|
|
7
|
+
SidebarNavGroup,
|
|
8
|
+
SidebarNavItem,
|
|
9
|
+
SidebarNavSubItem
|
|
10
|
+
} from '~/components/navigation-sidebar'
|
|
11
|
+
import { Button } from '~/components/ui/button'
|
|
12
|
+
import {
|
|
13
|
+
Command,
|
|
14
|
+
CommandDialog,
|
|
15
|
+
CommandEmpty,
|
|
16
|
+
CommandGroup,
|
|
17
|
+
CommandInput,
|
|
18
|
+
CommandItem,
|
|
19
|
+
CommandList,
|
|
20
|
+
CommandSeparator
|
|
21
|
+
} from '~/components/ui/command'
|
|
22
|
+
|
|
23
|
+
type NavigationSearchProps = {
|
|
24
|
+
groups: SidebarNavGroup[]
|
|
25
|
+
dialogDescription: string
|
|
26
|
+
emptyMessage?: string
|
|
27
|
+
buttonLabel?: string
|
|
28
|
+
buttonClassName?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function NavigationSearch({
|
|
32
|
+
groups,
|
|
33
|
+
dialogDescription,
|
|
34
|
+
emptyMessage = 'No results found.',
|
|
35
|
+
buttonLabel = 'Search…',
|
|
36
|
+
buttonClassName = 'text-muted-foreground relative h-9 w-36 justify-start rounded-md text-sm sm:w-64 sm:pr-12'
|
|
37
|
+
}: NavigationSearchProps) {
|
|
38
|
+
const router = useRouter()
|
|
39
|
+
const [searchOpen, setSearchOpen] = useState(false)
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
43
|
+
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
|
|
44
|
+
event.preventDefault()
|
|
45
|
+
setSearchOpen(true)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
document.addEventListener('keydown', onKeyDown)
|
|
50
|
+
return () => document.removeEventListener('keydown', onKeyDown)
|
|
51
|
+
}, [])
|
|
52
|
+
|
|
53
|
+
const navigate = useCallback(
|
|
54
|
+
(url: string) => {
|
|
55
|
+
setSearchOpen(false)
|
|
56
|
+
router.push(url)
|
|
57
|
+
},
|
|
58
|
+
[router]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const hasGroups = groups.length > 0
|
|
62
|
+
|
|
63
|
+
const normalizedGroups = useMemo(
|
|
64
|
+
() =>
|
|
65
|
+
groups.map(group => ({
|
|
66
|
+
label: group.label,
|
|
67
|
+
items: group.items.flatMap((item: SidebarNavItem) => {
|
|
68
|
+
if (item.items?.length) {
|
|
69
|
+
return item.items.map((subItem: SidebarNavSubItem) => ({
|
|
70
|
+
key: subItem.url,
|
|
71
|
+
label: `${item.title} › ${subItem.title}`,
|
|
72
|
+
value: `${group.label} ${item.title} ${subItem.title}`,
|
|
73
|
+
url: subItem.url,
|
|
74
|
+
icon: item.icon
|
|
75
|
+
}))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
key: item.url,
|
|
81
|
+
label: item.title,
|
|
82
|
+
value: `${group.label} ${item.title}`,
|
|
83
|
+
url: item.url,
|
|
84
|
+
icon: item.icon
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
})
|
|
88
|
+
})),
|
|
89
|
+
[groups]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
<Button
|
|
95
|
+
variant="outline"
|
|
96
|
+
className={buttonClassName}
|
|
97
|
+
onClick={() => setSearchOpen(true)}
|
|
98
|
+
disabled={!hasGroups}
|
|
99
|
+
>
|
|
100
|
+
<Search className="mr-2 size-4" />
|
|
101
|
+
<span>{buttonLabel}</span>
|
|
102
|
+
<kbd className="bg-muted pointer-events-none absolute top-[50%] right-1.5 hidden h-5 -translate-y-1/2 items-center gap-1 rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex">
|
|
103
|
+
<span className="text-xs">⌘</span>K
|
|
104
|
+
</kbd>
|
|
105
|
+
</Button>
|
|
106
|
+
|
|
107
|
+
<CommandDialog
|
|
108
|
+
open={searchOpen}
|
|
109
|
+
onOpenChange={setSearchOpen}
|
|
110
|
+
title="Search"
|
|
111
|
+
description={dialogDescription}
|
|
112
|
+
>
|
|
113
|
+
<Command>
|
|
114
|
+
<CommandInput placeholder="Type to search…" />
|
|
115
|
+
<CommandList>
|
|
116
|
+
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
|
117
|
+
|
|
118
|
+
{normalizedGroups.map((group, groupIndex) => (
|
|
119
|
+
<div key={group.label}>
|
|
120
|
+
{groupIndex > 0 ? <CommandSeparator /> : null}
|
|
121
|
+
<CommandGroup heading={group.label}>
|
|
122
|
+
{group.items.map(item => (
|
|
123
|
+
<CommandItem
|
|
124
|
+
key={item.key}
|
|
125
|
+
value={item.value}
|
|
126
|
+
onSelect={() => navigate(item.url)}
|
|
127
|
+
>
|
|
128
|
+
{item.icon ? <item.icon className="mr-2 size-4" /> : null}
|
|
129
|
+
<span>{item.label}</span>
|
|
130
|
+
</CommandItem>
|
|
131
|
+
))}
|
|
132
|
+
</CommandGroup>
|
|
133
|
+
</div>
|
|
134
|
+
))}
|
|
135
|
+
</CommandList>
|
|
136
|
+
</Command>
|
|
137
|
+
</CommandDialog>
|
|
138
|
+
</>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -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
|
+
}
|