@podosoft/podokit 0.3.0 → 0.5.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 +2 -2
- package/dist/add.d.ts +2 -0
- package/dist/add.js +5 -1
- package/dist/assemble.d.ts +14 -0
- package/dist/assemble.js +60 -0
- package/dist/create.d.ts +2 -0
- package/dist/create.js +7 -0
- package/dist/eject.d.ts +11 -0
- package/dist/eject.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +122 -1
- package/dist/inspect.d.ts +43 -0
- package/dist/inspect.js +82 -0
- package/dist/lockfile.d.ts +80 -0
- package/dist/lockfile.js +193 -0
- package/dist/templates/base/dot-env.example +11 -6
- package/dist/templates/fullstack-nest-svelte/README.md +2 -2
- package/dist/templates/fullstack-nest-svelte/apps/api/package.json +21 -2
- package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +6 -3
- package/dist/templates/fullstack-nest-svelte/apps/api/src/common/app-exception.ts +4 -12
- package/dist/templates/fullstack-nest-svelte/apps/api/src/config/env.validation.ts +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/api/src/main.ts +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/api/tsconfig.json +14 -5
- package/dist/templates/fullstack-nest-svelte/apps/web/package.json +4 -3
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/api.ts +5 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-content.svelte +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-item.svelte +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/index.ts +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/switch.svelte +29 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/api.ts +19 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +44 -5
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+page.svelte +8 -3
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/api/[...path]/+server.ts +12 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/api/auth/[...all]/+server.ts +12 -0
- package/dist/templates/fullstack-nest-svelte/dot-env.example +10 -5
- package/dist/templates/fullstack-nest-svelte/dot-gitignore +5 -0
- package/dist/templates/fullstack-nest-svelte/infra/docker/docker-compose.yml +37 -0
- package/dist/templates/fullstack-nest-svelte/infra/docker/sms-sink.mjs +40 -0
- package/dist/templates/fullstack-nest-svelte/package.json +11 -3
- package/dist/templates/fullstack-nest-svelte/tests/README.md +15 -0
- package/dist/templates/fullstack-nest-svelte/tests/api/health.api.spec.ts +14 -0
- package/dist/templates/fullstack-nest-svelte/tests/package.json +15 -0
- package/dist/templates/fullstack-nest-svelte/tests/playwright.config.ts +25 -0
- package/dist/templates/fullstack-nest-svelte/tests/ui/smoke.ui.spec.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +20 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/app-sidebar.svelte +65 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +149 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/language-switch.svelte +33 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +44 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-pagination.svelte +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-toolbar.svelte +93 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/theme-toggle.svelte +27 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/collapsible/collapsible-content.svelte +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/collapsible/collapsible-trigger.svelte +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/collapsible/collapsible.svelte +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/collapsible/index.ts +13 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/index.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-content.svelte +20 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-ellipsis.svelte +22 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-item.svelte +14 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-link.svelte +42 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-next-button.svelte +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-next.svelte +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-prev-button.svelte +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-previous.svelte +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination.svelte +28 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/index.ts +34 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-close.svelte +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-content.svelte +55 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-description.svelte +17 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-footer.svelte +20 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-header.svelte +20 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-overlay.svelte +17 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-portal.svelte +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-title.svelte +17 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet.svelte +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/constants.ts +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/context.svelte.ts +81 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/index.ts +75 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-content.svelte +24 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-footer.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-group-action.svelte +33 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-group-content.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-group-label.svelte +33 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-group.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-header.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-input.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-inset.svelte +20 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-action.svelte +37 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte +24 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-button.svelte +102 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-item.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte +36 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-provider.svelte +53 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-rail.svelte +36 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-separator.svelte +19 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-trigger.svelte +36 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar.svelte +108 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/user-avatar.svelte +12 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/hooks/is-mobile.svelte.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/index.ts +27 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +575 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +22 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +30 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.svelte +50 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/accept-invitation/[id]/+page.svelte +36 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/+page.svelte +45 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.server.ts +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.svelte +685 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.server.ts +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.svelte +123 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.server.ts +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.svelte +264 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.server.ts +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.svelte +156 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.server.ts +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +322 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.server.ts +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.svelte +552 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/+layout.svelte +13 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/forgot-password/+page.svelte +61 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.server.ts +14 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +181 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/reset-password/+page.svelte +51 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +76 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/verify-email/+page.svelte +63 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +22 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.svelte +20 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+page.svelte +22 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte +54 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +97 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +80 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +306 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +120 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +134 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +6 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/hydration.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +49 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/totp.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +26 -0
- package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +40 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +126 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +16 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +45 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +25 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +32 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +60 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +42 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +22 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +13 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +57 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +52 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +36 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +22 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +34 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sidebar.ui.spec.ts +16 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +32 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +145 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +51 -0
- package/dist/templates/modules/admin-dashboard/module.manifest.json +47 -0
- package/dist/templates/modules/api-key-auth/module.manifest.json +3 -3
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/README.md +59 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-enabled.ts +16 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-events.ts +34 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +74 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-log.entity.ts +29 -8
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.controller.ts +7 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.decorator.ts +23 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.interceptor.ts +38 -21
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +2 -1
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +36 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1720300000000-InitAuditLogs.ts +10 -4
- package/dist/templates/modules/audit-log/module.manifest.json +37 -8
- package/dist/templates/modules/auth/files/apps/api/src/account/account.controller.ts +85 -3
- package/dist/templates/modules/auth/files/apps/api/src/account/account.module.ts +2 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +73 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +199 -31
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +12 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +56 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/org-permissions.ts +22 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/permissions.ts +23 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.controller.ts +33 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.entity.ts +26 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.module.ts +12 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +118 -0
- package/dist/templates/modules/auth/files/apps/api/src/mail/mailer.ts +65 -0
- package/dist/templates/modules/auth/files/apps/api/src/migrations/1720400000000-InitAppSettings.ts +38 -0
- package/dist/templates/modules/auth/files/apps/api/src/migrations/1720500000000-InitAuthConfig.ts +27 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/app-setting.entity.ts +16 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +25 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.module.ts +11 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.ts +57 -0
- package/dist/templates/modules/auth/files/apps/api/src/sms/sms.ts +23 -0
- package/dist/templates/modules/auth/module.manifest.json +54 -20
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +6 -3
- package/dist/templates/modules/bullmq/module.manifest.json +4 -4
- package/dist/templates/modules/file-upload/module.manifest.json +3 -3
- package/dist/templates/modules/job-progress/module.manifest.json +9 -9
- package/dist/templates/modules/logging/module.manifest.json +3 -3
- package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -3
- package/dist/templates/modules/object-storage-s3/module.manifest.json +5 -5
- package/dist/templates/modules/rate-limit/module.manifest.json +2 -2
- package/dist/templates/modules/redis/module.manifest.json +4 -4
- package/dist/templates/modules/sse/module.manifest.json +4 -4
- package/dist/templates/todo/README.md +2 -2
- package/dist/templates/todo/apps/api/package.json +21 -2
- package/dist/templates/todo/apps/api/src/app.module.ts +6 -3
- package/dist/templates/todo/apps/api/src/common/app-exception.ts +4 -12
- package/dist/templates/todo/apps/api/src/config/env.validation.ts +1 -1
- package/dist/templates/todo/apps/api/src/main.ts +1 -1
- package/dist/templates/todo/apps/api/tsconfig.json +14 -5
- package/dist/templates/todo/apps/web/package.json +4 -3
- package/dist/templates/todo/apps/web/src/lib/api.ts +5 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/switch/index.ts +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/switch/switch.svelte +29 -0
- package/dist/templates/todo/apps/web/src/lib/server/api.ts +19 -0
- package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +44 -5
- package/dist/templates/todo/apps/web/src/routes/+page.svelte +19 -18
- package/dist/templates/todo/apps/web/src/routes/api/[...path]/+server.ts +12 -0
- package/dist/templates/todo/apps/web/src/routes/api/auth/[...all]/+server.ts +12 -0
- package/dist/templates/todo/dot-env.example +10 -5
- package/dist/templates/todo/dot-gitignore +5 -0
- package/dist/templates/todo/infra/docker/docker-compose.yml +4 -0
- package/dist/templates/todo/package.json +11 -3
- package/dist/templates/todo/tests/README.md +15 -0
- package/dist/templates/todo/tests/api/health.api.spec.ts +14 -0
- package/dist/templates/todo/tests/package.json +15 -0
- package/dist/templates/todo/tests/playwright.config.ts +25 -0
- package/dist/templates/todo/tests/ui/smoke.ui.spec.ts +8 -0
- package/dist/update.d.ts +49 -0
- package/dist/update.js +158 -0
- package/package.json +2 -2
- package/dist/templates/fullstack-nest-svelte/apps/api/test/health.e2e-spec.ts +0 -23
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/api/health/+server.ts +0 -12
- package/dist/templates/todo/apps/api/test/health.e2e-spec.ts +0 -23
- package/dist/templates/todo/apps/web/src/routes/api/health/+server.ts +0 -12
- package/dist/templates/todo/apps/web/src/routes/api/todos/+server.ts +0 -24
- package/dist/templates/todo/apps/web/src/routes/api/todos/[id]/+server.ts +0 -24
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
services:
|
|
2
|
+
# --- Runtime (always started by `docker compose up`) ---
|
|
2
3
|
postgres:
|
|
3
4
|
image: postgres:16-alpine
|
|
4
5
|
environment:
|
|
@@ -15,8 +16,11 @@ services:
|
|
|
15
16
|
timeout: 5s
|
|
16
17
|
retries: 5
|
|
17
18
|
|
|
19
|
+
# --- Runtime, but only needed by cache/queue modules (redis, bullmq,
|
|
20
|
+
# rate-limit, job-progress, sse). Start with: docker compose --profile cache up
|
|
18
21
|
redis:
|
|
19
22
|
image: redis:7-alpine
|
|
23
|
+
profiles: ["cache"]
|
|
20
24
|
ports:
|
|
21
25
|
- "${REDIS_PORT:-6379}:6379"
|
|
22
26
|
healthcheck:
|
|
@@ -25,5 +29,38 @@ services:
|
|
|
25
29
|
timeout: 5s
|
|
26
30
|
retries: 5
|
|
27
31
|
|
|
32
|
+
# --- Development / testing tools (NOT for production) ---
|
|
33
|
+
# Start with: docker compose --profile dev up
|
|
34
|
+
# Local email catcher: SMTP on 1025, web UI + REST API on 8025 (http://localhost:8025).
|
|
35
|
+
mailpit:
|
|
36
|
+
image: axllent/mailpit:latest
|
|
37
|
+
profiles: ["dev"]
|
|
38
|
+
ports:
|
|
39
|
+
- "${SMTP_PORT:-1025}:1025"
|
|
40
|
+
- "${MAILPIT_UI_PORT:-8025}:8025"
|
|
41
|
+
healthcheck:
|
|
42
|
+
test: ["CMD", "wget", "-qO-", "http://localhost:8025/readyz"]
|
|
43
|
+
interval: 5s
|
|
44
|
+
timeout: 5s
|
|
45
|
+
retries: 5
|
|
46
|
+
|
|
47
|
+
# Local SMS catcher (the "Mailpit for SMS"): the app posts outgoing SMS here via
|
|
48
|
+
# SMS_WEBHOOK_URL and tests read them over REST. Development only.
|
|
49
|
+
sms-sink:
|
|
50
|
+
image: node:20-alpine
|
|
51
|
+
profiles: ["dev"]
|
|
52
|
+
command: ["node", "/app/sms-sink.mjs"]
|
|
53
|
+
environment:
|
|
54
|
+
PORT: "8095"
|
|
55
|
+
ports:
|
|
56
|
+
- "${SMS_SINK_PORT:-8095}:8095"
|
|
57
|
+
volumes:
|
|
58
|
+
- ./sms-sink.mjs:/app/sms-sink.mjs:ro
|
|
59
|
+
healthcheck:
|
|
60
|
+
test: ["CMD", "wget", "-qO-", "http://localhost:8095/readyz"]
|
|
61
|
+
interval: 5s
|
|
62
|
+
timeout: 5s
|
|
63
|
+
retries: 5
|
|
64
|
+
|
|
28
65
|
volumes:
|
|
29
66
|
pgdata:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Dev/test SMS sink — the "Mailpit for SMS". The app posts outgoing SMS here
|
|
2
|
+
// (via SMS_WEBHOOK_URL) instead of a real provider, and tests read them back
|
|
3
|
+
// over REST. Development only; never runs in production (see the `dev` profile
|
|
4
|
+
// in docker-compose.yml). Uses only Node built-ins, run on a stock node image.
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
|
|
7
|
+
const PORT = Number(process.env.PORT ?? 8095);
|
|
8
|
+
/** @type {{to: string, body: string, receivedAt: string}[]} */
|
|
9
|
+
let messages = [];
|
|
10
|
+
|
|
11
|
+
const send = (res, status, body) => {
|
|
12
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
13
|
+
res.end(JSON.stringify(body));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
createServer((req, res) => {
|
|
17
|
+
const { method, url } = req;
|
|
18
|
+
if (method === "GET" && url === "/readyz") return send(res, 200, { status: "ok" });
|
|
19
|
+
if (method === "GET" && url === "/messages") return send(res, 200, messages);
|
|
20
|
+
if (method === "DELETE" && url === "/messages") {
|
|
21
|
+
messages = [];
|
|
22
|
+
return send(res, 200, { cleared: true });
|
|
23
|
+
}
|
|
24
|
+
if (method === "POST" && url === "/sms") {
|
|
25
|
+
let raw = "";
|
|
26
|
+
req.on("data", (c) => (raw += c));
|
|
27
|
+
req.on("end", () => {
|
|
28
|
+
let payload = {};
|
|
29
|
+
try {
|
|
30
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
31
|
+
} catch {
|
|
32
|
+
payload = { body: raw };
|
|
33
|
+
}
|
|
34
|
+
messages.unshift({ to: payload.to ?? payload.phoneNumber ?? "", body: payload.body ?? payload.code ?? raw, receivedAt: new Date().toISOString() });
|
|
35
|
+
send(res, 202, { accepted: true });
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
send(res, 404, { error: "not found" });
|
|
40
|
+
}).listen(PORT, () => console.log(`[sms-sink] listening on ${PORT}`));
|
|
@@ -2,12 +2,20 @@
|
|
|
2
2
|
"name": "{{projectName}}",
|
|
3
3
|
"version": "0.0.0",
|
|
4
4
|
"private": true,
|
|
5
|
-
"engines": {
|
|
6
|
-
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=20"
|
|
7
|
+
},
|
|
8
|
+
"workspaces": [
|
|
9
|
+
"apps/*",
|
|
10
|
+
"tests"
|
|
11
|
+
],
|
|
7
12
|
"scripts": {
|
|
8
13
|
"dev": "npm run dev --workspaces --if-present",
|
|
9
14
|
"build": "npm run build --workspaces --if-present",
|
|
10
15
|
"lint": "npm run lint --workspaces --if-present",
|
|
11
|
-
"test": "npm run test --workspaces --if-present"
|
|
16
|
+
"test": "npm run test --workspaces --if-present",
|
|
17
|
+
"test:e2e": "npm run test:e2e -w tests",
|
|
18
|
+
"test:e2e:ui": "npm run test:e2e:ui -w tests",
|
|
19
|
+
"test:e2e:api": "npm run test:e2e:api -w tests"
|
|
12
20
|
}
|
|
13
21
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# tests
|
|
2
|
+
|
|
3
|
+
End-to-end tests for this app, run with [`@playwright/test`](https://playwright.dev):
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm run test:e2e # all specs (api + ui)
|
|
7
|
+
npm run test:e2e:api # backend endpoints (request-only)
|
|
8
|
+
npm run test:e2e:ui # pages (chromium)
|
|
9
|
+
npm run test:e2e:report # open the HTML report
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The suite runs against a live stack (web on `E2E_BASE_URL`, default
|
|
13
|
+
`http://localhost:5001`, proxying `/api/*` to the API). Start PostgreSQL, run the
|
|
14
|
+
auth migration, and start the API + web first. Files ending in `*.api.spec.ts` run
|
|
15
|
+
in the request-only `api` project; `*.ui.spec.ts` run in the chromium `ui` project.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
// Health endpoints are public and present in every generated app.
|
|
4
|
+
test("GET /health is public and ok @smoke", async ({ request }) => {
|
|
5
|
+
const res = await request.get("/api/health");
|
|
6
|
+
expect(res.ok()).toBeTruthy();
|
|
7
|
+
expect((await res.json()).status).toBe("ok");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("GET /health/ready reports the database", async ({ request }) => {
|
|
11
|
+
const res = await request.get("/api/health/ready");
|
|
12
|
+
expect(res.status()).toBe(200);
|
|
13
|
+
expect((await res.json()).status).toBe("ready");
|
|
14
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}-tests",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test:e2e": "playwright test",
|
|
7
|
+
"test:e2e:ui": "playwright test --project=ui",
|
|
8
|
+
"test:e2e:api": "playwright test --project=api",
|
|
9
|
+
"test:e2e:report": "playwright show-report"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@playwright/test": "^1.61.1",
|
|
13
|
+
"otpauth": "^9.5.1"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
// The suite runs against a live stack: web on E2E_BASE_URL (proxying /api/* to
|
|
4
|
+
// the API). Start it first (see docs/testing.md) — locally via the dev harness,
|
|
5
|
+
// in CI via scripts/e2e-ci.mjs. Two projects: `api` (request-only, backend e2e)
|
|
6
|
+
// and `ui` (chromium, pages). Files are routed by suffix (*.api/*.ui.spec.ts).
|
|
7
|
+
const baseURL = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
testDir: ".",
|
|
11
|
+
fullyParallel: true,
|
|
12
|
+
forbidOnly: !!process.env.CI,
|
|
13
|
+
retries: process.env.CI ? 2 : 0,
|
|
14
|
+
workers: process.env.CI ? 1 : undefined,
|
|
15
|
+
reporter: [["list"], ["html", { open: "never" }]],
|
|
16
|
+
use: {
|
|
17
|
+
baseURL,
|
|
18
|
+
trace: "on-first-retry",
|
|
19
|
+
screenshot: "only-on-failure",
|
|
20
|
+
},
|
|
21
|
+
projects: [
|
|
22
|
+
{ name: "api", testMatch: /.*\.api\.spec\.ts/ },
|
|
23
|
+
{ name: "ui", testMatch: /.*\.ui\.spec\.ts/, use: { ...devices["Desktop Chrome"] } },
|
|
24
|
+
],
|
|
25
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
// Module-agnostic: the app responds at "/" (a page or a redirect to it).
|
|
4
|
+
test("app is reachable @smoke", async ({ page }) => {
|
|
5
|
+
const res = await page.goto("/");
|
|
6
|
+
expect(res?.status()).toBeLessThan(400);
|
|
7
|
+
await expect(page.locator("body")).toBeVisible();
|
|
8
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Locale } from "$lib/i18n/messages";
|
|
2
|
+
|
|
3
|
+
export type SessionUser = {
|
|
4
|
+
id: string;
|
|
5
|
+
email: string;
|
|
6
|
+
name: string;
|
|
7
|
+
role?: string | null;
|
|
8
|
+
image?: string | null;
|
|
9
|
+
emailVerified?: boolean;
|
|
10
|
+
twoFactorEnabled?: boolean | null;
|
|
11
|
+
username?: string | null;
|
|
12
|
+
phoneNumber?: string | null;
|
|
13
|
+
phoneNumberVerified?: boolean | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
namespace App {
|
|
18
|
+
interface Locals {
|
|
19
|
+
user: SessionUser | null;
|
|
20
|
+
session: { id: string; impersonatedBy?: string | null } | null;
|
|
21
|
+
locale: Locale;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Handle } from "@sveltejs/kit";
|
|
2
|
+
import { serverApiClient } from "$lib/server/api";
|
|
3
|
+
import { resolveLocale } from "$lib/i18n/messages";
|
|
4
|
+
|
|
5
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
6
|
+
event.locals.locale = resolveLocale(event.cookies.get("locale"));
|
|
7
|
+
try {
|
|
8
|
+
const { data } = await serverApiClient(event).auth.getSession();
|
|
9
|
+
event.locals.user = (data?.user as App.Locals["user"]) ?? null;
|
|
10
|
+
event.locals.session = data?.session
|
|
11
|
+
? { id: data.session.id, impersonatedBy: (data.session as { impersonatedBy?: string | null }).impersonatedBy ?? null }
|
|
12
|
+
: null;
|
|
13
|
+
} catch {
|
|
14
|
+
event.locals.user = null;
|
|
15
|
+
event.locals.session = null;
|
|
16
|
+
}
|
|
17
|
+
return resolve(event, {
|
|
18
|
+
transformPageChunk: ({ html }) => html.replace('lang="en"', `lang="${event.locals.locale}"`),
|
|
19
|
+
});
|
|
20
|
+
};
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/app-sidebar.svelte
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from "$app/state";
|
|
3
|
+
import * as Sidebar from "$lib/components/ui/sidebar";
|
|
4
|
+
import SidebarUserMenu from "./sidebar-user-menu.svelte";
|
|
5
|
+
import LayoutDashboardIcon from "@lucide/svelte/icons/layout-dashboard";
|
|
6
|
+
import UsersIcon from "@lucide/svelte/icons/users";
|
|
7
|
+
import MonitorSmartphoneIcon from "@lucide/svelte/icons/monitor-smartphone";
|
|
8
|
+
import ScrollTextIcon from "@lucide/svelte/icons/scroll-text";
|
|
9
|
+
import Building2Icon from "@lucide/svelte/icons/building-2";
|
|
10
|
+
import SettingsIcon from "@lucide/svelte/icons/settings";
|
|
11
|
+
import { getI18n } from "$lib/i18n";
|
|
12
|
+
import type { SessionUser } from "../../app.d.ts";
|
|
13
|
+
import type { Messages } from "$lib/i18n/messages";
|
|
14
|
+
import type { Component } from "svelte";
|
|
15
|
+
|
|
16
|
+
let { user, capabilities }: { user: SessionUser; capabilities?: { auditLog?: boolean; organization?: boolean } } = $props();
|
|
17
|
+
const i18n = getI18n();
|
|
18
|
+
|
|
19
|
+
type NavItem = { href: string; key: keyof Messages["nav"]; icon: Component; adminOnly?: boolean };
|
|
20
|
+
const items = $derived<NavItem[]>([
|
|
21
|
+
{ href: "/admin", key: "overview", icon: LayoutDashboardIcon },
|
|
22
|
+
{ href: "/admin/users", key: "users", icon: UsersIcon, adminOnly: true },
|
|
23
|
+
{ href: "/admin/sessions", key: "sessions", icon: MonitorSmartphoneIcon, adminOnly: true },
|
|
24
|
+
...(capabilities?.organization
|
|
25
|
+
? [{ href: "/admin/organizations", key: "organizations", icon: Building2Icon, adminOnly: true } as NavItem]
|
|
26
|
+
: []),
|
|
27
|
+
...(capabilities?.auditLog
|
|
28
|
+
? [{ href: "/admin/audit", key: "audit", icon: ScrollTextIcon, adminOnly: true } as NavItem]
|
|
29
|
+
: []),
|
|
30
|
+
{ href: "/admin/settings", key: "settings", icon: SettingsIcon, adminOnly: true },
|
|
31
|
+
]);
|
|
32
|
+
const visible = $derived(items.filter((item) => !item.adminOnly || user.role === "admin"));
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<Sidebar.Root collapsible="icon">
|
|
36
|
+
<Sidebar.Header>
|
|
37
|
+
<div
|
|
38
|
+
data-testid="sidebar-brand"
|
|
39
|
+
class="flex items-center gap-2 overflow-hidden px-2 py-1 font-semibold whitespace-nowrap group-data-[collapsible=icon]:hidden"
|
|
40
|
+
>
|
|
41
|
+
{i18n.t.common.appName}
|
|
42
|
+
</div>
|
|
43
|
+
</Sidebar.Header>
|
|
44
|
+
<Sidebar.Content>
|
|
45
|
+
<Sidebar.Group>
|
|
46
|
+
<Sidebar.Menu>
|
|
47
|
+
{#each visible as item (item.href)}
|
|
48
|
+
<Sidebar.MenuItem>
|
|
49
|
+
<Sidebar.MenuButton isActive={page.url.pathname === item.href} tooltipContent={i18n.t.nav[item.key]}>
|
|
50
|
+
{#snippet child({ props })}
|
|
51
|
+
<a href={item.href} {...props}>
|
|
52
|
+
<item.icon />
|
|
53
|
+
<span>{i18n.t.nav[item.key]}</span>
|
|
54
|
+
</a>
|
|
55
|
+
{/snippet}
|
|
56
|
+
</Sidebar.MenuButton>
|
|
57
|
+
</Sidebar.MenuItem>
|
|
58
|
+
{/each}
|
|
59
|
+
</Sidebar.Menu>
|
|
60
|
+
</Sidebar.Group>
|
|
61
|
+
</Sidebar.Content>
|
|
62
|
+
<Sidebar.Footer>
|
|
63
|
+
<SidebarUserMenu {user} />
|
|
64
|
+
</Sidebar.Footer>
|
|
65
|
+
</Sidebar.Root>
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type DataTableColumn<Row> = {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
sortable?: boolean;
|
|
6
|
+
class?: string;
|
|
7
|
+
// Value used for client-side sorting (defaults to row[key]). Use for nested
|
|
8
|
+
// or derived fields, e.g. (s) => s.user.email.
|
|
9
|
+
value?: (row: Row) => string | number | boolean | Date | null | undefined;
|
|
10
|
+
};
|
|
11
|
+
export type SortState = { key: string; dir: "asc" | "desc" };
|
|
12
|
+
|
|
13
|
+
// Shared page size for all admin tables — sized so a desktop viewport shows a
|
|
14
|
+
// full list at a glance (rather than the old 5-row pages). Change it here to
|
|
15
|
+
// retune every table at once.
|
|
16
|
+
export const DEFAULT_PAGE_SIZE = 15;
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script lang="ts" generics="Row">
|
|
20
|
+
import type { Snippet } from "svelte";
|
|
21
|
+
import { Button } from "$lib/components/ui/button";
|
|
22
|
+
import * as Table from "$lib/components/ui/table";
|
|
23
|
+
import TablePagination from "$lib/components/table-pagination.svelte";
|
|
24
|
+
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
|
25
|
+
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
|
26
|
+
import ChevronsUpDownIcon from "@lucide/svelte/icons/chevrons-up-down";
|
|
27
|
+
|
|
28
|
+
// The one table used across the admin console: sortable headers + a shared
|
|
29
|
+
// pagination footer. Two modes:
|
|
30
|
+
// - client (default): pass all `rows`; sorting + paging happen here.
|
|
31
|
+
// - manual (server): set manualSort/manualPagination, pass `total`, and refetch
|
|
32
|
+
// in `onChange` using the emitted sort + page.
|
|
33
|
+
type Props = {
|
|
34
|
+
columns: DataTableColumn<Row>[];
|
|
35
|
+
rows: Row[];
|
|
36
|
+
row: Snippet<[Row]>; // renders the <Table.Cell>s for one row
|
|
37
|
+
getKey?: (row: Row, index: number) => string | number;
|
|
38
|
+
empty?: string;
|
|
39
|
+
sort?: SortState | null;
|
|
40
|
+
manualSort?: boolean;
|
|
41
|
+
page?: number;
|
|
42
|
+
perPage?: number; // 0 => no pagination footer
|
|
43
|
+
total?: number; // server total (defaults to rows.length)
|
|
44
|
+
label?: string;
|
|
45
|
+
manualPagination?: boolean;
|
|
46
|
+
onChange?: (state: { sort: SortState | null; page: number }) => void;
|
|
47
|
+
};
|
|
48
|
+
let {
|
|
49
|
+
columns,
|
|
50
|
+
rows,
|
|
51
|
+
row,
|
|
52
|
+
getKey = (_r, i) => i,
|
|
53
|
+
empty = "No data.",
|
|
54
|
+
sort = $bindable(null),
|
|
55
|
+
manualSort = false,
|
|
56
|
+
page = $bindable(1),
|
|
57
|
+
perPage = 0,
|
|
58
|
+
total,
|
|
59
|
+
label,
|
|
60
|
+
manualPagination = false,
|
|
61
|
+
onChange,
|
|
62
|
+
}: Props = $props();
|
|
63
|
+
|
|
64
|
+
function toggle(col: DataTableColumn<Row>): void {
|
|
65
|
+
if (!col.sortable) return;
|
|
66
|
+
sort =
|
|
67
|
+
sort?.key === col.key
|
|
68
|
+
? { key: col.key, dir: sort.dir === "asc" ? "desc" : "asc" }
|
|
69
|
+
: { key: col.key, dir: "asc" };
|
|
70
|
+
if (!manualPagination) page = 1;
|
|
71
|
+
onChange?.({ sort, page });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cmp(a: unknown, b: unknown): number {
|
|
75
|
+
if (a == null && b == null) return 0;
|
|
76
|
+
if (a == null) return -1;
|
|
77
|
+
if (b == null) return 1;
|
|
78
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
79
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
|
|
80
|
+
return String(a).localeCompare(String(b), undefined, { numeric: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sorted = $derived.by(() => {
|
|
84
|
+
if (manualSort || !sort) return rows;
|
|
85
|
+
const active = sort;
|
|
86
|
+
const col = columns.find((c) => c.key === active.key);
|
|
87
|
+
const read = (r: Row) => (col?.value ? col.value(r) : (r as Record<string, unknown>)[active.key]);
|
|
88
|
+
const factor = active.dir === "asc" ? 1 : -1;
|
|
89
|
+
return [...rows].sort((a, b) => cmp(read(a), read(b)) * factor);
|
|
90
|
+
});
|
|
91
|
+
const view = $derived.by(() =>
|
|
92
|
+
perPage && !manualPagination ? sorted.slice((page - 1) * perPage, page * perPage) : sorted,
|
|
93
|
+
);
|
|
94
|
+
const count = $derived(total ?? rows.length);
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<div class="rounded-md border">
|
|
98
|
+
<Table.Root>
|
|
99
|
+
<Table.Header>
|
|
100
|
+
<Table.Row>
|
|
101
|
+
{#each columns as col (col.key)}
|
|
102
|
+
<Table.Head
|
|
103
|
+
class={col.class}
|
|
104
|
+
aria-sort={sort?.key === col.key ? (sort.dir === "asc" ? "ascending" : "descending") : undefined}
|
|
105
|
+
>
|
|
106
|
+
{#if col.sortable}
|
|
107
|
+
<Button variant="ghost" size="sm" class="-mx-2 h-auto gap-1 px-2 py-1 font-medium" onclick={() => toggle(col)}>
|
|
108
|
+
{col.label}
|
|
109
|
+
{#if sort?.key === col.key}
|
|
110
|
+
{#if sort.dir === "asc"}<ChevronUpIcon class="size-3.5" />{:else}<ChevronDownIcon class="size-3.5" />{/if}
|
|
111
|
+
{:else}
|
|
112
|
+
<ChevronsUpDownIcon class="size-3.5 opacity-40" />
|
|
113
|
+
{/if}
|
|
114
|
+
</Button>
|
|
115
|
+
{:else}
|
|
116
|
+
{col.label}
|
|
117
|
+
{/if}
|
|
118
|
+
</Table.Head>
|
|
119
|
+
{/each}
|
|
120
|
+
</Table.Row>
|
|
121
|
+
</Table.Header>
|
|
122
|
+
<Table.Body>
|
|
123
|
+
{#each view as r, i (getKey(r, i))}
|
|
124
|
+
<Table.Row>{@render row(r)}</Table.Row>
|
|
125
|
+
{:else}
|
|
126
|
+
<Table.Row>
|
|
127
|
+
<Table.Cell colspan={columns.length} class="text-muted-foreground py-8 text-center">{empty}</Table.Cell>
|
|
128
|
+
</Table.Row>
|
|
129
|
+
{/each}
|
|
130
|
+
</Table.Body>
|
|
131
|
+
<!-- Pagination lives in the table footer so every DataTable is consistent
|
|
132
|
+
top-to-bottom (sortable header, rows, footer pager). -->
|
|
133
|
+
{#if perPage}
|
|
134
|
+
<Table.Footer class="bg-transparent">
|
|
135
|
+
<Table.Row class="hover:bg-transparent">
|
|
136
|
+
<Table.Cell colspan={columns.length} class="py-2">
|
|
137
|
+
<TablePagination
|
|
138
|
+
{count}
|
|
139
|
+
{perPage}
|
|
140
|
+
bind:page
|
|
141
|
+
{label}
|
|
142
|
+
onPageChange={manualPagination ? (p) => onChange?.({ sort, page: p }) : undefined}
|
|
143
|
+
/>
|
|
144
|
+
</Table.Cell>
|
|
145
|
+
</Table.Row>
|
|
146
|
+
</Table.Footer>
|
|
147
|
+
{/if}
|
|
148
|
+
</Table.Root>
|
|
149
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { invalidateAll } from "$app/navigation";
|
|
3
|
+
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
|
4
|
+
import { Button } from "$lib/components/ui/button";
|
|
5
|
+
import GlobeIcon from "@lucide/svelte/icons/globe";
|
|
6
|
+
import { getI18n } from "$lib/i18n";
|
|
7
|
+
import { LOCALES, localeNames, type Locale } from "$lib/i18n/messages";
|
|
8
|
+
|
|
9
|
+
const i18n = getI18n();
|
|
10
|
+
|
|
11
|
+
async function change(locale: Locale): Promise<void> {
|
|
12
|
+
document.cookie = `locale=${locale};path=/;max-age=31536000;samesite=lax`;
|
|
13
|
+
document.documentElement.lang = locale;
|
|
14
|
+
await invalidateAll();
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<DropdownMenu.Root>
|
|
19
|
+
<DropdownMenu.Trigger>
|
|
20
|
+
{#snippet child({ props })}
|
|
21
|
+
<Button {...props} variant="ghost" size="icon" aria-label={i18n.t.language.label}>
|
|
22
|
+
<GlobeIcon class="size-4" />
|
|
23
|
+
</Button>
|
|
24
|
+
{/snippet}
|
|
25
|
+
</DropdownMenu.Trigger>
|
|
26
|
+
<DropdownMenu.Content align="end">
|
|
27
|
+
{#each LOCALES as locale (locale)}
|
|
28
|
+
<DropdownMenu.Item onSelect={() => change(locale)} class={i18n.locale === locale ? "font-medium" : ""}>
|
|
29
|
+
{localeNames[locale]}
|
|
30
|
+
</DropdownMenu.Item>
|
|
31
|
+
{/each}
|
|
32
|
+
</DropdownMenu.Content>
|
|
33
|
+
</DropdownMenu.Root>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from "$app/navigation";
|
|
3
|
+
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
|
4
|
+
import * as Sidebar from "$lib/components/ui/sidebar";
|
|
5
|
+
import UserAvatar from "./user-avatar.svelte";
|
|
6
|
+
import ChevronsUpDownIcon from "@lucide/svelte/icons/chevrons-up-down";
|
|
7
|
+
import LogOutIcon from "@lucide/svelte/icons/log-out";
|
|
8
|
+
import UserIcon from "@lucide/svelte/icons/user";
|
|
9
|
+
import { api } from "$lib/api";
|
|
10
|
+
import { getI18n } from "$lib/i18n";
|
|
11
|
+
import type { SessionUser } from "../../app.d.ts";
|
|
12
|
+
|
|
13
|
+
let { user }: { user: SessionUser } = $props();
|
|
14
|
+
const i18n = getI18n();
|
|
15
|
+
|
|
16
|
+
async function signOut(): Promise<void> {
|
|
17
|
+
await api.auth.signOut();
|
|
18
|
+
await goto("/login", { invalidateAll: true });
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<Sidebar.Menu>
|
|
23
|
+
<Sidebar.MenuItem>
|
|
24
|
+
<DropdownMenu.Root>
|
|
25
|
+
<DropdownMenu.Trigger>
|
|
26
|
+
{#snippet child({ props })}
|
|
27
|
+
<Sidebar.MenuButton {...props} size="lg" class="data-[state=open]:bg-sidebar-accent">
|
|
28
|
+
<UserAvatar {user} class="size-8" />
|
|
29
|
+
<div class="flex flex-col text-left text-sm leading-tight">
|
|
30
|
+
<span class="truncate font-medium">{user.name}</span>
|
|
31
|
+
<span class="text-muted-foreground truncate text-xs">{user.email}</span>
|
|
32
|
+
</div>
|
|
33
|
+
<ChevronsUpDownIcon class="ml-auto size-4" />
|
|
34
|
+
</Sidebar.MenuButton>
|
|
35
|
+
{/snippet}
|
|
36
|
+
</DropdownMenu.Trigger>
|
|
37
|
+
<DropdownMenu.Content align="end" side="top" class="w-56">
|
|
38
|
+
<DropdownMenu.Item onSelect={() => goto("/admin/account")}><UserIcon class="mr-2 size-4" /> {i18n.t.nav.account}</DropdownMenu.Item>
|
|
39
|
+
<DropdownMenu.Separator />
|
|
40
|
+
<DropdownMenu.Item onSelect={signOut}><LogOutIcon class="mr-2 size-4" /> {i18n.t.nav.signOut}</DropdownMenu.Item>
|
|
41
|
+
</DropdownMenu.Content>
|
|
42
|
+
</DropdownMenu.Root>
|
|
43
|
+
</Sidebar.MenuItem>
|
|
44
|
+
</Sidebar.Menu>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Pagination from "$lib/components/ui/pagination";
|
|
3
|
+
|
|
4
|
+
// Shared table footer: a count label plus shadcn-svelte pagination. The pager
|
|
5
|
+
// is always shown (even for a single page or an empty list) so the footer stays
|
|
6
|
+
// consistent across tables. Bind `page` (1-based). For server-side lists pass
|
|
7
|
+
// `onPageChange` to refetch; client-side lists just bind.
|
|
8
|
+
type Props = {
|
|
9
|
+
count: number;
|
|
10
|
+
perPage?: number;
|
|
11
|
+
page?: number;
|
|
12
|
+
label?: string;
|
|
13
|
+
onPageChange?: (page: number) => void;
|
|
14
|
+
};
|
|
15
|
+
let { count, perPage = 5, page = $bindable(1), label, onPageChange }: Props = $props();
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<div class="flex items-center justify-between gap-2">
|
|
19
|
+
{#if label}
|
|
20
|
+
<span class="text-muted-foreground shrink-0 text-sm whitespace-nowrap">{label}</span>
|
|
21
|
+
{/if}
|
|
22
|
+
<Pagination.Root count={Math.max(count, 1)} {perPage} bind:page {onPageChange}>
|
|
23
|
+
{#snippet children({ pages, currentPage })}
|
|
24
|
+
<Pagination.Content>
|
|
25
|
+
<Pagination.Item><Pagination.PrevButton /></Pagination.Item>
|
|
26
|
+
{#each pages as p (p.key)}
|
|
27
|
+
{#if p.type === "ellipsis"}
|
|
28
|
+
<Pagination.Item><Pagination.Ellipsis /></Pagination.Item>
|
|
29
|
+
{:else}
|
|
30
|
+
<Pagination.Item>
|
|
31
|
+
<Pagination.Link page={p} isActive={currentPage === p.value}>{p.value}</Pagination.Link>
|
|
32
|
+
</Pagination.Item>
|
|
33
|
+
{/if}
|
|
34
|
+
{/each}
|
|
35
|
+
<Pagination.Item><Pagination.NextButton /></Pagination.Item>
|
|
36
|
+
</Pagination.Content>
|
|
37
|
+
{/snippet}
|
|
38
|
+
</Pagination.Root>
|
|
39
|
+
</div>
|