create-mithaq 0.0.0-stage → 0.1.1
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/payload.txt +22 -0
- package/LICENSES/ponytail.txt +21 -0
- package/index.js +127 -0
- package/package.json +19 -4
- package/template/apps/asas-platform/components.json +25 -0
- package/template/apps/asas-platform/gitignore +3 -0
- package/template/apps/asas-platform/package.json +55 -0
- package/template/apps/asas-platform/public/favicon.svg +1 -0
- package/template/apps/asas-platform/src/api.ts +147 -0
- package/template/apps/asas-platform/src/auth-frame.tsx +150 -0
- package/template/apps/asas-platform/src/components/mithaq/cells.tsx +112 -0
- package/template/apps/asas-platform/src/components/mithaq/controls.tsx +650 -0
- package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +53 -0
- package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +108 -0
- package/template/apps/asas-platform/src/components/mithaq/form.tsx +1467 -0
- package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +108 -0
- package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +368 -0
- package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +153 -0
- package/template/apps/asas-platform/src/components/mithaq/list-query.ts +432 -0
- package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +74 -0
- package/template/apps/asas-platform/src/components/mithaq/list.tsx +865 -0
- package/template/apps/asas-platform/src/components/mithaq/nav.tsx +871 -0
- package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +1024 -0
- package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +127 -0
- package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +400 -0
- package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +6 -0
- package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +1083 -0
- package/template/apps/asas-platform/src/components/mithaq/upload.tsx +686 -0
- package/template/apps/asas-platform/src/components/ui/accordion.tsx +74 -0
- package/template/apps/asas-platform/src/components/ui/alert-dialog.tsx +185 -0
- package/template/apps/asas-platform/src/components/ui/avatar.tsx +106 -0
- package/template/apps/asas-platform/src/components/ui/badge.tsx +48 -0
- package/template/apps/asas-platform/src/components/ui/breadcrumb.tsx +124 -0
- package/template/apps/asas-platform/src/components/ui/button-group.tsx +87 -0
- package/template/apps/asas-platform/src/components/ui/button.tsx +55 -0
- package/template/apps/asas-platform/src/components/ui/calendar.tsx +223 -0
- package/template/apps/asas-platform/src/components/ui/card.tsx +102 -0
- package/template/apps/asas-platform/src/components/ui/chart.tsx +371 -0
- package/template/apps/asas-platform/src/components/ui/checkbox.tsx +28 -0
- package/template/apps/asas-platform/src/components/ui/collapsible.tsx +19 -0
- package/template/apps/asas-platform/src/components/ui/combobox.tsx +308 -0
- package/template/apps/asas-platform/src/components/ui/dialog.tsx +158 -0
- package/template/apps/asas-platform/src/components/ui/direction.tsx +6 -0
- package/template/apps/asas-platform/src/components/ui/directional-icon.tsx +20 -0
- package/template/apps/asas-platform/src/components/ui/drawer.tsx +216 -0
- package/template/apps/asas-platform/src/components/ui/dropdown-menu.tsx +267 -0
- package/template/apps/asas-platform/src/components/ui/field.tsx +236 -0
- package/template/apps/asas-platform/src/components/ui/input-group.tsx +155 -0
- package/template/apps/asas-platform/src/components/ui/input-otp.tsx +84 -0
- package/template/apps/asas-platform/src/components/ui/input.tsx +19 -0
- package/template/apps/asas-platform/src/components/ui/label.tsx +19 -0
- package/template/apps/asas-platform/src/components/ui/popover.tsx +90 -0
- package/template/apps/asas-platform/src/components/ui/radio-group.tsx +37 -0
- package/template/apps/asas-platform/src/components/ui/resizable.tsx +47 -0
- package/template/apps/asas-platform/src/components/ui/scroll-area.tsx +54 -0
- package/template/apps/asas-platform/src/components/ui/select.tsx +204 -0
- package/template/apps/asas-platform/src/components/ui/separator.tsx +24 -0
- package/template/apps/asas-platform/src/components/ui/sheet.tsx +139 -0
- package/template/apps/asas-platform/src/components/ui/sidebar.tsx +714 -0
- package/template/apps/asas-platform/src/components/ui/skeleton.tsx +13 -0
- package/template/apps/asas-platform/src/components/ui/slider.tsx +55 -0
- package/template/apps/asas-platform/src/components/ui/spinner.tsx +10 -0
- package/template/apps/asas-platform/src/components/ui/switch.tsx +29 -0
- package/template/apps/asas-platform/src/components/ui/table.tsx +115 -0
- package/template/apps/asas-platform/src/components/ui/tabs.tsx +79 -0
- package/template/apps/asas-platform/src/components/ui/textarea.tsx +17 -0
- package/template/apps/asas-platform/src/components/ui/toggle-group.tsx +89 -0
- package/template/apps/asas-platform/src/components/ui/toggle.tsx +44 -0
- package/template/apps/asas-platform/src/components/ui/tooltip.tsx +63 -0
- package/template/apps/asas-platform/src/copy.ts +1 -0
- package/template/apps/asas-platform/src/hooks/use-mobile.ts +19 -0
- package/template/apps/asas-platform/src/lib/utils.ts +1 -0
- package/template/apps/asas-platform/src/links.ts +38 -0
- package/template/apps/asas-platform/src/locale-switch.tsx +26 -0
- package/template/apps/asas-platform/src/redirect.ts +1 -0
- package/template/apps/asas-platform/src/request-headers.ts +11 -0
- package/template/apps/asas-platform/src/routeTree.gen.ts +181 -0
- package/template/apps/asas-platform/src/router.tsx +7 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +4 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform/screen.json +3609 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform/types.ts +145 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform.$.tsx +17 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform.index.tsx +10 -0
- package/template/apps/asas-platform/src/routes/(screens)/platform.tsx +13 -0
- package/template/apps/asas-platform/src/routes/__root.tsx +95 -0
- package/template/apps/asas-platform/src/routes/api/$.ts +30 -0
- package/template/apps/asas-platform/src/routes/auth.sign-in.tsx +72 -0
- package/template/apps/asas-platform/src/routes/unauthorized.tsx +122 -0
- package/template/apps/asas-platform/src/screen-form.tsx +34 -0
- package/template/apps/asas-platform/src/screen-list.tsx +139 -0
- package/template/apps/asas-platform/src/screen-names.ts +10 -0
- package/template/apps/asas-platform/src/screens.tsx +868 -0
- package/template/apps/asas-platform/src/server.ts +20 -0
- package/template/apps/asas-platform/src/slide.ts +22 -0
- package/template/apps/asas-platform/src/start.ts +42 -0
- package/template/apps/asas-platform/src/styles.css +200 -0
- package/template/apps/asas-platform/src/theme.ts +2 -0
- package/template/apps/asas-platform/tsconfig.json +19 -0
- package/template/apps/asas-platform/vite.config.ts +26 -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 +56 -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/components/mithaq/cells.tsx +112 -0
- package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +650 -0
- package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +53 -0
- package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +108 -0
- package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +1467 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +108 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +368 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +153 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +432 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +74 -0
- package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +865 -0
- package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +871 -0
- package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +1024 -0
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +127 -0
- package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +400 -0
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +6 -0
- package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +1083 -0
- package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +686 -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/drawer.tsx +215 -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/copy.ts +870 -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 +26 -0
- package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
- package/template/apps/asas-tanstack/src/redirect.ts +20 -0
- package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
- package/template/apps/asas-tanstack/src/routeTree.gen.ts +382 -0
- package/template/apps/asas-tanstack/src/router.tsx +7 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +4 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +3883 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +158 -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 +4 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1800 -0
- package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +92 -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 +101 -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 +109 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +86 -0
- package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +122 -0
- package/template/apps/asas-tanstack/src/routes/index.tsx +142 -0
- package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +122 -0
- package/template/apps/asas-tanstack/src/screen-form.tsx +34 -0
- package/template/apps/asas-tanstack/src/screen-list.tsx +139 -0
- package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
- package/template/apps/asas-tanstack/src/screens.tsx +868 -0
- package/template/apps/asas-tanstack/src/server.ts +16 -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 +159 -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 +28 -0
- package/template/package.json +16 -0
- package/template/templates/asas/.env.example +16 -0
- package/template/templates/asas/components.json +22 -0
- package/template/templates/asas/mithaq.hints.json +25 -0
- package/template/templates/asas/package.json +38 -0
- package/template/templates/asas/src/asas.ts +93 -0
- package/template/templates/asas/src/audit-log/audit-log.ts +187 -0
- package/template/templates/asas/src/audit-log/sink.ts +89 -0
- package/template/templates/asas/src/audit-log/ui/activity.tsx +243 -0
- package/template/templates/asas/src/courses/courses.ts +612 -0
- package/template/templates/asas/src/courses/ui/archive-button.tsx +25 -0
- package/template/templates/asas/src/courses/ui/fee-cell.tsx +19 -0
- package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +11 -0
- package/template/templates/asas/src/courses/ui/no-courses.tsx +13 -0
- package/template/templates/asas/src/courses/ui/on-offer.ts +6 -0
- package/template/templates/asas/src/courses/ui/status-cell.tsx +31 -0
- package/template/templates/asas/src/enrollments/enrollments.ts +137 -0
- package/template/templates/asas/src/enrollments/ui/session-cell.tsx +65 -0
- package/template/templates/asas/src/kayan/auth/user/account.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/account.ts +31 -0
- package/template/templates/asas/src/kayan/auth/user/index.ts +18 -0
- package/template/templates/asas/src/kayan/auth/user/invitation.access.ts +4 -0
- package/template/templates/asas/src/kayan/auth/user/invitation.ts +27 -0
- package/template/templates/asas/src/kayan/auth/user/member.access.ts +10 -0
- package/template/templates/asas/src/kayan/auth/user/member.screens.ts +37 -0
- package/template/templates/asas/src/kayan/auth/user/member.ts +25 -0
- package/template/templates/asas/src/kayan/auth/user/organization.access.ts +20 -0
- package/template/templates/asas/src/kayan/auth/user/organization.screens.ts +37 -0
- package/template/templates/asas/src/kayan/auth/user/organization.ts +24 -0
- package/template/templates/asas/src/kayan/auth/user/organizationRole.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/organizationRole.ts +25 -0
- package/template/templates/asas/src/kayan/auth/user/session.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/session.ts +28 -0
- package/template/templates/asas/src/kayan/auth/user/user.access.ts +29 -0
- package/template/templates/asas/src/kayan/auth/user/user.screens.ts +45 -0
- package/template/templates/asas/src/kayan/auth/user/user.ts +31 -0
- package/template/templates/asas/src/kayan/auth/user/verification.access.ts +8 -0
- package/template/templates/asas/src/kayan/auth/user/verification.ts +23 -0
- package/template/templates/asas/src/local-users/local-users.ts +143 -0
- package/template/templates/asas/src/media/media.ts +137 -0
- package/template/templates/asas/src/media/ui/preview.tsx +143 -0
- package/template/templates/asas/src/member-roles/member-roles.ts +207 -0
- package/template/templates/asas/src/messages/account.json +106 -0
- package/template/templates/asas/src/messages/auditLog.json +186 -0
- package/template/templates/asas/src/messages/courses.json +270 -0
- package/template/templates/asas/src/messages/enrollments.json +42 -0
- package/template/templates/asas/src/messages/invitation.json +66 -0
- package/template/templates/asas/src/messages/localUsers.json +42 -0
- package/template/templates/asas/src/messages/media.json +66 -0
- package/template/templates/asas/src/messages/member.json +42 -0
- package/template/templates/asas/src/messages/memberRoles.json +42 -0
- package/template/templates/asas/src/messages/mithaq.json +170 -0
- package/template/templates/asas/src/messages/organization.json +50 -0
- package/template/templates/asas/src/messages/organizationRole.json +50 -0
- package/template/templates/asas/src/messages/roles.json +58 -0
- package/template/templates/asas/src/messages/session.json +82 -0
- package/template/templates/asas/src/messages/sessions.json +106 -0
- package/template/templates/asas/src/messages/settings.json +66 -0
- package/template/templates/asas/src/messages/tags.json +38 -0
- package/template/templates/asas/src/messages/user.json +90 -0
- package/template/templates/asas/src/messages/verification.json +50 -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/drizzle.config.ts +11 -0
- package/template/templates/asas/src/migrations/schema.ts +29 -0
- package/template/templates/asas/src/mithaq.ts +406 -0
- package/template/templates/asas/src/pages.ts +74 -0
- package/template/templates/asas/src/platform-http.ts +96 -0
- package/template/templates/asas/src/rich-text-nodes.ts +11 -0
- package/template/templates/asas/src/roles/grants.ts +47 -0
- package/template/templates/asas/src/roles/roles.ts +184 -0
- package/template/templates/asas/src/roles/ui/role-form.tsx +226 -0
- package/template/templates/asas/src/roles/ui/roles.tsx +166 -0
- package/template/templates/asas/src/roles.ts +16 -0
- package/template/templates/asas/src/seed.ts +325 -0
- package/template/templates/asas/src/sessions/sessions.ts +155 -0
- package/template/templates/asas/src/settings/settings.ts +69 -0
- package/template/templates/asas/src/statements.ts +11 -0
- package/template/templates/asas/src/steps/expected-updated-at.ts +41 -0
- package/template/templates/asas/src/storage/defaults.ts +156 -0
- package/template/templates/asas/src/storage/index.ts +95 -0
- package/template/templates/asas/src/storage/local.ts +143 -0
- package/template/templates/asas/src/storage/r2.ts +5 -0
- package/template/templates/asas/src/storage/s3.ts +91 -0
- package/template/templates/asas/src/storage/uploadthing.ts +59 -0
- package/template/templates/asas/src/tags/tags.ts +80 -0
- package/template/templates/asas/src/tenant-access.ts +221 -0
- package/template/templates/asas/src/tenant-roles.ts +262 -0
- package/template/templates/asas/src/ui/app-shell.tsx +260 -0
- package/template/templates/asas/src/ui/kpi-card.tsx +25 -0
- package/template/templates/asas/src/ui/record-page.tsx +50 -0
- package/template/templates/asas/src/ui/rich-text-nodes.tsx +15 -0
- package/template/templates/asas/src/ui/screen-nav.tsx +28 -0
- package/template/templates/asas/src/ui/session-labels.tsx +36 -0
- package/template/templates/asas/src/ui/table-page.tsx +9 -0
- package/template/templates/asas/src/ui/top-shell.tsx +60 -0
- package/template/templates/asas/src/ui-dashboard/dashboard.tsx +294 -0
- package/template/templates/asas/src/ui-dashboard/members.tsx +630 -0
- package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
- package/template/templates/asas/src/ui-dashboard/versions.tsx +432 -0
- package/template/templates/asas/src/ui-platform/home.tsx +63 -0
- package/template/templates/asas/src/ui-platform/organization-actions.tsx +114 -0
- package/template/templates/asas/src/ui-platform/record-page.tsx +30 -0
- package/template/templates/asas/src/ui-platform/role-cells.tsx +52 -0
- package/template/templates/asas/src/ui-platform/user-actions.tsx +251 -0
- package/template/templates/asas/src/ui-portal/home.tsx +77 -0
- package/template/templates/asas/src/ui-portal/page.tsx +11 -0
- package/template/templates/auth/package.json +14 -0
- package/template/templates/auth/src/index.ts +690 -0
- package/template/templates/auth/src/platform-roles.ts +87 -0
- package/README.md +0 -3
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Written by `mithaq generate screens`. Do not edit.
|
|
2
|
+
import type { ScreenClient as Client } from 'mithaq/admin'
|
|
3
|
+
export type { PageClient } from 'mithaq/admin'
|
|
4
|
+
export type UserValues = {
|
|
5
|
+
id: string
|
|
6
|
+
updatedAt: Date
|
|
7
|
+
createdAt: Date
|
|
8
|
+
name: string
|
|
9
|
+
email: string
|
|
10
|
+
emailVerified: boolean
|
|
11
|
+
image: string | null
|
|
12
|
+
role: string | null
|
|
13
|
+
banned: boolean | null
|
|
14
|
+
banReason: string | null
|
|
15
|
+
banExpires: Date | null
|
|
16
|
+
}
|
|
17
|
+
export type OrganizationValues = {
|
|
18
|
+
id: string
|
|
19
|
+
updatedAt: Date
|
|
20
|
+
createdAt: Date
|
|
21
|
+
name: string
|
|
22
|
+
slug: string
|
|
23
|
+
logo: string | null
|
|
24
|
+
metadata: string | null
|
|
25
|
+
}
|
|
26
|
+
export type MemberValues = {
|
|
27
|
+
id: string
|
|
28
|
+
updatedAt: Date
|
|
29
|
+
createdAt: Date
|
|
30
|
+
organizationId: string
|
|
31
|
+
userId: string
|
|
32
|
+
role: string
|
|
33
|
+
}
|
|
34
|
+
export type CoursesValues = {
|
|
35
|
+
id: string
|
|
36
|
+
updatedAt: Date
|
|
37
|
+
createdAt: Date
|
|
38
|
+
organization: string
|
|
39
|
+
title: string
|
|
40
|
+
summary: string | null
|
|
41
|
+
fee: number | null
|
|
42
|
+
currency: 'SAR' | 'USD' | null
|
|
43
|
+
level: number | null
|
|
44
|
+
notifications: boolean | null
|
|
45
|
+
delivery: 'live' | 'recorded' | null
|
|
46
|
+
audience: 'beginner' | 'advanced' | null
|
|
47
|
+
accessCode: string | null
|
|
48
|
+
archived: boolean | null
|
|
49
|
+
topics: string[] | null
|
|
50
|
+
opensOn: string | null
|
|
51
|
+
archiveReason: string | null
|
|
52
|
+
tags: string[] | null
|
|
53
|
+
scheduledSessions: Values['sessions'][]
|
|
54
|
+
_status: 'draft' | 'published' | null
|
|
55
|
+
details: { root: Record<string, unknown> } | null
|
|
56
|
+
cover: unknown
|
|
57
|
+
}
|
|
58
|
+
export type SessionsValues = {
|
|
59
|
+
id: string
|
|
60
|
+
updatedAt: Date
|
|
61
|
+
createdAt: Date
|
|
62
|
+
organization: string
|
|
63
|
+
course: string
|
|
64
|
+
startsAt: Date
|
|
65
|
+
capacity: number | null
|
|
66
|
+
venue: 'online' | 'campus' | null
|
|
67
|
+
format: 'workshop' | 'lecture' | null
|
|
68
|
+
archived: boolean | null
|
|
69
|
+
details: { root: Record<string, unknown> } | null
|
|
70
|
+
enrollments: Values['enrollments'][]
|
|
71
|
+
}
|
|
72
|
+
export type EnrollmentsValues = {
|
|
73
|
+
id: string
|
|
74
|
+
updatedAt: Date
|
|
75
|
+
createdAt: Date
|
|
76
|
+
organization: string
|
|
77
|
+
session: string
|
|
78
|
+
student: string
|
|
79
|
+
deletedAt: Date | null
|
|
80
|
+
}
|
|
81
|
+
export type TagsValues = {
|
|
82
|
+
id: string
|
|
83
|
+
updatedAt: Date
|
|
84
|
+
createdAt: Date
|
|
85
|
+
organization: string
|
|
86
|
+
name: Record<'ar' | 'en', string>
|
|
87
|
+
}
|
|
88
|
+
export type RolesValues = {
|
|
89
|
+
id: string
|
|
90
|
+
updatedAt: Date
|
|
91
|
+
createdAt: Date
|
|
92
|
+
organization: string
|
|
93
|
+
name: string
|
|
94
|
+
label: Record<'ar' | 'en', string>
|
|
95
|
+
permissions: string[]
|
|
96
|
+
}
|
|
97
|
+
export type MemberRolesValues = {
|
|
98
|
+
id: string
|
|
99
|
+
updatedAt: Date
|
|
100
|
+
createdAt: Date
|
|
101
|
+
organization: string
|
|
102
|
+
user: string
|
|
103
|
+
role: string
|
|
104
|
+
}
|
|
105
|
+
export type LocalUsersValues = {
|
|
106
|
+
id: string
|
|
107
|
+
updatedAt: Date
|
|
108
|
+
createdAt: Date
|
|
109
|
+
email: string
|
|
110
|
+
hash: string
|
|
111
|
+
salt: string
|
|
112
|
+
sessions: string | null
|
|
113
|
+
loginAttempts: number | null
|
|
114
|
+
lockUntil: Date | null
|
|
115
|
+
resetPasswordToken: string | null
|
|
116
|
+
resetPasswordAttempts: number | null
|
|
117
|
+
resetPasswordExpiration: Date | null
|
|
118
|
+
_verified: boolean | null
|
|
119
|
+
_verificationToken: string | null
|
|
120
|
+
}
|
|
121
|
+
export type Values = {
|
|
122
|
+
user: UserValues
|
|
123
|
+
organization: OrganizationValues
|
|
124
|
+
member: MemberValues
|
|
125
|
+
courses: CoursesValues
|
|
126
|
+
sessions: SessionsValues
|
|
127
|
+
enrollments: EnrollmentsValues
|
|
128
|
+
tags: TagsValues
|
|
129
|
+
roles: RolesValues
|
|
130
|
+
memberRoles: MemberRolesValues
|
|
131
|
+
localUsers: LocalUsersValues
|
|
132
|
+
}
|
|
133
|
+
export type Pages = {
|
|
134
|
+
user: 'list' | 'view'
|
|
135
|
+
organization: 'list' | 'view'
|
|
136
|
+
member: 'list' | 'view'
|
|
137
|
+
courses: 'list' | 'view'
|
|
138
|
+
sessions: 'list' | 'view'
|
|
139
|
+
enrollments: 'list' | 'view'
|
|
140
|
+
tags: 'list' | 'view'
|
|
141
|
+
roles: 'list' | 'view'
|
|
142
|
+
memberRoles: 'list' | 'view'
|
|
143
|
+
localUsers: 'list' | 'edit'
|
|
144
|
+
}
|
|
145
|
+
export type ScreenClient = Client<Pages>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Written by `mithaq generate screens`. Do not edit.
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import { ScreenNotFound, ScreenPageView, askedFor, pageHead, pageLoader } from '@/screens.tsx'
|
|
4
|
+
import { files } from './platform/files.ts'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/(screens)/platform/$')({
|
|
7
|
+
validateSearch: askedFor,
|
|
8
|
+
loader: ({ location, params }) => pageLoader({ screen: 'platform', files, path: params._splat ?? '', query: location.search, href: location.href }),
|
|
9
|
+
head: ({ loaderData, match }) => pageHead(loaderData, match.context),
|
|
10
|
+
notFoundComponent: () => <ScreenNotFound files={files} screen={'platform'} />,
|
|
11
|
+
component: Page,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
function Page() {
|
|
15
|
+
const navigate = Route.useNavigate()
|
|
16
|
+
return <ScreenPageView files={files} data={Route.useLoaderData()} query={Route.useSearch()} onQuery={(search) => void navigate({ search })} />
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Written by `mithaq generate screens`. Do not edit.
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import { ScreenPageView, askedFor, pageHead, pageLoader } from '@/screens.tsx'
|
|
4
|
+
import { files } from './platform/files.ts'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/(screens)/platform/')({
|
|
7
|
+
loader: ({ location }) => pageLoader({ screen: 'platform', files, path: '', query: {}, href: location.href }),
|
|
8
|
+
head: ({ loaderData, match }) => pageHead(loaderData, match.context),
|
|
9
|
+
component: () => <ScreenPageView files={files} data={Route.useLoaderData()} query={askedFor({})} onQuery={() => {}} />,
|
|
10
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Written by `mithaq generate screens`. Do not edit.
|
|
2
|
+
import { Outlet, createFileRoute } from '@tanstack/react-router'
|
|
3
|
+
import { ScreenFrame, frameLoader } from '@/screens.tsx'
|
|
4
|
+
import { files } from './platform/files.ts'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/(screens)/platform')({
|
|
7
|
+
loader: ({ location }) => frameLoader({ screen: 'platform', files, href: location.href }),
|
|
8
|
+
component: () => (
|
|
9
|
+
<ScreenFrame files={files} data={Route.useLoaderData()}>
|
|
10
|
+
<Outlet />
|
|
11
|
+
</ScreenFrame>
|
|
12
|
+
),
|
|
13
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { HeadContent, Outlet, Scripts, createRootRoute } from '@tanstack/react-router'
|
|
2
|
+
import { DirectionProvider } from '@/components/ui/direction'
|
|
3
|
+
export { locales } from '../copy.ts'
|
|
4
|
+
import { dirOf, documentHead, localeOf, translator } from '../copy.ts'
|
|
5
|
+
import { ThemeProvider } from '../components/mithaq/nav.tsx'
|
|
6
|
+
import { themeStorageKey } from '../theme.ts'
|
|
7
|
+
import appCss from '../styles.css?url'
|
|
8
|
+
|
|
9
|
+
// Puts the chosen theme on the document before first paint, which the provider's effect cannot do.
|
|
10
|
+
// Also reloads a page restored from the back-forward cache, so Back after signing out asks the server again.
|
|
11
|
+
const theme = `
|
|
12
|
+
(function() {
|
|
13
|
+
var storageKey = ${JSON.stringify(themeStorageKey)};
|
|
14
|
+
var defaultTheme = "system";
|
|
15
|
+
try {
|
|
16
|
+
var theme = localStorage.getItem(storageKey) || defaultTheme;
|
|
17
|
+
var systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
18
|
+
var root = document.documentElement;
|
|
19
|
+
root.classList.remove("light", "dark");
|
|
20
|
+
root.classList.add(theme === "system" ? systemTheme : theme);
|
|
21
|
+
} catch (e) {}
|
|
22
|
+
window.addEventListener("pageshow", function (event) {
|
|
23
|
+
if (event.persisted) location.reload();
|
|
24
|
+
});
|
|
25
|
+
})()
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
// A request context nothing registered is `never` in the router's types, so the shape is named and read
|
|
29
|
+
// through a cast; `localeOf` turns it back into a declared locale.
|
|
30
|
+
type FromRequest = { serverContext?: { locale?: unknown } }
|
|
31
|
+
|
|
32
|
+
export const Route = createRootRoute({
|
|
33
|
+
// On the client the locale is read back off the `lang` the server wrote on `<html>`, so the hydrated tree
|
|
34
|
+
// matches the one sent. `localeOf` on both refuses a hand-edited `lang` like a hand-written cookie.
|
|
35
|
+
beforeLoad: (ctx) => {
|
|
36
|
+
const locale =
|
|
37
|
+
typeof document === 'undefined'
|
|
38
|
+
? localeOf((ctx as unknown as FromRequest).serverContext?.locale as string | undefined)
|
|
39
|
+
: localeOf(document.documentElement.lang)
|
|
40
|
+
return { locale, dir: dirOf(locale), app: 'platform.brand' }
|
|
41
|
+
},
|
|
42
|
+
// The favicon stops the browser asking for a `/favicon.ico` nobody serves.
|
|
43
|
+
head: ({ match }) => ({
|
|
44
|
+
meta: [
|
|
45
|
+
{ charSet: 'utf-8' },
|
|
46
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
47
|
+
...documentHead(match.context, match._notFound ? (t) => t('admin.notFoundTitle', 'Page not found') : undefined).meta,
|
|
48
|
+
],
|
|
49
|
+
links: [{ rel: 'stylesheet', href: appCss }, { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
|
|
50
|
+
}),
|
|
51
|
+
// Without it the router draws a development-only placeholder and warns.
|
|
52
|
+
notFoundComponent: NotFound,
|
|
53
|
+
component: RootDocument,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/** The locale and direction of this request and the `t` resolved for it, read from the root route's context. */
|
|
57
|
+
export const useCopy = () => {
|
|
58
|
+
const { dir, locale } = Route.useRouteContext()
|
|
59
|
+
return { dir, locale, t: translator(locale) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function NotFound() {
|
|
63
|
+
const { t } = useCopy()
|
|
64
|
+
return (
|
|
65
|
+
<main className="flex min-h-screen flex-col items-center justify-center gap-4 px-4 text-center">
|
|
66
|
+
<h1 className="text-2xl font-bold tracking-tight">{t('admin.notFoundTitle', 'Page not found')}</h1>
|
|
67
|
+
<p className="text-muted-foreground text-sm">{t('admin.notFound', 'That page does not exist.')}</p>
|
|
68
|
+
{/* An anchor, because `/` is no route here. */}
|
|
69
|
+
<a className="text-foreground text-sm underline underline-offset-4" href="/">
|
|
70
|
+
{t('admin.goHome', 'Go to the home page')}
|
|
71
|
+
</a>
|
|
72
|
+
</main>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function RootDocument() {
|
|
77
|
+
const { dir, locale } = useCopy()
|
|
78
|
+
return (
|
|
79
|
+
<html lang={locale} dir={dir} suppressHydrationWarning>
|
|
80
|
+
<head>
|
|
81
|
+
<HeadContent />
|
|
82
|
+
{/* The string is this module's own and carries no request value, so writing it raw is safe. */}
|
|
83
|
+
<script dangerouslySetInnerHTML={{ __html: theme }} />
|
|
84
|
+
</head>
|
|
85
|
+
<body>
|
|
86
|
+
<DirectionProvider direction={dir}>
|
|
87
|
+
<ThemeProvider defaultTheme="system" storageKey={themeStorageKey}>
|
|
88
|
+
<Outlet />
|
|
89
|
+
</ThemeProvider>
|
|
90
|
+
</DirectionProvider>
|
|
91
|
+
<Scripts />
|
|
92
|
+
</body>
|
|
93
|
+
</html>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
// Loads the types of the `server` route option; erased at build.
|
|
3
|
+
import type {} from '@tanstack/start-client-core'
|
|
4
|
+
import { asas } from 'asas'
|
|
5
|
+
|
|
6
|
+
// The platform reads every organization: a read of a kayan asks `tenant: 'all'`, which the runner
|
|
7
|
+
// grants platform staff in this app alone and refuses anyone else. A global is one organization's row
|
|
8
|
+
// and asks for none.
|
|
9
|
+
const reads = new Set(asas.operations.filter((one) => one.effect === 'read' && !one.single).map((one) => `/api/${one.kayan}/${one.name}`))
|
|
10
|
+
|
|
11
|
+
const acrossTenants = async (request: Request) => {
|
|
12
|
+
const url = new URL(request.url)
|
|
13
|
+
if (!reads.has(url.pathname)) return request
|
|
14
|
+
url.searchParams.set('tenant', 'all')
|
|
15
|
+
const body = request.method === 'GET' || request.method === 'HEAD' ? undefined : await request.text()
|
|
16
|
+
return new Request(url, { method: request.method, headers: request.headers, body })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A write whose `Origin` is not this app's own is refused: the data plug checks no origin and leans on
|
|
20
|
+
// the session cookie's `SameSite=Lax`, which a sibling origin of the same site passes.
|
|
21
|
+
export const Route = createFileRoute('/api/$')({
|
|
22
|
+
server: {
|
|
23
|
+
handlers: {
|
|
24
|
+
ANY: async ({ request }) =>
|
|
25
|
+
request.method === 'GET' || request.method === 'HEAD' || request.headers.get('origin') === new URL(request.url).origin
|
|
26
|
+
? asas.handler(await acrossTenants(request))
|
|
27
|
+
: new Response('Forbidden', { status: 403 }),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useState, type ComponentProps } from 'react'
|
|
2
|
+
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { authArm } from '../api.ts'
|
|
5
|
+
import { AuthBox, AuthFrame, authGate, notAnEmail, redirectSearch, useRefusal } from '../auth-frame.tsx'
|
|
6
|
+
import { documentHead } from '../copy.ts'
|
|
7
|
+
import { useCopy } from './__root.tsx'
|
|
8
|
+
import { wanted } from '../redirect.ts'
|
|
9
|
+
|
|
10
|
+
export const Route = createFileRoute('/auth/sign-in')({
|
|
11
|
+
head: ({ match }) => documentHead(match.context, (t) => t('auth.signIn')),
|
|
12
|
+
validateSearch: redirectSearch,
|
|
13
|
+
component: SignIn,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
function SignIn() {
|
|
17
|
+
const navigate = useNavigate()
|
|
18
|
+
const { t } = useCopy()
|
|
19
|
+
const search = Route.useSearch()
|
|
20
|
+
const refused = useRefusal()
|
|
21
|
+
const [email, setEmail] = useState('')
|
|
22
|
+
const [password, setPassword] = useState('')
|
|
23
|
+
const [pending, setPending] = useState(false)
|
|
24
|
+
const [refusal, setRefusal] = useState<string | null>(null)
|
|
25
|
+
const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
|
|
26
|
+
const required = t('auth.required', 'This field is required.')
|
|
27
|
+
const wrong = {
|
|
28
|
+
email: () => (!email ? required : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined),
|
|
29
|
+
password: () => (password ? undefined : required),
|
|
30
|
+
}
|
|
31
|
+
const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
|
|
32
|
+
const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'>) => (
|
|
33
|
+
<AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const submit = async (event: React.FormEvent) => {
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
setLeft(new Set(Object.keys(wrong)))
|
|
39
|
+
if (Object.values(wrong).some((check) => check())) return
|
|
40
|
+
setPending(true)
|
|
41
|
+
setRefusal(null)
|
|
42
|
+
const answered = await authArm('sign-in/email', { email, password })
|
|
43
|
+
// Our own sentence for the provider's code, not its English `message`: a refusal is copy and carries a string per locale.
|
|
44
|
+
if (!answered.ok) {
|
|
45
|
+
setPending(false)
|
|
46
|
+
return setRefusal(refused(answered, t('auth.failed', 'That sign-in did not work.')))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setPending(false)
|
|
50
|
+
|
|
51
|
+
// A navigation, not a reload, so the screen's loader runs with the cookie just set.
|
|
52
|
+
await navigate({ to: wanted({ asked: search.redirect, home: (await authGate()).home.path }) })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<AuthFrame title={t('auth.signIn', 'Sign in')} description={t('auth.signInPlatformDescription', 'Sign in to reach the platform.')}>
|
|
57
|
+
<form className="grid gap-4" noValidate onSubmit={submit}>
|
|
58
|
+
{box('email', t('auth.email', 'Email'), { autoComplete: 'email', onChange: (event) => setEmail(event.target.value), type: 'email', value: email })}
|
|
59
|
+
{box('password', t('auth.password', 'Password'), { autoComplete: 'current-password', onChange: (event) => setPassword(event.target.value), type: 'password', value: password })}
|
|
60
|
+
{/* `role="alert"` so a refusal is spoken when it appears; nothing moves the focus to it. */}
|
|
61
|
+
{refusal && (
|
|
62
|
+
<p className="text-destructive text-sm" role="alert">
|
|
63
|
+
{refusal}
|
|
64
|
+
</p>
|
|
65
|
+
)}
|
|
66
|
+
<Button disabled={pending} type="submit">
|
|
67
|
+
{pending ? t('auth.signInPending', 'Signing in…') : t('auth.signIn', 'Sign in')}
|
|
68
|
+
</Button>
|
|
69
|
+
</form>
|
|
70
|
+
</AuthFrame>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Fragment, useState, type ReactNode } from 'react'
|
|
2
|
+
import { createFileRoute, redirect } from '@tanstack/react-router'
|
|
3
|
+
import { Button } from '@/components/ui/button'
|
|
4
|
+
import { authArm } from '../api.ts'
|
|
5
|
+
import { AuthFrame, useRefusal } from '../auth-frame.tsx'
|
|
6
|
+
import { documentHead } from '../copy.ts'
|
|
7
|
+
import { ImpersonationBar, loadEnterable } from '../screens.tsx'
|
|
8
|
+
import { useCopy } from './__root.tsx'
|
|
9
|
+
|
|
10
|
+
export const Route = createFileRoute('/unauthorized')({
|
|
11
|
+
// The path a screen refused; the server only matches it against the build's pages.
|
|
12
|
+
validateSearch: (search: Record<string, unknown>): { from?: string } => (typeof search.from === 'string' ? { from: search.from } : {}),
|
|
13
|
+
loaderDeps: ({ search }) => ({ from: search.from }),
|
|
14
|
+
loader: async ({ deps, context }) => {
|
|
15
|
+
const entered = await loadEnterable({ data: { from: deps.from, locale: context.locale } })
|
|
16
|
+
// Carries this page's own path so a visitor whose session expired lands back here after signing in.
|
|
17
|
+
if (!entered) throw redirect({ to: '/auth/sign-in', search: { redirect: '/unauthorized' } })
|
|
18
|
+
return entered
|
|
19
|
+
},
|
|
20
|
+
// After the loader, so its data's type is inferred before this reads it.
|
|
21
|
+
head: ({ match, loaderData }) => documentHead(match.context, (t) => (loaderData?.screens.length === 0 ? t('auth.noAccess') : t('auth.unauthorized'))),
|
|
22
|
+
component: Unauthorized,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function Unauthorized() {
|
|
26
|
+
const { t } = useCopy()
|
|
27
|
+
const { screens, refused, email, impersonating, organizations = [], others = [] } = Route.useLoaderData()
|
|
28
|
+
const refusal = useRefusal()
|
|
29
|
+
const [switching, setSwitching] = useState<string>()
|
|
30
|
+
const [failed, setFailed] = useState<string>()
|
|
31
|
+
// The slug sits beside the name where another membership shares the name.
|
|
32
|
+
const memberships = [...organizations, ...others]
|
|
33
|
+
const named = (one: { id: string; name: string; slug?: string }): ReactNode =>
|
|
34
|
+
one.slug && memberships.some((other) => other.id !== one.id && other.name === one.name) ? (
|
|
35
|
+
<>
|
|
36
|
+
{one.name} <span dir="ltr" data-slug="" className="text-muted-foreground text-xs font-normal">{one.slug}</span>
|
|
37
|
+
</>
|
|
38
|
+
) : (
|
|
39
|
+
one.name
|
|
40
|
+
)
|
|
41
|
+
// Each name is isolated so an email or Latin name in an Arabic sentence keeps
|
|
42
|
+
// its order, and is one inline box that wraps whole, so a long address never
|
|
43
|
+
// runs past the card on a phone.
|
|
44
|
+
const whole = 'inline-block max-w-full [overflow-wrap:anywhere]'
|
|
45
|
+
const said = (text: string | undefined) =>
|
|
46
|
+
text!.split(/(\{email\}|\{organization\}|\{page\})/).map((part, at) =>
|
|
47
|
+
part === '{email}' ? <bdi key={at} className={whole}>{email}</bdi>
|
|
48
|
+
: part === '{organization}' ? <bdi key={at}>{organizations.map((one, index) => <Fragment key={one.id}>{index ? ', ' : ''}<span className={whole}>{named(one)}</span></Fragment>)}</bdi>
|
|
49
|
+
: part === '{page}' ? <bdi key={at}>{refused?.page}</bdi>
|
|
50
|
+
: part)
|
|
51
|
+
const link = 'text-foreground underline underline-offset-4'
|
|
52
|
+
const screenName = (name: string) => t(`screens.${name}`, name) ?? name
|
|
53
|
+
return (
|
|
54
|
+
<AuthFrame
|
|
55
|
+
title={screens.length ? t('auth.unauthorized', 'Not for your role') : t('auth.noAccess', 'Your account has no access yet')}
|
|
56
|
+
description={
|
|
57
|
+
<span data-no-access={screens.length ? undefined : ''}>
|
|
58
|
+
{refused
|
|
59
|
+
? said(t('auth.unauthorizedPage', 'Your role does not open that page: {page}.'))
|
|
60
|
+
: screens.length
|
|
61
|
+
? t('auth.unauthorizedDescription', 'Your role does not open that screen. These are the screens it opens:')
|
|
62
|
+
: organizations.length
|
|
63
|
+
? said(t('auth.noAccessDescription', '{email} is a member of {organization} and holds no role that opens a screen here. Ask an administrator of {organization} to give you one.'))
|
|
64
|
+
: said(t('auth.noOrganization', '{email} belongs to no organization yet. Ask an administrator to invite you.'))}
|
|
65
|
+
</span>
|
|
66
|
+
}
|
|
67
|
+
>
|
|
68
|
+
<div className="grid gap-4">
|
|
69
|
+
{/* Stopping impersonation is the way out in place of Sign out. */}
|
|
70
|
+
{impersonating ? <ImpersonationBar name={impersonating} /> : null}
|
|
71
|
+
{refused ? (
|
|
72
|
+
<a className={link} href={refused.screen.path}>
|
|
73
|
+
{(t('auth.backTo', 'Back to {screen}') ?? 'Back to {screen}').replace('{screen}', () => screenName(refused.screen.name))}
|
|
74
|
+
</a>
|
|
75
|
+
) : screens.length ? (
|
|
76
|
+
<ul className="grid gap-2">
|
|
77
|
+
{screens.map((screen) => (
|
|
78
|
+
<li key={screen.name}>
|
|
79
|
+
{/* An anchor: the path is the screen's own data, not a route the router knows. */}
|
|
80
|
+
<a className={link} href={screen.path}>
|
|
81
|
+
{screenName(screen.name)}
|
|
82
|
+
</a>
|
|
83
|
+
</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
) : null}
|
|
87
|
+
{/* So a switch into an organization where the actor holds no role is not a dead end. */}
|
|
88
|
+
{others.map((one) => (
|
|
89
|
+
<Button
|
|
90
|
+
key={one.id}
|
|
91
|
+
type="button"
|
|
92
|
+
variant="secondary"
|
|
93
|
+
data-switch-organization={one.id}
|
|
94
|
+
// Busy while it runs, so a second press sends nothing.
|
|
95
|
+
disabled={switching !== undefined}
|
|
96
|
+
aria-busy={switching === one.id || undefined}
|
|
97
|
+
onClick={() => {
|
|
98
|
+
setSwitching(one.id)
|
|
99
|
+
setFailed(undefined)
|
|
100
|
+
void authArm('organization/set-active', { organizationId: one.id }).then((answer) => {
|
|
101
|
+
if (answer.ok) return window.location.assign('/auth/sign-in')
|
|
102
|
+
setSwitching(undefined)
|
|
103
|
+
setFailed(refusal(answer, t('auth.requestFailed', 'That request did not work.')))
|
|
104
|
+
})
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{/* One box, so the button's gap does not open beside the name. */}
|
|
108
|
+
<span>{(t('auth.switchTo', 'Switch to {organization}') ?? 'Switch to {organization}').split(/(\{organization\})/).map((part, at) => (part === '{organization}' ? <bdi key={at}>{named(one)}</bdi> : part))}</span>
|
|
109
|
+
</Button>
|
|
110
|
+
))}
|
|
111
|
+
{failed ? <p role="alert" className="text-destructive text-sm">{failed}</p> : null}
|
|
112
|
+
{/* `replace`, so Back does not return to the page signed out of. */}
|
|
113
|
+
{impersonating ? null : (
|
|
114
|
+
<Button type="button" variant="outline" className="max-sm:h-11" onClick={() => void authArm('sign-out').then(() => window.location.replace('/auth/sign-in'))}>
|
|
115
|
+
{t('admin.signOut', 'Sign out')}
|
|
116
|
+
</Button>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
</AuthFrame>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
// mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 3c5c304b3892c56759bf780f155ef3aa154ce3cc954e3d240b3537cdd97da735
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useBlocker, useRouter } from '@tanstack/react-router'
|
|
2
|
+
import { useState, type ReactElement } from 'react'
|
|
3
|
+
import { Form, type FormProps } from './components/mithaq/form.tsx'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A kayan's form on a screen's page. A create moves to the new row's edit page, a delete to the list.
|
|
7
|
+
* While the form holds a change the router blocks navigation and the form asks its discard question.
|
|
8
|
+
*/
|
|
9
|
+
export function ScreenForm({ edit, list, ...props }: Omit<FormProps, 'onDone' | 'onDirty' | 'onCancel' | 'blocked'> & {
|
|
10
|
+
/** The href of a row's edit page, or nothing where the kayan has none. */
|
|
11
|
+
edit: (id: string) => string | undefined
|
|
12
|
+
/** The href of the kayan's list, or nothing where it has none. */
|
|
13
|
+
list?: string
|
|
14
|
+
}): ReactElement {
|
|
15
|
+
const router = useRouter()
|
|
16
|
+
const [dirty, setDirty] = useState(false)
|
|
17
|
+
const blocker = useBlocker({ shouldBlockFn: () => dirty, withResolver: true, enableBeforeUnload: false })
|
|
18
|
+
// `ignoreBlocker`: the form reports clean only on the render after the save.
|
|
19
|
+
const go = (href: string | undefined) => void (href ? router.navigate({ href, ignoreBlocker: true }) : router.invalidate())
|
|
20
|
+
const landing = (row: unknown, effect?: string) => {
|
|
21
|
+
const id = (row as { id?: unknown } | null | undefined)?.id
|
|
22
|
+
return effect === 'delete' ? list : effect === 'create' && id !== undefined ? edit(String(id)) : undefined
|
|
23
|
+
}
|
|
24
|
+
return (
|
|
25
|
+
<Form
|
|
26
|
+
{...props}
|
|
27
|
+
onDirty={setDirty}
|
|
28
|
+
onDone={(row, effect) => go(landing(row, effect))}
|
|
29
|
+
onCancel={list ? () => go(list) : undefined}
|
|
30
|
+
blocked={blocker.status === 'blocked' ? blocker : undefined}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
// mithaq-admin b06d8d335d306c07556f8f2b81fba20ba7ded198 7284b5e97770fd0c395fafa0b7459cab4ceb458e4bc982dc8233894126116170
|