create-mithaq 0.0.0-stage → 0.2.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/LICENSE +22 -0
- package/LICENSES/openstatus-time-picker.txt +21 -0
- package/LICENSES/payload.txt +22 -0
- package/LICENSES/ponytail.txt +21 -0
- package/index.js +521 -0
- package/package.json +19 -4
- package/template/apps/asas-backoffice/components.json +25 -0
- package/template/apps/asas-backoffice/gitignore +3 -0
- package/template/apps/asas-backoffice/package.json +57 -0
- package/template/apps/asas-backoffice/public/favicon.svg +1 -0
- package/template/apps/asas-backoffice/src/api.ts +147 -0
- package/template/apps/asas-backoffice/src/auth-frame.tsx +150 -0
- package/template/apps/asas-backoffice/src/components/ui/accordion.tsx +74 -0
- package/template/apps/asas-backoffice/src/components/ui/alert-dialog.tsx +185 -0
- package/template/apps/asas-backoffice/src/components/ui/avatar.tsx +106 -0
- package/template/apps/asas-backoffice/src/components/ui/badge.tsx +48 -0
- package/template/apps/asas-backoffice/src/components/ui/breadcrumb.tsx +124 -0
- package/template/apps/asas-backoffice/src/components/ui/button-group.tsx +87 -0
- package/template/apps/asas-backoffice/src/components/ui/button.tsx +55 -0
- package/template/apps/asas-backoffice/src/components/ui/calendar.tsx +223 -0
- package/template/apps/asas-backoffice/src/components/ui/card.tsx +102 -0
- package/template/apps/asas-backoffice/src/components/ui/chart.tsx +371 -0
- package/template/apps/asas-backoffice/src/components/ui/checkbox.tsx +28 -0
- package/template/apps/asas-backoffice/src/components/ui/collapsible.tsx +19 -0
- package/template/apps/asas-backoffice/src/components/ui/combobox.tsx +308 -0
- package/template/apps/asas-backoffice/src/components/ui/dialog.tsx +158 -0
- package/template/apps/asas-backoffice/src/components/ui/direction.tsx +6 -0
- package/template/apps/asas-backoffice/src/components/ui/directional-icon.tsx +20 -0
- package/template/apps/asas-backoffice/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/apps/asas-backoffice/src/components/ui/field.tsx +236 -0
- package/template/apps/asas-backoffice/src/components/ui/input-group.tsx +155 -0
- package/template/apps/asas-backoffice/src/components/ui/input-otp.tsx +84 -0
- package/template/apps/asas-backoffice/src/components/ui/input.tsx +19 -0
- package/template/apps/asas-backoffice/src/components/ui/label.tsx +19 -0
- package/template/apps/asas-backoffice/src/components/ui/popover.tsx +90 -0
- package/template/apps/asas-backoffice/src/components/ui/radio-group.tsx +37 -0
- package/template/apps/asas-backoffice/src/components/ui/resizable.tsx +47 -0
- package/template/apps/asas-backoffice/src/components/ui/scroll-area.tsx +54 -0
- package/template/apps/asas-backoffice/src/components/ui/select.tsx +204 -0
- package/template/apps/asas-backoffice/src/components/ui/separator.tsx +24 -0
- package/template/apps/asas-backoffice/src/components/ui/sheet.tsx +139 -0
- package/template/apps/asas-backoffice/src/components/ui/sidebar.tsx +714 -0
- package/template/apps/asas-backoffice/src/components/ui/skeleton.tsx +13 -0
- package/template/apps/asas-backoffice/src/components/ui/slider.tsx +55 -0
- package/template/apps/asas-backoffice/src/components/ui/spinner.tsx +10 -0
- package/template/apps/asas-backoffice/src/components/ui/switch.tsx +29 -0
- package/template/apps/asas-backoffice/src/components/ui/table.tsx +115 -0
- package/template/apps/asas-backoffice/src/components/ui/tabs.tsx +79 -0
- package/template/apps/asas-backoffice/src/components/ui/textarea.tsx +17 -0
- package/template/apps/asas-backoffice/src/components/ui/toggle-group.tsx +89 -0
- package/template/apps/asas-backoffice/src/components/ui/toggle.tsx +44 -0
- package/template/apps/asas-backoffice/src/components/ui/tooltip.tsx +63 -0
- package/template/apps/asas-backoffice/src/components.ts +35 -0
- package/template/apps/asas-backoffice/src/copy.ts +1 -0
- package/template/apps/asas-backoffice/src/first-user.ts +14 -0
- package/template/apps/asas-backoffice/src/hooks/use-mobile.ts +19 -0
- package/template/apps/asas-backoffice/src/lib/utils.ts +1 -0
- package/template/apps/asas-backoffice/src/links.ts +38 -0
- package/template/apps/asas-backoffice/src/locale-switch.tsx +28 -0
- package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
- package/template/apps/asas-backoffice/src/redirect.ts +1 -0
- package/template/apps/asas-backoffice/src/request-headers.ts +11 -0
- package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
- package/template/apps/asas-backoffice/src/router.tsx +18 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/screen.json +3971 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/types.ts +163 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.$.tsx +17 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.index.tsx +10 -0
- package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.tsx +13 -0
- package/template/apps/asas-backoffice/src/routes/__root.tsx +95 -0
- package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
- package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
- package/template/apps/asas-backoffice/src/routes/auth.sign-in.tsx +77 -0
- package/template/apps/asas-backoffice/src/routes/unauthorized.tsx +134 -0
- package/template/apps/asas-backoffice/src/screen-form.tsx +36 -0
- package/template/apps/asas-backoffice/src/screen-list.tsx +140 -0
- package/template/apps/asas-backoffice/src/screen-names.ts +10 -0
- package/template/apps/asas-backoffice/src/screens.tsx +888 -0
- package/template/apps/asas-backoffice/src/server.ts +30 -0
- package/template/apps/asas-backoffice/src/slide.ts +22 -0
- package/template/apps/asas-backoffice/src/start.ts +42 -0
- package/template/apps/asas-backoffice/src/styles.css +202 -0
- package/template/apps/asas-backoffice/src/theme.ts +2 -0
- package/template/apps/asas-backoffice/tsconfig.json +19 -0
- package/template/apps/asas-backoffice/vite.config.ts +32 -0
- package/template/apps/asas-tanstack/components.json +25 -0
- package/template/apps/asas-tanstack/gitignore +3 -0
- package/template/apps/asas-tanstack/package.json +58 -0
- package/template/apps/asas-tanstack/public/favicon.svg +1 -0
- package/template/apps/asas-tanstack/src/api.ts +147 -0
- package/template/apps/asas-tanstack/src/auth-frame.tsx +150 -0
- package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
- package/template/apps/asas-tanstack/src/components/ui/accordion.tsx +10 -0
- package/template/apps/asas-tanstack/src/components/ui/alert-dialog.tsx +173 -0
- package/template/apps/asas-tanstack/src/components/ui/avatar.tsx +104 -0
- package/template/apps/asas-tanstack/src/components/ui/badge.tsx +48 -0
- package/template/apps/asas-tanstack/src/components/ui/breadcrumb.tsx +125 -0
- package/template/apps/asas-tanstack/src/components/ui/button-group.tsx +87 -0
- package/template/apps/asas-tanstack/src/components/ui/button.tsx +56 -0
- package/template/apps/asas-tanstack/src/components/ui/calendar.tsx +223 -0
- package/template/apps/asas-tanstack/src/components/ui/card.tsx +94 -0
- package/template/apps/asas-tanstack/src/components/ui/chart.tsx +369 -0
- package/template/apps/asas-tanstack/src/components/ui/checkbox.tsx +27 -0
- package/template/apps/asas-tanstack/src/components/ui/collapsible.tsx +19 -0
- package/template/apps/asas-tanstack/src/components/ui/combobox.tsx +300 -0
- package/template/apps/asas-tanstack/src/components/ui/dialog.tsx +149 -0
- package/template/apps/asas-tanstack/src/components/ui/direction.tsx +5 -0
- package/template/apps/asas-tanstack/src/components/ui/directional-icon.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/apps/asas-tanstack/src/components/ui/field.tsx +225 -0
- package/template/apps/asas-tanstack/src/components/ui/input-group.tsx +147 -0
- package/template/apps/asas-tanstack/src/components/ui/input-otp.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/input.tsx +20 -0
- package/template/apps/asas-tanstack/src/components/ui/label.tsx +18 -0
- package/template/apps/asas-tanstack/src/components/ui/popover.tsx +50 -0
- package/template/apps/asas-tanstack/src/components/ui/radio-group.tsx +10 -0
- package/template/apps/asas-tanstack/src/components/ui/resizable.tsx +47 -0
- package/template/apps/asas-tanstack/src/components/ui/scroll-area.tsx +52 -0
- package/template/apps/asas-tanstack/src/components/ui/select.tsx +190 -0
- package/template/apps/asas-tanstack/src/components/ui/separator.tsx +23 -0
- package/template/apps/asas-tanstack/src/components/ui/sheet.tsx +127 -0
- package/template/apps/asas-tanstack/src/components/ui/sidebar.tsx +716 -0
- package/template/apps/asas-tanstack/src/components/ui/skeleton.tsx +13 -0
- package/template/apps/asas-tanstack/src/components/ui/slider.tsx +13 -0
- package/template/apps/asas-tanstack/src/components/ui/spinner.tsx +11 -0
- package/template/apps/asas-tanstack/src/components/ui/switch.tsx +8 -0
- package/template/apps/asas-tanstack/src/components/ui/table.tsx +119 -0
- package/template/apps/asas-tanstack/src/components/ui/tabs.tsx +80 -0
- package/template/apps/asas-tanstack/src/components/ui/textarea.tsx +18 -0
- package/template/apps/asas-tanstack/src/components/ui/toggle-group.tsx +8 -0
- package/template/apps/asas-tanstack/src/components/ui/toggle.tsx +44 -0
- package/template/apps/asas-tanstack/src/components/ui/tooltip.tsx +68 -0
- package/template/apps/asas-tanstack/src/components.ts +35 -0
- package/template/apps/asas-tanstack/src/copy.ts +1207 -0
- package/template/apps/asas-tanstack/src/first-run.ts +16 -0
- package/template/apps/asas-tanstack/src/hooks/use-mobile.ts +19 -0
- package/template/apps/asas-tanstack/src/lib/utils.ts +6 -0
- package/template/apps/asas-tanstack/src/links.ts +38 -0
- package/template/apps/asas-tanstack/src/locale-switch.tsx +28 -0
- package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
- package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
- package/template/apps/asas-tanstack/src/redirect.ts +39 -0
- package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
- package/template/apps/asas-tanstack/src/routeTree.gen.ts +424 -0
- package/template/apps/asas-tanstack/src/router.tsx +18 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +11 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +4242 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +160 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.$.tsx +17 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.index.tsx +10 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.tsx +13 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +9 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1564 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +94 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.$.tsx +17 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.index.tsx +10 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal.tsx +13 -0
- package/template/apps/asas-tanstack/src/routes/__root.tsx +99 -0
- package/template/apps/asas-tanstack/src/routes/api/$.ts +11 -0
- package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +108 -0
- package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
- package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +94 -0
- package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +39 -0
- package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +16 -0
- package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +98 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +143 -0
- package/template/apps/asas-tanstack/src/routes/index.tsx +146 -0
- package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +134 -0
- package/template/apps/asas-tanstack/src/screen-form.tsx +36 -0
- package/template/apps/asas-tanstack/src/screen-list.tsx +140 -0
- package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
- package/template/apps/asas-tanstack/src/screens.tsx +888 -0
- package/template/apps/asas-tanstack/src/server.ts +26 -0
- package/template/apps/asas-tanstack/src/slide.ts +22 -0
- package/template/apps/asas-tanstack/src/start.ts +57 -0
- package/template/apps/asas-tanstack/src/styles.css +163 -0
- package/template/apps/asas-tanstack/src/theme.ts +5 -0
- package/template/apps/asas-tanstack/tsconfig.json +19 -0
- package/template/apps/asas-tanstack/vite.config.ts +34 -0
- package/template/package.json +16 -0
- package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
- package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
- package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
- package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
- package/template/templates/asas/.env.example +33 -0
- package/template/templates/asas/AGENTS.md +101 -0
- package/template/templates/asas/CLAUDE.md +1 -0
- package/template/templates/asas/README.md +1056 -0
- package/template/templates/asas/mithaq.hints.json +25 -0
- package/template/templates/asas/package.json +53 -0
- package/template/templates/asas/src/asas.ts +118 -0
- package/template/templates/asas/src/backoffice-http.ts +117 -0
- package/template/templates/asas/src/dev-setup.ts +112 -0
- package/template/templates/asas/src/local-database.ts +22 -0
- package/template/templates/asas/src/messages/account.json +132 -0
- package/template/templates/asas/src/messages/auditLog.json +304 -0
- package/template/templates/asas/src/messages/backofficeLog.json +132 -0
- package/template/templates/asas/src/messages/courses.json +312 -0
- package/template/templates/asas/src/messages/enrollments.json +60 -0
- package/template/templates/asas/src/messages/invitation.json +82 -0
- package/template/templates/asas/src/messages/localUsers.json +48 -0
- package/template/templates/asas/src/messages/media.json +76 -0
- package/template/templates/asas/src/messages/member.json +62 -0
- package/template/templates/asas/src/messages/memberRoles.json +48 -0
- package/template/templates/asas/src/messages/mithaq.json +214 -0
- package/template/templates/asas/src/messages/organization.json +72 -0
- package/template/templates/asas/src/messages/organizationRole.json +62 -0
- package/template/templates/asas/src/messages/roles.json +72 -0
- package/template/templates/asas/src/messages/session.json +102 -0
- package/template/templates/asas/src/messages/sessions.json +136 -0
- package/template/templates/asas/src/messages/settings.json +64 -0
- package/template/templates/asas/src/messages/tags.json +48 -0
- package/template/templates/asas/src/messages/user.json +112 -0
- package/template/templates/asas/src/messages/verification.json +62 -0
- package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/migration.sql +177 -0
- package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/snapshot.json +2169 -0
- package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/snapshot.json +2182 -0
- package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/migration.sql +20 -0
- package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/snapshot.json +2409 -0
- package/template/templates/asas/src/migrations/20260922082945_military_ironclad/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20260922082945_military_ironclad/snapshot.json +2627 -0
- package/template/templates/asas/src/migrations/20260922112630_bitter_chat/migration.sql +5 -0
- package/template/templates/asas/src/migrations/20260922112630_bitter_chat/snapshot.json +2691 -0
- package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/migration.sql +8 -0
- package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/snapshot.json +2795 -0
- package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/migration.sql +3 -0
- package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/snapshot.json +2834 -0
- package/template/templates/asas/src/migrations/20260927212525_bent_prowler/migration.sql +9 -0
- package/template/templates/asas/src/migrations/20260927212525_bent_prowler/snapshot.json +2870 -0
- package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/snapshot.json +2881 -0
- package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/snapshot.json +3073 -0
- package/template/templates/asas/src/migrations/20260929024407_grey_morlun/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20260929024407_grey_morlun/snapshot.json +3098 -0
- package/template/templates/asas/src/migrations/20260929130530_bored_jackal/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20260929130530_bored_jackal/snapshot.json +3124 -0
- package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/migration.sql +32 -0
- package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/snapshot.json +3324 -0
- package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/migration.sql +2 -0
- package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/snapshot.json +3350 -0
- package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/migration.sql +21 -0
- package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/migration.sql +8 -0
- package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/migration.sql +18 -0
- package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/migration.sql +18 -0
- package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/snapshot.json +3635 -0
- package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/migration.sql +11 -0
- package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/snapshot.json +3667 -0
- package/template/templates/asas/src/migrations/20261012000752_organization_ownership/migration.sql +48 -0
- package/template/templates/asas/src/migrations/20261012000752_organization_ownership/snapshot.json +3891 -0
- package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
- package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
- package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
- package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
- package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
- package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
- package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
- package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
- package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
- package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
- package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
- package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
- package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
- package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
- package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
- package/template/templates/asas/src/migrations/drizzle.config.ts +11 -0
- package/template/templates/asas/src/migrations/schema.ts +30 -0
- package/template/templates/asas/src/mithaq.ts +4 -0
- package/template/templates/asas/src/organizations.ts +3 -0
- package/template/templates/asas/src/seed.ts +427 -0
- package/template/templates/asas/src/ui-backoffice/home.tsx +63 -0
- package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
- package/template/templates/asas/src/ui-backoffice/record-page.tsx +30 -0
- package/template/templates/asas/src/ui-backoffice/role-cells.tsx +59 -0
- package/template/templates/asas/src/ui-backoffice/user-actions.tsx +249 -0
- package/template/templates/asas/src/ui-dashboard/dashboard.tsx +300 -0
- package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
- package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
- package/template/templates/asas/src/ui-dashboard/members.tsx +829 -0
- package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
- package/template/templates/asas/src/ui-dashboard/versions.tsx +446 -0
- package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
- package/template/templates/asas/src/ui-portal/home.tsx +73 -0
- package/template/templates/asas/src/ui-portal/page.tsx +11 -0
- package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
- package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
- package/template/templates/features/audit-log/index.ts +4 -0
- package/template/templates/features/audit-log/package.json +13 -0
- package/template/templates/features/auth/index.ts +11 -0
- package/template/templates/features/auth/local-users/local-users.ts +143 -0
- package/template/templates/features/auth/package.json +13 -0
- package/template/templates/features/auth/user/account.access.ts +8 -0
- package/template/templates/features/auth/user/account.ts +31 -0
- package/template/templates/features/auth/user/index.ts +18 -0
- package/template/templates/features/auth/user/invitation.access.ts +4 -0
- package/template/templates/features/auth/user/invitation.ts +27 -0
- package/template/templates/features/auth/user/member.access.ts +10 -0
- package/template/templates/features/auth/user/member.screens.ts +37 -0
- package/template/templates/features/auth/user/member.ts +26 -0
- package/template/templates/features/auth/user/organization.access.ts +20 -0
- package/template/templates/features/auth/user/organization.screens.ts +37 -0
- package/template/templates/features/auth/user/organization.ts +25 -0
- package/template/templates/features/auth/user/organizationRole.access.ts +8 -0
- package/template/templates/features/auth/user/organizationRole.ts +25 -0
- package/template/templates/features/auth/user/session.access.ts +8 -0
- package/template/templates/features/auth/user/session.ts +28 -0
- package/template/templates/features/auth/user/user.access.ts +30 -0
- package/template/templates/features/auth/user/user.screens.ts +45 -0
- package/template/templates/features/auth/user/user.ts +31 -0
- package/template/templates/features/auth/user/verification.access.ts +8 -0
- package/template/templates/features/auth/user/verification.ts +23 -0
- package/template/templates/features/courses/courses/courses.ts +601 -0
- package/template/templates/features/courses/courses/cover-reads.ts +33 -0
- package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
- package/template/templates/features/courses/courses/ui/fee-cell.tsx +19 -0
- package/template/templates/features/courses/courses/ui/no-courses.tsx +13 -0
- package/template/templates/features/courses/courses/ui/status-cell.tsx +33 -0
- package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
- package/template/templates/features/courses/domain/archive.ts +39 -0
- package/template/templates/features/courses/domain/on-offer.ts +6 -0
- package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
- package/template/templates/features/courses/enrollments/ui/session-cell.tsx +75 -0
- package/template/templates/features/courses/enrollments/ui/session-labels.tsx +36 -0
- package/template/templates/features/courses/index.ts +8 -0
- package/template/templates/features/courses/package.json +24 -0
- package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
- package/template/templates/features/courses/portal/ui/course.tsx +176 -0
- package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
- package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
- package/template/templates/features/courses/portal/ui/reads.ts +76 -0
- package/template/templates/features/courses/portal/ui/rules.ts +87 -0
- package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
- package/template/templates/features/courses/sessions/seats.ts +42 -0
- package/template/templates/features/courses/sessions/sessions.ts +186 -0
- package/template/templates/features/media/index.ts +10 -0
- package/template/templates/features/media/media/media.ts +131 -0
- package/template/templates/features/media/media/ui/preview.tsx +143 -0
- package/template/templates/features/media/package.json +19 -0
- package/template/templates/features/roles/index.ts +4 -0
- package/template/templates/features/roles/member-roles/member-roles.ts +207 -0
- package/template/templates/features/roles/package.json +21 -0
- package/template/templates/features/roles/roles/roles.ts +184 -0
- package/template/templates/features/roles/roles/ui/role-form.tsx +267 -0
- package/template/templates/features/roles/roles/ui/roles.tsx +190 -0
- package/template/templates/features/settings/index.ts +3 -0
- package/template/templates/features/settings/package.json +13 -0
- package/template/templates/features/settings/settings/settings.ts +64 -0
- package/template/templates/features/tags/index.ts +3 -0
- package/template/templates/features/tags/package.json +14 -0
- package/template/templates/features/tags/tags/tags.ts +80 -0
- package/template/templates/mithaq/components.json +22 -0
- package/template/templates/mithaq/package.json +33 -0
- package/template/templates/mithaq/src/mail/index.ts +32 -0
- package/template/templates/mithaq/src/mail/mailers.ts +36 -0
- package/template/templates/mithaq/src/mithaq/audit-sink.ts +111 -0
- package/template/templates/mithaq/src/mithaq/auth/backoffice-roles.ts +108 -0
- package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
- package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
- package/template/templates/mithaq/src/mithaq/pages.ts +83 -0
- package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
- package/template/templates/mithaq/src/mithaq.ts +593 -0
- package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
- package/template/templates/mithaq/src/roles/grants.ts +47 -0
- package/template/templates/mithaq/src/roles/member-access.ts +11 -0
- package/template/templates/mithaq/src/roles/statements.ts +13 -0
- package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
- package/template/templates/mithaq/src/roles/tenant-access.ts +260 -0
- package/template/templates/mithaq/src/roles/tenant-roles.ts +321 -0
- package/template/templates/mithaq/src/steps/expected-updated-at.ts +41 -0
- package/template/templates/mithaq/src/storage/defaults.ts +156 -0
- package/template/templates/mithaq/src/storage/index.ts +97 -0
- package/template/templates/mithaq/src/storage/local.ts +143 -0
- package/template/templates/mithaq/src/storage/r2.ts +5 -0
- package/template/templates/mithaq/src/storage/s3.ts +91 -0
- package/template/templates/mithaq/src/storage/uploadthing.ts +59 -0
- package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
- package/template/templates/mithaq/src/ui/activity.tsx +527 -0
- package/template/templates/mithaq/src/ui/app-shell.tsx +268 -0
- package/template/templates/mithaq/src/ui/kpi-card.tsx +25 -0
- package/template/templates/mithaq/src/ui/record-page.tsx +50 -0
- package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
- package/template/templates/mithaq/src/ui/screen-nav.tsx +28 -0
- package/template/templates/mithaq/src/ui/table-page.tsx +9 -0
- package/template/templates/mithaq/src/ui/top-shell.tsx +60 -0
- package/README.md +0 -3
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// The frame both screens may put at the root of a page: a collapsible sidebar holding the organization switcher, the nav and the user menu, and an inset holding the site header over the page.
|
|
2
|
+
// A row that goes nowhere is not drawn, so there are no quick-create or inbox buttons, documents group, or help and search rows: none has a destination in this template.
|
|
3
|
+
import { useState, useSyncExternalStore, type CSSProperties, type ReactNode } from 'react'
|
|
4
|
+
import {
|
|
5
|
+
ArrowUpRight,
|
|
6
|
+
BookOpen,
|
|
7
|
+
Building2,
|
|
8
|
+
CalendarDays,
|
|
9
|
+
ClipboardList,
|
|
10
|
+
History,
|
|
11
|
+
LayoutDashboard,
|
|
12
|
+
Settings2,
|
|
13
|
+
ShieldCheck,
|
|
14
|
+
Table2,
|
|
15
|
+
Tags,
|
|
16
|
+
UserCog,
|
|
17
|
+
UserRound,
|
|
18
|
+
Users,
|
|
19
|
+
type LucideIcon,
|
|
20
|
+
} from 'lucide-react'
|
|
21
|
+
import type { ShellProps } from 'mithaq/admin'
|
|
22
|
+
import { useLocale, useNav, useScreen, useUser } from 'mithaq/admin/react'
|
|
23
|
+
import { authArm } from '@/api.ts'
|
|
24
|
+
import { locales, switchLocale } from '@/copy.ts'
|
|
25
|
+
import { DiscardDialog } from 'mithaq-admin/discard-dialog.tsx'
|
|
26
|
+
import { NavSidebar } from 'mithaq-admin/nav.tsx'
|
|
27
|
+
import { NavUserMenu } from 'mithaq-admin/nav-user.tsx'
|
|
28
|
+
import { OrganizationSwitcher, type NavOrganization } from 'mithaq-admin/organization-switcher.tsx'
|
|
29
|
+
import { buttonVariants } from '@/components/ui/button'
|
|
30
|
+
import { Separator } from '@/components/ui/separator'
|
|
31
|
+
import {
|
|
32
|
+
SidebarContent,
|
|
33
|
+
SidebarFooter,
|
|
34
|
+
SidebarGroup,
|
|
35
|
+
SidebarGroupContent,
|
|
36
|
+
SidebarHeader,
|
|
37
|
+
SidebarInset,
|
|
38
|
+
SidebarMenu,
|
|
39
|
+
SidebarMenuButton,
|
|
40
|
+
SidebarMenuItem,
|
|
41
|
+
SidebarProvider,
|
|
42
|
+
SidebarTrigger,
|
|
43
|
+
} from '@/components/ui/sidebar'
|
|
44
|
+
import { navLabel } from './screen-nav.tsx'
|
|
45
|
+
import { nameIn, showOrganization } from '../mithaq/auth/organizations.ts'
|
|
46
|
+
|
|
47
|
+
export const regions = ['header', 'main'] as const
|
|
48
|
+
|
|
49
|
+
/** One icon per nav entry, keyed by the entry's path under the screen, and `Table2` for any other. */
|
|
50
|
+
const icons: Record<string, LucideIcon> = {
|
|
51
|
+
'/': LayoutDashboard,
|
|
52
|
+
'/users': UserRound,
|
|
53
|
+
'/organizations': Building2,
|
|
54
|
+
'/members': Users,
|
|
55
|
+
'/courses': BookOpen,
|
|
56
|
+
'/sessions': CalendarDays,
|
|
57
|
+
'/enrollments': ClipboardList,
|
|
58
|
+
'/tags': Tags,
|
|
59
|
+
'/roles': ShieldCheck,
|
|
60
|
+
'/member-roles': UserCog,
|
|
61
|
+
'/settings': Settings2,
|
|
62
|
+
'/activity': History,
|
|
63
|
+
'/history': History,
|
|
64
|
+
}
|
|
65
|
+
export const iconOf = (path: string) => (Object.hasOwn(icons, path) ? icons[path]! : Table2)
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The path the browser is on. The server has no location, so nothing is
|
|
69
|
+
* marked current in the first paint and the entry is marked once it hydrates.
|
|
70
|
+
*/
|
|
71
|
+
const usePathname = () => useSyncExternalStore(() => () => {}, () => window.location.pathname, () => undefined)
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The locale switch and sign-out, in the user menu. The locale is a cookie and a reload, because the request middleware reads it before any route runs;
|
|
75
|
+
* the switch is asked first, in the app's discard dialog, where a form holds a change nobody saved.
|
|
76
|
+
*/
|
|
77
|
+
export function UserMenu() {
|
|
78
|
+
const { locale, t } = useLocale()
|
|
79
|
+
// The language asked for while a form holds a change nobody saved, which the
|
|
80
|
+
// app's own discard dialog confirms in the page's words.
|
|
81
|
+
const [asked, setAsked] = useState<string>()
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
<NavUserMenu
|
|
85
|
+
user={useUser()}
|
|
86
|
+
t={t}
|
|
87
|
+
locale={locale}
|
|
88
|
+
locales={locales}
|
|
89
|
+
onLocale={(next) => (window.onbeforeunload ? setAsked(next) : switchLocale({ to: next }))}
|
|
90
|
+
// Only once the provider has answered: its sign-out answers 200 with a
|
|
91
|
+
// session or without one, so a refusal stays where it is. `replace`,
|
|
92
|
+
// so Back does not return to the page signed out of. The sign-out that
|
|
93
|
+
// ends an impersonation answers the backoffice's address, where the staff
|
|
94
|
+
// member goes next.
|
|
95
|
+
onSignOut={() => void authArm('sign-out').then((answer) => answer.ok && window.location.replace(String(answer.body?.url ?? '/auth/sign-in')))}
|
|
96
|
+
/>
|
|
97
|
+
<DiscardDialog open={asked !== undefined} onOpenChange={(open) => !open && setAsked(undefined)} onDiscard={() => switchLocale({ to: asked! })} t={t} />
|
|
98
|
+
</>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The organization switcher over the organizations the session came with. The host sends them with the session, so it is drawn with the page and never flashes a skeleton.
|
|
104
|
+
* Switching reloads the page, because every loader reads its rows under the active organization on the server.
|
|
105
|
+
*/
|
|
106
|
+
function Organizations() {
|
|
107
|
+
const { locale, t } = useLocale()
|
|
108
|
+
const { organizations = [], activeOrganizationId } = useUser()
|
|
109
|
+
return (
|
|
110
|
+
<OrganizationSwitcher
|
|
111
|
+
organizations={organizations.map((one) => ({ ...one, name: nameIn(one, locale) }))}
|
|
112
|
+
activeOrganizationId={activeOrganizationId}
|
|
113
|
+
t={t}
|
|
114
|
+
onSwitch={(organizationId) => void authArm('organization/set-active', { organizationId }).then((answer) => answer.ok && window.location.reload())}
|
|
115
|
+
/>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One entry per destination, the shell's own first and the entities after.
|
|
121
|
+
* The dashboard is current on its own path alone; every other entry on its path and below it.
|
|
122
|
+
*/
|
|
123
|
+
function NavMain({ home }: { home?: string }) {
|
|
124
|
+
const { locale, t } = useLocale()
|
|
125
|
+
const screen = useScreen()
|
|
126
|
+
const pathname = usePathname()
|
|
127
|
+
const entries = [
|
|
128
|
+
{ path: '/', label: home ?? t('dashboard.dashboard', 'Dashboard') },
|
|
129
|
+
...[...useNav()].sort((one, other) => Number(!!one.kayan) - Number(!!other.kayan)).map((entry) => ({ path: entry.path, label: navLabel({ entry, screen, locale, t }) })),
|
|
130
|
+
]
|
|
131
|
+
return (
|
|
132
|
+
<SidebarGroup>
|
|
133
|
+
<SidebarGroupContent className="flex flex-col gap-2">
|
|
134
|
+
<SidebarMenu>
|
|
135
|
+
{entries.map((entry) => {
|
|
136
|
+
const href = entry.path === '/' ? screen.path : `${screen.path}${entry.path}`
|
|
137
|
+
const Icon = iconOf(entry.path)
|
|
138
|
+
const isActive = pathname !== undefined && (pathname === href || (entry.path !== '/' && pathname.startsWith(`${href}/`)))
|
|
139
|
+
return (
|
|
140
|
+
<SidebarMenuItem key={entry.path}>
|
|
141
|
+
{/* A 44 pixel row wherever the sidebar is the phone's sheet. */}
|
|
142
|
+
<SidebarMenuButton
|
|
143
|
+
isActive={isActive}
|
|
144
|
+
className="max-md:h-11"
|
|
145
|
+
render={
|
|
146
|
+
<a href={href}>
|
|
147
|
+
<Icon />
|
|
148
|
+
<span>{entry.label}</span>
|
|
149
|
+
</a>
|
|
150
|
+
}
|
|
151
|
+
/>
|
|
152
|
+
</SidebarMenuItem>
|
|
153
|
+
)
|
|
154
|
+
})}
|
|
155
|
+
</SidebarMenu>
|
|
156
|
+
</SidebarGroupContent>
|
|
157
|
+
</SidebarGroup>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const frame = { '--sidebar-width': 'calc(var(--spacing) * 72)', '--header-height': 'calc(var(--spacing) * 12)' } as CSSProperties
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The site header's bar, with the page's own title. It is sticky: its trigger is the only way back to a hidden sidebar, so it stays on screen however far the page is scrolled.
|
|
165
|
+
* Below sm the title may take two lines, set tight so both fit the bar, since on a phone it is often the record's only name; from sm up it is one line, cut at its end.
|
|
166
|
+
* From sm up the row wraps and the bar grows with it, so a form's controls that do not fit take a line of their own rather than run over the title or past the bar's end.
|
|
167
|
+
* A bar that wrapped is taller than `--header-height`, so it measures itself into `--header-bar-height`, which what sticks under it reads.
|
|
168
|
+
*/
|
|
169
|
+
export const headerBar = 'sticky top-0 z-10 bg-background rounded-t-[inherit] flex min-h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:min-h-(--header-height)'
|
|
170
|
+
export const headerRow = 'flex w-full min-w-0 items-center gap-1 px-4 sm:flex-wrap sm:gap-y-2 sm:py-2 lg:gap-2 lg:px-6 [&_h1]:min-w-0 max-sm:[&_h1]:line-clamp-2 max-sm:[&_h1]:leading-tight max-sm:[&_h1]:wrap-break-word sm:[&_h1]:truncate [&_h1]:text-base [&_h1]:font-medium [&_h1]:tracking-normal'
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The header's own height on the document, as `--header-bar-height`, kept while the header is on screen and taken off with it.
|
|
174
|
+
* A variable of its own and never `--header-height`, which the bar's `min-h-(--header-height)` reads, so the measure would feed itself.
|
|
175
|
+
* A module function, so React hands the ref the same callback on every render and observes the header once.
|
|
176
|
+
*/
|
|
177
|
+
const measureHeader = (header: HTMLElement | null) => {
|
|
178
|
+
if (!header) return
|
|
179
|
+
const root = document.documentElement.style
|
|
180
|
+
// `borderBoxSize` is missing before Safari 15.4, so the header's own `offsetHeight` stands in there.
|
|
181
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
182
|
+
if (entry) root.setProperty('--header-bar-height', `${entry.borderBoxSize?.[0]?.blockSize ?? (entry.target as HTMLElement).offsetHeight}px`)
|
|
183
|
+
})
|
|
184
|
+
observer.observe(header)
|
|
185
|
+
return () => {
|
|
186
|
+
observer.disconnect()
|
|
187
|
+
root.removeProperty('--header-bar-height')
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The one way from a tenant screen to the backoffice app, in the header, for an actor whose own `user.role` holds a backoffice role that enters it.
|
|
193
|
+
* The link grants nothing here: the backoffice app's own server decides entry.
|
|
194
|
+
* Below xl it is its icon alone, a 44 pixel square on a phone, its words kept for a screen reader, so the page's title keeps the header's width.
|
|
195
|
+
* A quiet ghost button at the row's end, so the page's own actions beside the title lead.
|
|
196
|
+
*/
|
|
197
|
+
export function BackofficeLink() {
|
|
198
|
+
const { t } = useLocale()
|
|
199
|
+
const backoffice = useUser().screens?.find((one) => one.name === 'backoffice')
|
|
200
|
+
if (!backoffice) return null
|
|
201
|
+
return (
|
|
202
|
+
<a href={backoffice.path} data-backoffice-link="" className={buttonVariants({ variant: 'ghost', size: 'sm', className: 'text-muted-foreground ms-auto max-sm:size-11 max-xl:px-2.5 max-sm:px-0 [[data-node=controls]:not(:empty)~&]:ms-0' })}>
|
|
203
|
+
<span className="max-xl:sr-only">{t('dashboard.openBackoffice', 'Open the backoffice')}</span>
|
|
204
|
+
<ArrowUpRight data-icon="inline-end" className="rtl:-scale-x-100" />
|
|
205
|
+
</a>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The sidebar on the side the document's direction starts from. */
|
|
210
|
+
export function AppShell({ regions, brand, home }: ShellProps<readonly ['header', 'main'], ReactNode> & { brand?: ReactNode; home?: string }) {
|
|
211
|
+
const { dir, t } = useLocale()
|
|
212
|
+
return (
|
|
213
|
+
<SidebarProvider data-shell="app" style={frame}>
|
|
214
|
+
<NavSidebar variant="inset" side={dir === 'rtl' ? 'right' : 'left'} t={t}>
|
|
215
|
+
{/* One navigation landmark around the whole sidebar, named in the
|
|
216
|
+
page's language, so the switcher and the user menu are inside a
|
|
217
|
+
landmark as the links are. It is a box of its own and not a role on
|
|
218
|
+
the sidebar, whose props reach no element on a phone, where it is a
|
|
219
|
+
sheet. */}
|
|
220
|
+
<nav aria-label={t('admin.navigation', 'Navigation')} className="flex min-h-0 flex-1 flex-col">
|
|
221
|
+
{/* With no organization shown, the app's own name sits where the switcher would. */}
|
|
222
|
+
<SidebarHeader>{brand ?? (!showOrganization ? <span data-brand="" className="truncate px-2 py-1.5 text-base font-semibold">{t('marketing.brand', 'Asas')}</span> : <Organizations />)}</SidebarHeader>
|
|
223
|
+
<SidebarContent>
|
|
224
|
+
<NavMain home={home} />
|
|
225
|
+
</SidebarContent>
|
|
226
|
+
<SidebarFooter>
|
|
227
|
+
<UserMenu />
|
|
228
|
+
</SidebarFooter>
|
|
229
|
+
</nav>
|
|
230
|
+
</NavSidebar>
|
|
231
|
+
{/* `min-w-0` so the inset, a flex item beside the sidebar, never takes
|
|
232
|
+
the width of the widest thing a page holds: without it a table wider
|
|
233
|
+
than a phone would widen the document, header and trigger with it. A
|
|
234
|
+
wide table scrolls in its own frame instead. */}
|
|
235
|
+
<SidebarInset className="min-w-0">
|
|
236
|
+
<header data-region="header" ref={measureHeader} className={headerBar}>
|
|
237
|
+
<div className={headerRow}>
|
|
238
|
+
<SidebarTrigger aria-label={t('admin.toggleSidebar', 'Toggle Sidebar')} className="-ms-1 max-md:size-11" />
|
|
239
|
+
<Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" />
|
|
240
|
+
{regions.header}
|
|
241
|
+
<BackofficeLink />
|
|
242
|
+
</div>
|
|
243
|
+
</header>
|
|
244
|
+
<div className="flex min-w-0 flex-1 flex-col px-4 py-4 lg:px-6">
|
|
245
|
+
{/* `min-w-0` so a table wider than the page scrolls rather than
|
|
246
|
+
pushing the sidebar off it. A div and not a `main`, because the
|
|
247
|
+
inset around it is already the page's one main landmark. */}
|
|
248
|
+
<div data-region="main" className="@container/main flex min-w-0 flex-1 flex-col gap-2">{regions.main}</div>
|
|
249
|
+
</div>
|
|
250
|
+
</SidebarInset>
|
|
251
|
+
</SidebarProvider>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The backoffice's frame, the dashboard's with two changes: the app's name where the organization switcher sits, since staff work above every organization and switch to none of them,
|
|
257
|
+
* and the home entry named as its page names itself, so the nav and the page say one word.
|
|
258
|
+
*/
|
|
259
|
+
export function BackofficeShell(props: ShellProps<readonly ['header', 'main'], ReactNode>) {
|
|
260
|
+
const { t } = useLocale()
|
|
261
|
+
return (
|
|
262
|
+
<AppShell
|
|
263
|
+
{...props}
|
|
264
|
+
brand={<span data-brand="" className="truncate px-2 py-1.5 text-base font-semibold">{t('backoffice.brand', 'Asas backoffice')}</span>}
|
|
265
|
+
home={t('backoffice.title', 'Backoffice')}
|
|
266
|
+
/>
|
|
267
|
+
)
|
|
268
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// One figure on a card, which the dashboard's home and the portal's home both draw.
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A figure on a card, marked with the figure it shows. On a phone, two to a row,
|
|
7
|
+
* a card is about 136 pixels wide, so there the padding is a step smaller, and
|
|
8
|
+
* below 220 pixels of card the figure is set in the body size, which holds a sum
|
|
9
|
+
* such as "SAR 5,150.00" on one line in either locale; one longer still breaks at
|
|
10
|
+
* the edge rather than being cut.
|
|
11
|
+
*/
|
|
12
|
+
export function KpiCard({ kpi, icon, title, value, footer }: { kpi: string; icon: ReactNode; title?: string; value: ReactNode; footer: ReactNode }) {
|
|
13
|
+
return (
|
|
14
|
+
<Card data-kpi={kpi} className="@container/card min-w-0">
|
|
15
|
+
<CardHeader className="max-sm:px-4">
|
|
16
|
+
<CardDescription className="flex items-center gap-2 text-sm font-medium">
|
|
17
|
+
<span className="text-muted-foreground inline-flex">{icon}</span>
|
|
18
|
+
<span>{title}</span>
|
|
19
|
+
</CardDescription>
|
|
20
|
+
<CardTitle className="min-w-0 text-base font-semibold tabular-nums wrap-break-word @[220px]/card:text-2xl @[250px]/card:text-3xl">{value}</CardTitle>
|
|
21
|
+
</CardHeader>
|
|
22
|
+
<CardFooter className="text-muted-foreground pt-2 text-xs max-sm:px-4">{footer}</CardFooter>
|
|
23
|
+
</Card>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// The frame of one record: the form in `main`, what sits beside it in `side`, and the buttons a
|
|
2
|
+
// person should not reach by accident in `danger`. A page fills only the regions this file exports,
|
|
3
|
+
// which the build checks.
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import type { LayoutProps } from 'mithaq/admin'
|
|
6
|
+
import { useKayan, useLocale, useRecord } from 'mithaq/admin/react'
|
|
7
|
+
|
|
8
|
+
export const regions = ['main', 'side', 'danger'] as const
|
|
9
|
+
|
|
10
|
+
export function RecordPage({ regions, page }: LayoutProps<readonly ['main', 'side', 'danger'], ReactNode>) {
|
|
11
|
+
const { t } = useLocale()
|
|
12
|
+
const record = useRecord()
|
|
13
|
+
return (
|
|
14
|
+
// main, side, danger in document order is how a phone stacks them: the danger zone last. From lg up
|
|
15
|
+
// the grid puts the side beside both and the danger zone back under the form; the second row's `1fr`
|
|
16
|
+
// takes whatever height the side adds, so the danger zone sits right under the form. A page with
|
|
17
|
+
// nothing beside its form gives the form the whole width. A side marked `data-lead`, such as an
|
|
18
|
+
// image beside its description, comes before the form on a phone instead.
|
|
19
|
+
<div data-layout="record" data-page={page.path} data-record={record ? 'row' : 'none'} className={regions.side ? 'grid gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(16rem,1fr)] lg:grid-rows-[auto_1fr]' : 'grid gap-4'}>
|
|
20
|
+
<div data-region="main" className="grid min-w-0 gap-4 lg:col-start-1">
|
|
21
|
+
{regions.main}
|
|
22
|
+
</div>
|
|
23
|
+
{regions.side ? <aside data-region="side" className="grid h-fit gap-4 max-lg:has-[[data-lead]]:order-first lg:col-start-2 lg:row-span-2 lg:row-start-1">{regions.side}</aside> : null}
|
|
24
|
+
{/* The danger zone, fenced by the destructive border, under the form rather than in its row. */}
|
|
25
|
+
{regions.danger ? (
|
|
26
|
+
<section data-region="danger" className="grid h-fit min-w-0 gap-2 rounded-lg border border-destructive/50 p-4 lg:col-start-1">
|
|
27
|
+
<h2 className="text-base font-medium text-destructive">{t('admin.dangerZone', 'Danger zone')}</h2>
|
|
28
|
+
<DangerWarning />
|
|
29
|
+
<div className="flex flex-wrap items-center gap-2">{regions.danger}</div>
|
|
30
|
+
</section>
|
|
31
|
+
) : null}
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What the danger zone says under its heading: the kayan's own sentence, or the shared one with the
|
|
38
|
+
* kind put in. Its own component, since only a kayan's page has a danger zone and `useKayan` refuses
|
|
39
|
+
* any other.
|
|
40
|
+
*/
|
|
41
|
+
function DangerWarning() {
|
|
42
|
+
const { t, locale } = useLocale()
|
|
43
|
+
const kayan = useKayan()
|
|
44
|
+
return (
|
|
45
|
+
<p className="text-muted-foreground text-sm">
|
|
46
|
+
{t(`${kayan.name}.dangerZoneWarning`)
|
|
47
|
+
?? (t('admin.dangerZoneWarning', 'These actions change this {kind} for everyone.') ?? '').replace('{kind}', () => (t(`${kayan.name}.singular`) ?? kayan.name).toLocaleLowerCase(locale))}
|
|
48
|
+
</p>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// What every screen's rich text editor and viewer draw with: this project's
|
|
2
|
+
// own node list, the one its server parses every write with, each class swapped
|
|
3
|
+
// for its React subclass where one extends it, and the `/` menu's picks. A
|
|
4
|
+
// provider each screen names, so no control, read view or panel can take
|
|
5
|
+
// another list.
|
|
6
|
+
import type { ReactNode } from 'react'
|
|
7
|
+
import type { ProviderProps } from 'mithaq/admin'
|
|
8
|
+
import { RichTextContext, defaultPicks, defaultViews, viewsOf } from 'mithaq-admin/rich-text-picks.tsx'
|
|
9
|
+
// add-on: uploads
|
|
10
|
+
import { UploadPlugin, UploadView, imagePick } from 'mithaq-admin/upload-node.tsx'
|
|
11
|
+
// end add-on: uploads
|
|
12
|
+
import { richTextNodes } from '../mithaq/rich-text-nodes.ts'
|
|
13
|
+
|
|
14
|
+
const value = {
|
|
15
|
+
nodes: viewsOf(richTextNodes, [
|
|
16
|
+
...defaultViews,
|
|
17
|
+
// add-on: uploads
|
|
18
|
+
UploadView,
|
|
19
|
+
// end add-on: uploads
|
|
20
|
+
]),
|
|
21
|
+
picks: [
|
|
22
|
+
...defaultPicks,
|
|
23
|
+
// add-on: uploads
|
|
24
|
+
// The upload node's `/` entry "Image" opens its dialog in every editor.
|
|
25
|
+
imagePick,
|
|
26
|
+
// end add-on: uploads
|
|
27
|
+
],
|
|
28
|
+
// add-on: uploads
|
|
29
|
+
Plugin: UploadPlugin,
|
|
30
|
+
// end add-on: uploads
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const RichTextNodes = ({ children }: ProviderProps<ReactNode>) => <RichTextContext value={value}>{children}</RichTextContext>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The nav a page drawn whole puts in its shell's nav region, since such a page
|
|
2
|
+
// has no tree for a `{ nav: {} }` node to sit in: one link per entry of the
|
|
3
|
+
// screen's nav, which the host has already cut to the viewer, read through the hooks.
|
|
4
|
+
import { use } from 'react'
|
|
5
|
+
import type { NavEntry, ScreenClient, Translate } from 'mithaq/admin'
|
|
6
|
+
import { ScreenContext, useLocale, useScreen } from 'mithaq/admin/react'
|
|
7
|
+
import { currentOf } from 'mithaq-admin/nav-links.tsx'
|
|
8
|
+
|
|
9
|
+
/** What an entry is called: its own label, else its kayan's name in the singular for a global and the plural otherwise. */
|
|
10
|
+
export const navLabel = ({ entry, screen, locale, t }: { entry: NavEntry; screen: ScreenClient; locale: string; t: Translate }) =>
|
|
11
|
+
entry.label?.[locale] ?? t(`${entry.kayan}.${screen.kayan[entry.kayan ?? '']?.single ? 'singular' : 'plural'}`, entry.kayan)
|
|
12
|
+
|
|
13
|
+
export function ScreenNav() {
|
|
14
|
+
const { locale, t } = useLocale()
|
|
15
|
+
const screen = useScreen()
|
|
16
|
+
const kayan = use(ScreenContext)?.kayan?.name
|
|
17
|
+
return (
|
|
18
|
+
<ul className="flex flex-col gap-1">
|
|
19
|
+
{screen.nav.map((entry) => (
|
|
20
|
+
<li key={entry.page + entry.path}>
|
|
21
|
+
<a href={`${screen.path}${entry.path}`} aria-current={currentOf(entry, kayan)} className="text-sm">
|
|
22
|
+
{navLabel({ entry, screen, locale, t })}
|
|
23
|
+
</a>
|
|
24
|
+
</li>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The layout of a page that lists rows: one region, `main`.
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import type { LayoutProps } from 'mithaq/admin'
|
|
4
|
+
|
|
5
|
+
export const regions = ['main'] as const
|
|
6
|
+
|
|
7
|
+
export const TablePage = ({ regions, page }: LayoutProps<readonly ['main'], ReactNode>) => (
|
|
8
|
+
<div data-layout="table" data-page={page.path} className="grid gap-4">{regions.main}</div>
|
|
9
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The second frame both screens may put at the root of a page: the nav as a
|
|
2
|
+
// bar across the top, the header under it, and one column below. The header is the app
|
|
3
|
+
// shell's own bar with the user menu at its end in place of the sidebar
|
|
4
|
+
// trigger, so both frames sign out and switch locale from the same menu. The
|
|
5
|
+
// provider is there for the menu, whose primitives read the sidebar's context;
|
|
6
|
+
// no sidebar is drawn.
|
|
7
|
+
import { use, type ReactNode } from 'react'
|
|
8
|
+
import type { ShellProps } from 'mithaq/admin'
|
|
9
|
+
import { ScreenContext, useLocale, useScreen } from 'mithaq/admin/react'
|
|
10
|
+
import { SidebarProvider } from '@/components/ui/sidebar'
|
|
11
|
+
import { BackofficeLink, UserMenu, frame, headerBar, headerRow } from './app-shell.tsx'
|
|
12
|
+
|
|
13
|
+
export const regions = ['header', 'nav', 'main'] as const
|
|
14
|
+
|
|
15
|
+
/** The dashboard's top shell, whose header links the backoffice app for staff the way the app shell's does. */
|
|
16
|
+
export function TopShell(props: ShellProps<readonly ['header', 'nav', 'main'], ReactNode>) {
|
|
17
|
+
return <Top {...props} backoffice />
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The portal's, the same shell with no backoffice link: the portal is where a
|
|
22
|
+
* tenant's people work, and staff reach the backoffice from the dashboard
|
|
23
|
+
* alone. Which of the two a page draws is the shell its screen names under
|
|
24
|
+
* `top`, never the URL.
|
|
25
|
+
*/
|
|
26
|
+
export function PortalTopShell(props: ShellProps<readonly ['header', 'nav', 'main'], ReactNode>) {
|
|
27
|
+
return <Top {...props} backoffice={false} />
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function Top({ regions, backoffice }: ShellProps<readonly ['header', 'nav', 'main'], ReactNode> & { backoffice: boolean }) {
|
|
31
|
+
const { t } = useLocale()
|
|
32
|
+
const screen = useScreen()
|
|
33
|
+
// Home is current on the screen's own home alone, whose path is the
|
|
34
|
+
// screen's, and each entry on its kayan's pages; the current one is drawn in
|
|
35
|
+
// the foreground and the rest muted. On a phone each link is a 44 pixel row,
|
|
36
|
+
// a touch target.
|
|
37
|
+
const home = use(ScreenContext)?.path === screen.path
|
|
38
|
+
return (
|
|
39
|
+
<SidebarProvider data-shell="top" style={frame} className="flex-col">
|
|
40
|
+
<nav data-region="nav" aria-label={t('admin.mainNavigation', 'Main navigation')} className="mx-auto flex w-full max-w-4xl flex-wrap items-center gap-x-4 gap-y-1 px-6 pt-4 text-sm [&_a]:text-muted-foreground [&_a:hover]:text-foreground [&_a[aria-current=page]]:font-semibold [&_a[aria-current=page]]:text-foreground [&_ul]:flex [&_ul]:flex-row [&_ul]:flex-wrap [&_ul]:gap-x-4 [&_ul]:gap-y-1 max-sm:[&_a]:inline-flex max-sm:[&_a]:min-h-11 max-sm:[&_a]:min-w-11 max-sm:[&_a]:items-center max-sm:[&_a]:justify-center">
|
|
41
|
+
<a href={screen.path} aria-current={home ? 'page' : undefined}>{t('portal.home', 'Home')}</a>
|
|
42
|
+
{regions.nav}
|
|
43
|
+
</nav>
|
|
44
|
+
<header data-region="header" className={`${headerBar} mx-auto w-full max-w-4xl`}>
|
|
45
|
+
<div className={headerRow}>
|
|
46
|
+
{regions.header}
|
|
47
|
+
<div className="ms-auto flex items-center gap-2">
|
|
48
|
+
{backoffice ? <BackofficeLink /> : null}
|
|
49
|
+
{/* The avatar alone on a phone, the whole card from sm up, so the
|
|
50
|
+
title keeps the room it needs. */}
|
|
51
|
+
<div className="w-12 sm:w-56">
|
|
52
|
+
<UserMenu />
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</header>
|
|
57
|
+
<main data-region="main" className="mx-auto grid w-full max-w-4xl grid-cols-[minmax(0,1fr)] gap-4 p-6">{regions.main}</main>
|
|
58
|
+
</SidebarProvider>
|
|
59
|
+
)
|
|
60
|
+
}
|
package/README.md
DELETED