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.
Files changed (396) hide show
  1. package/LICENSE +22 -0
  2. package/LICENSES/openstatus-time-picker.txt +21 -0
  3. package/LICENSES/payload.txt +22 -0
  4. package/LICENSES/ponytail.txt +21 -0
  5. package/index.js +521 -0
  6. package/package.json +19 -4
  7. package/template/apps/asas-backoffice/components.json +25 -0
  8. package/template/apps/asas-backoffice/gitignore +3 -0
  9. package/template/apps/asas-backoffice/package.json +57 -0
  10. package/template/apps/asas-backoffice/public/favicon.svg +1 -0
  11. package/template/apps/asas-backoffice/src/api.ts +147 -0
  12. package/template/apps/asas-backoffice/src/auth-frame.tsx +150 -0
  13. package/template/apps/asas-backoffice/src/components/ui/accordion.tsx +74 -0
  14. package/template/apps/asas-backoffice/src/components/ui/alert-dialog.tsx +185 -0
  15. package/template/apps/asas-backoffice/src/components/ui/avatar.tsx +106 -0
  16. package/template/apps/asas-backoffice/src/components/ui/badge.tsx +48 -0
  17. package/template/apps/asas-backoffice/src/components/ui/breadcrumb.tsx +124 -0
  18. package/template/apps/asas-backoffice/src/components/ui/button-group.tsx +87 -0
  19. package/template/apps/asas-backoffice/src/components/ui/button.tsx +55 -0
  20. package/template/apps/asas-backoffice/src/components/ui/calendar.tsx +223 -0
  21. package/template/apps/asas-backoffice/src/components/ui/card.tsx +102 -0
  22. package/template/apps/asas-backoffice/src/components/ui/chart.tsx +371 -0
  23. package/template/apps/asas-backoffice/src/components/ui/checkbox.tsx +28 -0
  24. package/template/apps/asas-backoffice/src/components/ui/collapsible.tsx +19 -0
  25. package/template/apps/asas-backoffice/src/components/ui/combobox.tsx +308 -0
  26. package/template/apps/asas-backoffice/src/components/ui/dialog.tsx +158 -0
  27. package/template/apps/asas-backoffice/src/components/ui/direction.tsx +6 -0
  28. package/template/apps/asas-backoffice/src/components/ui/directional-icon.tsx +20 -0
  29. package/template/apps/asas-backoffice/src/components/ui/dropdown-menu.tsx +267 -0
  30. package/template/apps/asas-backoffice/src/components/ui/field.tsx +236 -0
  31. package/template/apps/asas-backoffice/src/components/ui/input-group.tsx +155 -0
  32. package/template/apps/asas-backoffice/src/components/ui/input-otp.tsx +84 -0
  33. package/template/apps/asas-backoffice/src/components/ui/input.tsx +19 -0
  34. package/template/apps/asas-backoffice/src/components/ui/label.tsx +19 -0
  35. package/template/apps/asas-backoffice/src/components/ui/popover.tsx +90 -0
  36. package/template/apps/asas-backoffice/src/components/ui/radio-group.tsx +37 -0
  37. package/template/apps/asas-backoffice/src/components/ui/resizable.tsx +47 -0
  38. package/template/apps/asas-backoffice/src/components/ui/scroll-area.tsx +54 -0
  39. package/template/apps/asas-backoffice/src/components/ui/select.tsx +204 -0
  40. package/template/apps/asas-backoffice/src/components/ui/separator.tsx +24 -0
  41. package/template/apps/asas-backoffice/src/components/ui/sheet.tsx +139 -0
  42. package/template/apps/asas-backoffice/src/components/ui/sidebar.tsx +714 -0
  43. package/template/apps/asas-backoffice/src/components/ui/skeleton.tsx +13 -0
  44. package/template/apps/asas-backoffice/src/components/ui/slider.tsx +55 -0
  45. package/template/apps/asas-backoffice/src/components/ui/spinner.tsx +10 -0
  46. package/template/apps/asas-backoffice/src/components/ui/switch.tsx +29 -0
  47. package/template/apps/asas-backoffice/src/components/ui/table.tsx +115 -0
  48. package/template/apps/asas-backoffice/src/components/ui/tabs.tsx +79 -0
  49. package/template/apps/asas-backoffice/src/components/ui/textarea.tsx +17 -0
  50. package/template/apps/asas-backoffice/src/components/ui/toggle-group.tsx +89 -0
  51. package/template/apps/asas-backoffice/src/components/ui/toggle.tsx +44 -0
  52. package/template/apps/asas-backoffice/src/components/ui/tooltip.tsx +63 -0
  53. package/template/apps/asas-backoffice/src/components.ts +35 -0
  54. package/template/apps/asas-backoffice/src/copy.ts +1 -0
  55. package/template/apps/asas-backoffice/src/first-user.ts +14 -0
  56. package/template/apps/asas-backoffice/src/hooks/use-mobile.ts +19 -0
  57. package/template/apps/asas-backoffice/src/lib/utils.ts +1 -0
  58. package/template/apps/asas-backoffice/src/links.ts +38 -0
  59. package/template/apps/asas-backoffice/src/locale-switch.tsx +28 -0
  60. package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
  61. package/template/apps/asas-backoffice/src/redirect.ts +1 -0
  62. package/template/apps/asas-backoffice/src/request-headers.ts +11 -0
  63. package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
  64. package/template/apps/asas-backoffice/src/router.tsx +18 -0
  65. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
  66. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/screen.json +3971 -0
  67. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/types.ts +163 -0
  68. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.$.tsx +17 -0
  69. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.index.tsx +10 -0
  70. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice.tsx +13 -0
  71. package/template/apps/asas-backoffice/src/routes/__root.tsx +95 -0
  72. package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
  73. package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
  74. package/template/apps/asas-backoffice/src/routes/auth.sign-in.tsx +77 -0
  75. package/template/apps/asas-backoffice/src/routes/unauthorized.tsx +134 -0
  76. package/template/apps/asas-backoffice/src/screen-form.tsx +36 -0
  77. package/template/apps/asas-backoffice/src/screen-list.tsx +140 -0
  78. package/template/apps/asas-backoffice/src/screen-names.ts +10 -0
  79. package/template/apps/asas-backoffice/src/screens.tsx +888 -0
  80. package/template/apps/asas-backoffice/src/server.ts +30 -0
  81. package/template/apps/asas-backoffice/src/slide.ts +22 -0
  82. package/template/apps/asas-backoffice/src/start.ts +42 -0
  83. package/template/apps/asas-backoffice/src/styles.css +202 -0
  84. package/template/apps/asas-backoffice/src/theme.ts +2 -0
  85. package/template/apps/asas-backoffice/tsconfig.json +19 -0
  86. package/template/apps/asas-backoffice/vite.config.ts +32 -0
  87. package/template/apps/asas-tanstack/components.json +25 -0
  88. package/template/apps/asas-tanstack/gitignore +3 -0
  89. package/template/apps/asas-tanstack/package.json +58 -0
  90. package/template/apps/asas-tanstack/public/favicon.svg +1 -0
  91. package/template/apps/asas-tanstack/src/api.ts +147 -0
  92. package/template/apps/asas-tanstack/src/auth-frame.tsx +150 -0
  93. package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
  94. package/template/apps/asas-tanstack/src/components/ui/accordion.tsx +10 -0
  95. package/template/apps/asas-tanstack/src/components/ui/alert-dialog.tsx +173 -0
  96. package/template/apps/asas-tanstack/src/components/ui/avatar.tsx +104 -0
  97. package/template/apps/asas-tanstack/src/components/ui/badge.tsx +48 -0
  98. package/template/apps/asas-tanstack/src/components/ui/breadcrumb.tsx +125 -0
  99. package/template/apps/asas-tanstack/src/components/ui/button-group.tsx +87 -0
  100. package/template/apps/asas-tanstack/src/components/ui/button.tsx +56 -0
  101. package/template/apps/asas-tanstack/src/components/ui/calendar.tsx +223 -0
  102. package/template/apps/asas-tanstack/src/components/ui/card.tsx +94 -0
  103. package/template/apps/asas-tanstack/src/components/ui/chart.tsx +369 -0
  104. package/template/apps/asas-tanstack/src/components/ui/checkbox.tsx +27 -0
  105. package/template/apps/asas-tanstack/src/components/ui/collapsible.tsx +19 -0
  106. package/template/apps/asas-tanstack/src/components/ui/combobox.tsx +300 -0
  107. package/template/apps/asas-tanstack/src/components/ui/dialog.tsx +149 -0
  108. package/template/apps/asas-tanstack/src/components/ui/direction.tsx +5 -0
  109. package/template/apps/asas-tanstack/src/components/ui/directional-icon.tsx +20 -0
  110. package/template/apps/asas-tanstack/src/components/ui/dropdown-menu.tsx +267 -0
  111. package/template/apps/asas-tanstack/src/components/ui/field.tsx +225 -0
  112. package/template/apps/asas-tanstack/src/components/ui/input-group.tsx +147 -0
  113. package/template/apps/asas-tanstack/src/components/ui/input-otp.tsx +20 -0
  114. package/template/apps/asas-tanstack/src/components/ui/input.tsx +20 -0
  115. package/template/apps/asas-tanstack/src/components/ui/label.tsx +18 -0
  116. package/template/apps/asas-tanstack/src/components/ui/popover.tsx +50 -0
  117. package/template/apps/asas-tanstack/src/components/ui/radio-group.tsx +10 -0
  118. package/template/apps/asas-tanstack/src/components/ui/resizable.tsx +47 -0
  119. package/template/apps/asas-tanstack/src/components/ui/scroll-area.tsx +52 -0
  120. package/template/apps/asas-tanstack/src/components/ui/select.tsx +190 -0
  121. package/template/apps/asas-tanstack/src/components/ui/separator.tsx +23 -0
  122. package/template/apps/asas-tanstack/src/components/ui/sheet.tsx +127 -0
  123. package/template/apps/asas-tanstack/src/components/ui/sidebar.tsx +716 -0
  124. package/template/apps/asas-tanstack/src/components/ui/skeleton.tsx +13 -0
  125. package/template/apps/asas-tanstack/src/components/ui/slider.tsx +13 -0
  126. package/template/apps/asas-tanstack/src/components/ui/spinner.tsx +11 -0
  127. package/template/apps/asas-tanstack/src/components/ui/switch.tsx +8 -0
  128. package/template/apps/asas-tanstack/src/components/ui/table.tsx +119 -0
  129. package/template/apps/asas-tanstack/src/components/ui/tabs.tsx +80 -0
  130. package/template/apps/asas-tanstack/src/components/ui/textarea.tsx +18 -0
  131. package/template/apps/asas-tanstack/src/components/ui/toggle-group.tsx +8 -0
  132. package/template/apps/asas-tanstack/src/components/ui/toggle.tsx +44 -0
  133. package/template/apps/asas-tanstack/src/components/ui/tooltip.tsx +68 -0
  134. package/template/apps/asas-tanstack/src/components.ts +35 -0
  135. package/template/apps/asas-tanstack/src/copy.ts +1207 -0
  136. package/template/apps/asas-tanstack/src/first-run.ts +16 -0
  137. package/template/apps/asas-tanstack/src/hooks/use-mobile.ts +19 -0
  138. package/template/apps/asas-tanstack/src/lib/utils.ts +6 -0
  139. package/template/apps/asas-tanstack/src/links.ts +38 -0
  140. package/template/apps/asas-tanstack/src/locale-switch.tsx +28 -0
  141. package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
  142. package/template/apps/asas-tanstack/src/quiet-abort.ts +25 -0
  143. package/template/apps/asas-tanstack/src/redirect.ts +39 -0
  144. package/template/apps/asas-tanstack/src/request-headers.ts +11 -0
  145. package/template/apps/asas-tanstack/src/routeTree.gen.ts +424 -0
  146. package/template/apps/asas-tanstack/src/router.tsx +18 -0
  147. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +11 -0
  148. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +4242 -0
  149. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +160 -0
  150. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.$.tsx +17 -0
  151. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.index.tsx +10 -0
  152. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard.tsx +13 -0
  153. package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +9 -0
  154. package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +1564 -0
  155. package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +94 -0
  156. package/template/apps/asas-tanstack/src/routes/(screens)/portal.$.tsx +17 -0
  157. package/template/apps/asas-tanstack/src/routes/(screens)/portal.index.tsx +10 -0
  158. package/template/apps/asas-tanstack/src/routes/(screens)/portal.tsx +13 -0
  159. package/template/apps/asas-tanstack/src/routes/__root.tsx +99 -0
  160. package/template/apps/asas-tanstack/src/routes/api/$.ts +11 -0
  161. package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +108 -0
  162. package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
  163. package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +94 -0
  164. package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +39 -0
  165. package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +16 -0
  166. package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
  167. package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +98 -0
  168. package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +143 -0
  169. package/template/apps/asas-tanstack/src/routes/index.tsx +146 -0
  170. package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +134 -0
  171. package/template/apps/asas-tanstack/src/screen-form.tsx +36 -0
  172. package/template/apps/asas-tanstack/src/screen-list.tsx +140 -0
  173. package/template/apps/asas-tanstack/src/screen-names.ts +10 -0
  174. package/template/apps/asas-tanstack/src/screens.tsx +888 -0
  175. package/template/apps/asas-tanstack/src/server.ts +26 -0
  176. package/template/apps/asas-tanstack/src/slide.ts +22 -0
  177. package/template/apps/asas-tanstack/src/start.ts +57 -0
  178. package/template/apps/asas-tanstack/src/styles.css +163 -0
  179. package/template/apps/asas-tanstack/src/theme.ts +5 -0
  180. package/template/apps/asas-tanstack/tsconfig.json +19 -0
  181. package/template/apps/asas-tanstack/vite.config.ts +34 -0
  182. package/template/package.json +16 -0
  183. package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
  184. package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
  185. package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
  186. package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
  187. package/template/templates/asas/.env.example +33 -0
  188. package/template/templates/asas/AGENTS.md +101 -0
  189. package/template/templates/asas/CLAUDE.md +1 -0
  190. package/template/templates/asas/README.md +1056 -0
  191. package/template/templates/asas/mithaq.hints.json +25 -0
  192. package/template/templates/asas/package.json +53 -0
  193. package/template/templates/asas/src/asas.ts +118 -0
  194. package/template/templates/asas/src/backoffice-http.ts +117 -0
  195. package/template/templates/asas/src/dev-setup.ts +112 -0
  196. package/template/templates/asas/src/local-database.ts +22 -0
  197. package/template/templates/asas/src/messages/account.json +132 -0
  198. package/template/templates/asas/src/messages/auditLog.json +304 -0
  199. package/template/templates/asas/src/messages/backofficeLog.json +132 -0
  200. package/template/templates/asas/src/messages/courses.json +312 -0
  201. package/template/templates/asas/src/messages/enrollments.json +60 -0
  202. package/template/templates/asas/src/messages/invitation.json +82 -0
  203. package/template/templates/asas/src/messages/localUsers.json +48 -0
  204. package/template/templates/asas/src/messages/media.json +76 -0
  205. package/template/templates/asas/src/messages/member.json +62 -0
  206. package/template/templates/asas/src/messages/memberRoles.json +48 -0
  207. package/template/templates/asas/src/messages/mithaq.json +214 -0
  208. package/template/templates/asas/src/messages/organization.json +72 -0
  209. package/template/templates/asas/src/messages/organizationRole.json +62 -0
  210. package/template/templates/asas/src/messages/roles.json +72 -0
  211. package/template/templates/asas/src/messages/session.json +102 -0
  212. package/template/templates/asas/src/messages/sessions.json +136 -0
  213. package/template/templates/asas/src/messages/settings.json +64 -0
  214. package/template/templates/asas/src/messages/tags.json +48 -0
  215. package/template/templates/asas/src/messages/user.json +112 -0
  216. package/template/templates/asas/src/messages/verification.json +62 -0
  217. package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/migration.sql +177 -0
  218. package/template/templates/asas/src/migrations/20260920080701_parched_tombstone/snapshot.json +2169 -0
  219. package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/migration.sql +1 -0
  220. package/template/templates/asas/src/migrations/20260920230505_zippy_ben_urich/snapshot.json +2182 -0
  221. package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/migration.sql +20 -0
  222. package/template/templates/asas/src/migrations/20260920231219_slim_retro_girl/snapshot.json +2409 -0
  223. package/template/templates/asas/src/migrations/20260922082945_military_ironclad/migration.sql +15 -0
  224. package/template/templates/asas/src/migrations/20260922082945_military_ironclad/snapshot.json +2627 -0
  225. package/template/templates/asas/src/migrations/20260922112630_bitter_chat/migration.sql +5 -0
  226. package/template/templates/asas/src/migrations/20260922112630_bitter_chat/snapshot.json +2691 -0
  227. package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/migration.sql +8 -0
  228. package/template/templates/asas/src/migrations/20260923235915_foamy_rachel_grey/snapshot.json +2795 -0
  229. package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/migration.sql +3 -0
  230. package/template/templates/asas/src/migrations/20260926145709_clean_the_initiative/snapshot.json +2834 -0
  231. package/template/templates/asas/src/migrations/20260927212525_bent_prowler/migration.sql +9 -0
  232. package/template/templates/asas/src/migrations/20260927212525_bent_prowler/snapshot.json +2870 -0
  233. package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/migration.sql +1 -0
  234. package/template/templates/asas/src/migrations/20260927223441_tranquil_bromley/snapshot.json +2881 -0
  235. package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/migration.sql +15 -0
  236. package/template/templates/asas/src/migrations/20260928030047_outgoing_wraith/snapshot.json +3073 -0
  237. package/template/templates/asas/src/migrations/20260929024407_grey_morlun/migration.sql +21 -0
  238. package/template/templates/asas/src/migrations/20260929024407_grey_morlun/snapshot.json +3098 -0
  239. package/template/templates/asas/src/migrations/20260929130530_bored_jackal/migration.sql +21 -0
  240. package/template/templates/asas/src/migrations/20260929130530_bored_jackal/snapshot.json +3124 -0
  241. package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/migration.sql +32 -0
  242. package/template/templates/asas/src/migrations/20260930102924_material_newton_destine/snapshot.json +3324 -0
  243. package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/migration.sql +2 -0
  244. package/template/templates/asas/src/migrations/20261003022353_clumsy_taskmaster/snapshot.json +3350 -0
  245. package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/migration.sql +21 -0
  246. package/template/templates/asas/src/migrations/20261003143249_premium_red_shift/snapshot.json +3635 -0
  247. package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/migration.sql +8 -0
  248. package/template/templates/asas/src/migrations/20261008215349_square_norrin_radd/snapshot.json +3635 -0
  249. package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/migration.sql +18 -0
  250. package/template/templates/asas/src/migrations/20261008230204_many_scarecrow/snapshot.json +3635 -0
  251. package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/migration.sql +18 -0
  252. package/template/templates/asas/src/migrations/20261010005521_elite_giant_girl/snapshot.json +3635 -0
  253. package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/migration.sql +11 -0
  254. package/template/templates/asas/src/migrations/20261010120822_needy_thunderbolt/snapshot.json +3667 -0
  255. package/template/templates/asas/src/migrations/20261012000752_organization_ownership/migration.sql +48 -0
  256. package/template/templates/asas/src/migrations/20261012000752_organization_ownership/snapshot.json +3891 -0
  257. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
  258. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
  259. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
  260. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
  261. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
  262. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
  263. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
  264. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
  265. package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
  266. package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
  267. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
  268. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
  269. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
  270. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
  271. package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
  272. package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
  273. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
  274. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
  275. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
  276. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
  277. package/template/templates/asas/src/migrations/drizzle.config.ts +11 -0
  278. package/template/templates/asas/src/migrations/schema.ts +30 -0
  279. package/template/templates/asas/src/mithaq.ts +4 -0
  280. package/template/templates/asas/src/organizations.ts +3 -0
  281. package/template/templates/asas/src/seed.ts +427 -0
  282. package/template/templates/asas/src/ui-backoffice/home.tsx +63 -0
  283. package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
  284. package/template/templates/asas/src/ui-backoffice/record-page.tsx +30 -0
  285. package/template/templates/asas/src/ui-backoffice/role-cells.tsx +59 -0
  286. package/template/templates/asas/src/ui-backoffice/user-actions.tsx +249 -0
  287. package/template/templates/asas/src/ui-dashboard/dashboard.tsx +300 -0
  288. package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
  289. package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
  290. package/template/templates/asas/src/ui-dashboard/members.tsx +829 -0
  291. package/template/templates/asas/src/ui-dashboard/theme-provider.tsx +10 -0
  292. package/template/templates/asas/src/ui-dashboard/versions.tsx +446 -0
  293. package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
  294. package/template/templates/asas/src/ui-portal/home.tsx +73 -0
  295. package/template/templates/asas/src/ui-portal/page.tsx +11 -0
  296. package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
  297. package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
  298. package/template/templates/features/audit-log/index.ts +4 -0
  299. package/template/templates/features/audit-log/package.json +13 -0
  300. package/template/templates/features/auth/index.ts +11 -0
  301. package/template/templates/features/auth/local-users/local-users.ts +143 -0
  302. package/template/templates/features/auth/package.json +13 -0
  303. package/template/templates/features/auth/user/account.access.ts +8 -0
  304. package/template/templates/features/auth/user/account.ts +31 -0
  305. package/template/templates/features/auth/user/index.ts +18 -0
  306. package/template/templates/features/auth/user/invitation.access.ts +4 -0
  307. package/template/templates/features/auth/user/invitation.ts +27 -0
  308. package/template/templates/features/auth/user/member.access.ts +10 -0
  309. package/template/templates/features/auth/user/member.screens.ts +37 -0
  310. package/template/templates/features/auth/user/member.ts +26 -0
  311. package/template/templates/features/auth/user/organization.access.ts +20 -0
  312. package/template/templates/features/auth/user/organization.screens.ts +37 -0
  313. package/template/templates/features/auth/user/organization.ts +25 -0
  314. package/template/templates/features/auth/user/organizationRole.access.ts +8 -0
  315. package/template/templates/features/auth/user/organizationRole.ts +25 -0
  316. package/template/templates/features/auth/user/session.access.ts +8 -0
  317. package/template/templates/features/auth/user/session.ts +28 -0
  318. package/template/templates/features/auth/user/user.access.ts +30 -0
  319. package/template/templates/features/auth/user/user.screens.ts +45 -0
  320. package/template/templates/features/auth/user/user.ts +31 -0
  321. package/template/templates/features/auth/user/verification.access.ts +8 -0
  322. package/template/templates/features/auth/user/verification.ts +23 -0
  323. package/template/templates/features/courses/courses/courses.ts +601 -0
  324. package/template/templates/features/courses/courses/cover-reads.ts +33 -0
  325. package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
  326. package/template/templates/features/courses/courses/ui/fee-cell.tsx +19 -0
  327. package/template/templates/features/courses/courses/ui/no-courses.tsx +13 -0
  328. package/template/templates/features/courses/courses/ui/status-cell.tsx +33 -0
  329. package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
  330. package/template/templates/features/courses/domain/archive.ts +39 -0
  331. package/template/templates/features/courses/domain/on-offer.ts +6 -0
  332. package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
  333. package/template/templates/features/courses/enrollments/ui/session-cell.tsx +75 -0
  334. package/template/templates/features/courses/enrollments/ui/session-labels.tsx +36 -0
  335. package/template/templates/features/courses/index.ts +8 -0
  336. package/template/templates/features/courses/package.json +24 -0
  337. package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
  338. package/template/templates/features/courses/portal/ui/course.tsx +176 -0
  339. package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
  340. package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
  341. package/template/templates/features/courses/portal/ui/reads.ts +76 -0
  342. package/template/templates/features/courses/portal/ui/rules.ts +87 -0
  343. package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
  344. package/template/templates/features/courses/sessions/seats.ts +42 -0
  345. package/template/templates/features/courses/sessions/sessions.ts +186 -0
  346. package/template/templates/features/media/index.ts +10 -0
  347. package/template/templates/features/media/media/media.ts +131 -0
  348. package/template/templates/features/media/media/ui/preview.tsx +143 -0
  349. package/template/templates/features/media/package.json +19 -0
  350. package/template/templates/features/roles/index.ts +4 -0
  351. package/template/templates/features/roles/member-roles/member-roles.ts +207 -0
  352. package/template/templates/features/roles/package.json +21 -0
  353. package/template/templates/features/roles/roles/roles.ts +184 -0
  354. package/template/templates/features/roles/roles/ui/role-form.tsx +267 -0
  355. package/template/templates/features/roles/roles/ui/roles.tsx +190 -0
  356. package/template/templates/features/settings/index.ts +3 -0
  357. package/template/templates/features/settings/package.json +13 -0
  358. package/template/templates/features/settings/settings/settings.ts +64 -0
  359. package/template/templates/features/tags/index.ts +3 -0
  360. package/template/templates/features/tags/package.json +14 -0
  361. package/template/templates/features/tags/tags/tags.ts +80 -0
  362. package/template/templates/mithaq/components.json +22 -0
  363. package/template/templates/mithaq/package.json +33 -0
  364. package/template/templates/mithaq/src/mail/index.ts +32 -0
  365. package/template/templates/mithaq/src/mail/mailers.ts +36 -0
  366. package/template/templates/mithaq/src/mithaq/audit-sink.ts +111 -0
  367. package/template/templates/mithaq/src/mithaq/auth/backoffice-roles.ts +108 -0
  368. package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
  369. package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
  370. package/template/templates/mithaq/src/mithaq/pages.ts +83 -0
  371. package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
  372. package/template/templates/mithaq/src/mithaq.ts +593 -0
  373. package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
  374. package/template/templates/mithaq/src/roles/grants.ts +47 -0
  375. package/template/templates/mithaq/src/roles/member-access.ts +11 -0
  376. package/template/templates/mithaq/src/roles/statements.ts +13 -0
  377. package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
  378. package/template/templates/mithaq/src/roles/tenant-access.ts +260 -0
  379. package/template/templates/mithaq/src/roles/tenant-roles.ts +321 -0
  380. package/template/templates/mithaq/src/steps/expected-updated-at.ts +41 -0
  381. package/template/templates/mithaq/src/storage/defaults.ts +156 -0
  382. package/template/templates/mithaq/src/storage/index.ts +97 -0
  383. package/template/templates/mithaq/src/storage/local.ts +143 -0
  384. package/template/templates/mithaq/src/storage/r2.ts +5 -0
  385. package/template/templates/mithaq/src/storage/s3.ts +91 -0
  386. package/template/templates/mithaq/src/storage/uploadthing.ts +59 -0
  387. package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
  388. package/template/templates/mithaq/src/ui/activity.tsx +527 -0
  389. package/template/templates/mithaq/src/ui/app-shell.tsx +268 -0
  390. package/template/templates/mithaq/src/ui/kpi-card.tsx +25 -0
  391. package/template/templates/mithaq/src/ui/record-page.tsx +50 -0
  392. package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
  393. package/template/templates/mithaq/src/ui/screen-nav.tsx +28 -0
  394. package/template/templates/mithaq/src/ui/table-page.tsx +9 -0
  395. package/template/templates/mithaq/src/ui/top-shell.tsx +60 -0
  396. package/README.md +0 -3
@@ -0,0 +1,98 @@
1
+ import { useState, type ComponentProps } from 'react'
2
+ import { Link, createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
3
+ import { organizations, signUp } from 'asas/organizations'
4
+ import { Button } from '@/components/ui/button'
5
+ import { authArm } from '../api.ts'
6
+ import { documentHead } from '../copy.ts'
7
+ import { AuthBox, AuthFrame, authGate, gate, notAnEmail, redirectSearch, useRefusal } from '../auth-frame.tsx'
8
+ import { firstRun } from '../first-run.ts'
9
+ import { useCopy } from './__root.tsx'
10
+ import { landing } from '../redirect.ts'
11
+
12
+ // The password has no length rule here because the provider's sign-in has none: a wrong
13
+ // password of any length is the one refusal it answers.
14
+
15
+ export const Route = createFileRoute('/auth/sign-in')({
16
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.signIn')),
17
+ // The router hands `validateSearch` whatever the URL carried; `landing` decides what is honoured.
18
+ validateSearch: redirectSearch,
19
+ // Nobody signs in to an app of one organization before its owner has set it up.
20
+ beforeLoad: async ({ search }: { search: { redirect?: string } }) => {
21
+ if ((await firstRun()).open) throw redirect({ href: '/auth/first-run' })
22
+ return gate({ search })
23
+ },
24
+ component: SignIn,
25
+ })
26
+
27
+ /** Whether a visitor with no invitation may make an account here, so the page offers one. */
28
+ const offersSignUp = organizations === 'many' || signUp === 'open'
29
+
30
+ function SignIn() {
31
+ const navigate = useNavigate()
32
+ const { t } = useCopy()
33
+ const search = Route.useSearch()
34
+ const refused = useRefusal()
35
+ const [email, setEmail] = useState('')
36
+ const [password, setPassword] = useState('')
37
+ const [pending, setPending] = useState(false)
38
+ // The answer, not its sentence, so a language switch says it again in the language chosen.
39
+ const [refusal, setRefusal] = useState<Parameters<typeof refused>[0] | null>(null)
40
+ const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
41
+ const required = t('auth.required', 'This field is required.')
42
+ const wrong = {
43
+ email: () => (!email ? required : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined),
44
+ password: () => (password ? undefined : required),
45
+ }
46
+ const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]() : undefined)
47
+ const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'>) => (
48
+ <AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
49
+ )
50
+
51
+ const submit = async (event: React.FormEvent) => {
52
+ event.preventDefault()
53
+ setLeft(new Set(Object.keys(wrong)))
54
+ if (Object.values(wrong).some((check) => check())) return
55
+ setPending(true)
56
+ setRefusal(null)
57
+ const answered = await authArm('sign-in/email', { email, password })
58
+ // The project's own sentence for the provider's code, never its English `message`, since copy carries every locale.
59
+ if (!answered.ok) {
60
+ setPending(false)
61
+ return setRefusal(answered)
62
+ }
63
+
64
+ setPending(false)
65
+
66
+ // The first screen this actor enters is read only once the session exists.
67
+ await navigate({ href: landing({ asked: search.redirect, screens: (await authGate()).screens }) })
68
+ }
69
+
70
+ return (
71
+ <AuthFrame title={t('auth.signIn', 'Sign in')} description={t('auth.signInDescription', 'Welcome back. Sign in with your email and password.')}>
72
+ <form className="grid gap-4" noValidate onSubmit={submit}>
73
+ {box('email', t('auth.email', 'Email'), { autoComplete: 'email', onChange: (event) => setEmail(event.target.value), type: 'email', value: email })}
74
+ {box('password', t('auth.password', 'Password'), { autoComplete: 'current-password', onChange: (event) => setPassword(event.target.value), type: 'password', value: password })}
75
+ <Link className="-mt-2 justify-self-end text-sm text-muted-foreground underline-offset-4 hover:underline" to="/auth/forgot-password" search={{ redirect: search.redirect }}>
76
+ {t('auth.forgotPasswordQuestion', 'Forgot password?')}
77
+ </Link>
78
+ {/* `role="alert"` so a refusal is spoken when it appears: nothing moves the focus to it. */}
79
+ {refusal && (
80
+ <p className="text-destructive text-sm" role="alert">
81
+ {refused(refusal, t('auth.failed', 'That sign-in did not work.'))}
82
+ </p>
83
+ )}
84
+ <Button disabled={pending} type="submit">
85
+ {pending ? t('auth.signInPending', 'Signing in…') : t('auth.signIn', 'Sign in')}
86
+ </Button>
87
+ </form>
88
+ {offersSignUp ? (
89
+ <p className="mt-4 text-center text-sm text-muted-foreground">
90
+ {t('auth.noAccount', 'No account yet?')}{' '}
91
+ <Link className="text-primary underline underline-offset-4" to="/auth/sign-up" search={{ redirect: search.redirect }}>
92
+ {t('auth.signUp', 'Sign up')}
93
+ </Link>
94
+ </p>
95
+ ) : null}
96
+ </AuthFrame>
97
+ )
98
+ }
@@ -0,0 +1,143 @@
1
+ import { useState, type ComponentProps } from 'react'
2
+ import { Link, createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
3
+ import { organizations, signUp, verifyEmail } from 'asas/organizations'
4
+ import { Button } from '@/components/ui/button'
5
+ import { authArm } from '../api.ts'
6
+ import { documentHead } from '../copy.ts'
7
+ import { AuthBox, AuthFrame, authGate, gate, notAnEmail, redirectSearch, useRefusal } from '../auth-frame.tsx'
8
+ import { firstRun } from '../first-run.ts'
9
+ import { landing, wanted } from '../redirect.ts'
10
+ import { useCopy } from './__root.tsx'
11
+
12
+ export const Route = createFileRoute('/auth/sign-up')({
13
+ head: ({ match }) => documentHead(match.context, (t) => t('auth.signUp')),
14
+ validateSearch: redirectSearch,
15
+ // An app of one organization that has none yet takes no account before its owner's.
16
+ beforeLoad: async ({ search }: { search: { redirect?: string } }) => {
17
+ if ((await firstRun()).open) throw redirect({ href: '/auth/first-run' })
18
+ return gate({ search })
19
+ },
20
+ component: SignUp,
21
+ })
22
+
23
+ /**
24
+ * Whether a new account proves its address before it signs in, so the sign-up answers with no session: with many
25
+ * organizations always, where the person then founds or joins one, and with one where an open sign-up says so. Such a
26
+ * sign-up asks no password: it is chosen once, on the page the mailed link opens, and the server keeps none typed here.
27
+ */
28
+ const verifies = signUp === 'open' && verifyEmail
29
+
30
+ function SignUp() {
31
+ const navigate = useNavigate()
32
+ const { t } = useCopy()
33
+ const search = Route.useSearch()
34
+ const refused = useRefusal()
35
+ const { password: bounds } = Route.useRouteContext()
36
+ // The redirect is a link anyone can write; a crafted one only makes an account with no organization,
37
+ // which reads nothing until an organization invites it.
38
+ const asked = wanted({ asked: search.redirect, home: '/' })
39
+ const invited = asked.startsWith('/auth/accept-invitation?')
40
+ // The invitation's id, which a sign-up by invitation carries: holding it is what lets it through.
41
+ const invitationId = invited ? new URLSearchParams(asked.slice(asked.indexOf('?'))).get('id') ?? undefined : undefined
42
+ const [name, setName] = useState('')
43
+ const [email, setEmail] = useState('')
44
+ const [password, setPassword] = useState('')
45
+ const [confirmation, setConfirmation] = useState('')
46
+ const [pending, setPending] = useState(false)
47
+ const [refusal, setRefusal] = useState<string | null>(null)
48
+ const [left, setLeft] = useState<ReadonlySet<string>>(() => new Set())
49
+ const [sent, setSent] = useState<string | null>(null)
50
+ const required = t('auth.required', 'This field is required.')
51
+ const named = {
52
+ name: () => (name ? undefined : required),
53
+ email: () => (!email ? required : notAnEmail(email) ? t('auth.invalidEmail', 'Invalid email') : undefined),
54
+ }
55
+ const chosen = {
56
+ password: () =>
57
+ !password ? required
58
+ : password.length < bounds.min ? t('auth.passwordTooShort', 'Password too short')
59
+ : password.length > bounds.max ? t('auth.passwordTooLong', 'Password too long')
60
+ : undefined,
61
+ confirmation: () => (!confirmation ? required : confirmation === password ? undefined : t('auth.passwordsDiffer', 'The two passwords are not the same.')),
62
+ }
63
+ const wrong: Partial<Record<'name' | 'email' | 'password' | 'confirmation', () => string | undefined>> = verifies ? named : { ...named, ...chosen }
64
+ const errorOf = (box: keyof typeof wrong) => (left.has(box) ? wrong[box]?.() : undefined)
65
+ const box = (id: keyof typeof wrong, label: string | undefined, props: ComponentProps<'input'> & { description?: string }) => (
66
+ <AuthBox id={id} label={label} error={errorOf(id)} onLeave={() => setLeft((already) => new Set(already).add(id))} {...props} />
67
+ )
68
+
69
+ const submit = async (event: React.FormEvent) => {
70
+ event.preventDefault()
71
+ setLeft(new Set(Object.keys(wrong)))
72
+ if (Object.values(wrong).some((check) => check())) return
73
+ setPending(true)
74
+ setRefusal(null)
75
+ const made = await authArm('sign-up/email', { name, email, ...(!verifies && { password }), ...(invitationId && { invitationId }) })
76
+ setPending(false)
77
+ if (!made.ok) return setRefusal(refused(made, t('auth.signUpFailed', 'That sign-up did not work.')))
78
+ // An account that proves its address first has no session yet, invited or not, so it is told to open its mail.
79
+ if (verifies) return setSent(email)
80
+ // The server accepted the invitation the sign-up carried, so its page would only say it is spent.
81
+ await navigate({ href: landing({ asked: invited ? undefined : search.redirect, screens: (await authGate()).screens }) })
82
+ }
83
+
84
+ const toSignIn = (
85
+ <p className="mt-4 text-center text-sm text-muted-foreground">
86
+ {t('auth.haveAccount', 'Already have an account?')}{' '}
87
+ <Link className="text-primary underline underline-offset-4" to="/auth/sign-in" search={{ redirect: search.redirect }}>
88
+ {t('auth.signIn', 'Sign in')}
89
+ </Link>
90
+ </p>
91
+ )
92
+ // By invitation, nobody without one is offered a form the server would refuse.
93
+ if (organizations === 'one' && signUp === 'invite' && !invitationId) {
94
+ return (
95
+ <AuthFrame title={t('auth.signUp', 'Sign up')} description={t('auth.inviteOnly', 'Accounts here are made by invitation. Open the link of the invitation sent to you, or sign in.')}>
96
+ {toSignIn}
97
+ </AuthFrame>
98
+ )
99
+ }
100
+ if (sent) {
101
+ return (
102
+ <AuthFrame
103
+ title={t('auth.verifySent', 'Check your email')}
104
+ description={<span className="[overflow-wrap:anywhere]">{t('auth.verifySentDescription', 'We sent a link to {email}. Open it to confirm your address and choose your password, then sign in. It lasts an hour.')!.split(/(\{email\})/).map((part, at) => (part === '{email}' ? <bdi key={at}>{sent}</bdi> : part))}</span>}
105
+ >
106
+ {toSignIn}
107
+ </AuthFrame>
108
+ )
109
+ }
110
+ return (
111
+ <AuthFrame
112
+ title={t('auth.signUp', 'Sign up')}
113
+ description={invited ? t('auth.signUpInvitedDescription', 'Make an account with the email address the invitation was sent to.') : t('auth.signUpDescription', 'Make an account.')}
114
+ >
115
+ <form className="grid gap-4" noValidate onSubmit={submit}>
116
+ {box('name', t('auth.name', 'Name'), { autoComplete: 'name', onChange: (event) => setName(event.target.value), type: 'text', value: name })}
117
+ {box('email', t('auth.email', 'Email'), { autoComplete: 'email', onChange: (event) => setEmail(event.target.value), type: 'email', value: email })}
118
+ {verifies ? null : box('password', t('auth.password', 'Password'), {
119
+ autoComplete: 'new-password',
120
+ description: t('auth.passwordRule', 'At least {count} characters.')!.replace('{count}', String(bounds.min)),
121
+ onChange: (event) => setPassword(event.target.value),
122
+ type: 'password',
123
+ value: password,
124
+ })}
125
+ {verifies ? null : box('confirmation', t('auth.confirmPassword', 'Confirm the password'), {
126
+ autoComplete: 'new-password',
127
+ onChange: (event) => setConfirmation(event.target.value),
128
+ type: 'password',
129
+ value: confirmation,
130
+ })}
131
+ {refusal && (
132
+ <p className="text-destructive text-sm" role="alert">
133
+ {refusal}
134
+ </p>
135
+ )}
136
+ <Button disabled={pending} type="submit">
137
+ {pending ? t('auth.signUpPending', 'Signing up…') : t('auth.signUp', 'Sign up')}
138
+ </Button>
139
+ </form>
140
+ {toSignIn}
141
+ </AuthFrame>
142
+ )
143
+ }
@@ -0,0 +1,146 @@
1
+ import { Link, createFileRoute } from '@tanstack/react-router'
2
+ import { organizations, signUp } from 'asas/organizations'
3
+ import { LayoutDashboard, Languages, ShieldCheck } from 'lucide-react'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
6
+ import { useHomeLabel } from '../auth-frame.tsx'
7
+ import { LocaleSwitch } from '../locale-switch.tsx'
8
+ import { loadEnterable } from '../screens.tsx'
9
+ import { useCopy } from './__root.tsx'
10
+
11
+ // A host file because it reads `loadEnterable`, which only a host file may.
12
+ export const Route = createFileRoute('/')({
13
+ loader: () => loadEnterable(),
14
+ component: Landing,
15
+ })
16
+
17
+ /** Whether a visitor with no invitation may make an account, so the page offers one, as sign-in does. */
18
+ const offersSignUp = organizations === 'many' || signUp === 'open'
19
+
20
+ function EntryActions({ screens, size }: { screens: readonly { name: string; path: string }[] | null; size: 'sm' | 'lg' }) {
21
+ const { t } = useCopy()
22
+ const label = useHomeLabel()
23
+ if (screens === null) {
24
+ return (
25
+ <>
26
+ {offersSignUp ? (
27
+ <Link to="/auth/sign-up">
28
+ <Button size={size}>{t('marketing.getStarted', 'Get started')}</Button>
29
+ </Link>
30
+ ) : null}
31
+ <Link to="/auth/sign-in">
32
+ <Button size={size} variant={offersSignUp ? 'outline' : 'default'}>
33
+ {t('auth.signIn', 'Sign in')}
34
+ </Button>
35
+ </Link>
36
+ </>
37
+ )
38
+ }
39
+ // A plain anchor: the path is the screen's own data, not a route the router knows.
40
+ const home: { path: string; name?: string } = screens[0] ?? { path: '/unauthorized' }
41
+ return (
42
+ <a href={home.path}>
43
+ <Button size={size}>{label(home, 'marketing.openScreen')}</Button>
44
+ </a>
45
+ )
46
+ }
47
+
48
+ function Feature({ icon: Icon, title, description }: { icon: typeof ShieldCheck; title: string; description: string }) {
49
+ return (
50
+ <Card>
51
+ <CardHeader>
52
+ <Icon className="mb-2 h-6 w-6 text-primary" />
53
+ <CardTitle className="text-base font-semibold">{title}</CardTitle>
54
+ </CardHeader>
55
+ <CardContent className="text-sm text-muted-foreground">{description}</CardContent>
56
+ </Card>
57
+ )
58
+ }
59
+
60
+ function Step({ number, title, description }: { number: number; title: string; description: string }) {
61
+ return (
62
+ <div className="flex flex-col gap-2">
63
+ <div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-semibold">
64
+ {number}
65
+ </div>
66
+ <h3 className="font-semibold">{title}</h3>
67
+ <p className="text-sm text-muted-foreground">{description}</p>
68
+ </div>
69
+ )
70
+ }
71
+
72
+ function Landing() {
73
+ const { t, locale } = useCopy()
74
+ const screens = Route.useLoaderData()?.screens ?? null
75
+ // `t` answers `string | undefined` for a key no locale carries; the fallback only satisfies that type.
76
+ const tt = (key: string, fallback: string) => t(key, fallback) ?? fallback
77
+ const features = [
78
+ { icon: ShieldCheck, title: tt('marketing.featureRolesTitle', 'Roles and permissions'), description: tt('marketing.featureRolesDescription', '') },
79
+ { icon: Languages, title: tt('marketing.featureBilingualTitle', 'Arabic and English, either direction'), description: tt('marketing.featureBilingualDescription', '') },
80
+ { icon: LayoutDashboard, title: tt('marketing.featurePortalTitle', 'A dashboard and a customer portal'), description: tt('marketing.featurePortalDescription', '') },
81
+ ]
82
+ const steps = [
83
+ { title: tt('marketing.howStep1Title', 'Set up your organization'), description: tt('marketing.howStep1Description', '') },
84
+ { title: tt('marketing.howStep2Title', 'Add your records and roles'), description: tt('marketing.howStep2Description', '') },
85
+ { title: tt('marketing.howStep3Title', 'Open the portal to your customers'), description: tt('marketing.howStep3Description', '') },
86
+ ]
87
+ return (
88
+ <div className="flex min-h-screen flex-col">
89
+ <header className="sticky top-0 z-50 border-b bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60">
90
+ <div className="mx-auto flex h-16 max-w-6xl items-center justify-between gap-2 px-4 sm:gap-4 sm:px-6">
91
+ <span className="text-lg font-semibold">{t('marketing.brand', 'Asas')}</span>
92
+ <div className="flex items-center gap-1 sm:gap-2">
93
+ <LocaleSwitch locale={locale} />
94
+ <EntryActions screens={screens} size="sm" />
95
+ </div>
96
+ </div>
97
+ </header>
98
+
99
+ <main className="flex-1">
100
+ <section className="mx-auto flex max-w-4xl flex-col items-center gap-6 px-4 py-20 text-center sm:px-6 sm:py-28">
101
+ <h1 className="text-4xl font-bold tracking-tight sm:text-5xl">{t('marketing.heroTitle', 'Run your organization without the busywork')}</h1>
102
+ <p className="max-w-2xl text-lg text-muted-foreground">{t('marketing.heroSubtitle', '')}</p>
103
+ <div className="flex flex-col gap-3 sm:flex-row">
104
+ <EntryActions screens={screens} size="lg" />
105
+ </div>
106
+ </section>
107
+
108
+ <section className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
109
+ <div className="mx-auto mb-12 max-w-2xl text-center">
110
+ <h2 className="text-3xl font-bold tracking-tight">{t('marketing.featuresTitle', 'Everything an organization needs')}</h2>
111
+ <p className="mt-3 text-muted-foreground">{t('marketing.featuresSubtitle', '')}</p>
112
+ </div>
113
+ <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
114
+ {features.map((feature) => (
115
+ <Feature key={feature.title} {...feature} />
116
+ ))}
117
+ </div>
118
+ </section>
119
+
120
+ <section className="border-t bg-muted/30">
121
+ <div className="mx-auto max-w-6xl px-4 py-16 sm:px-6">
122
+ <h2 className="mb-10 text-center text-3xl font-bold tracking-tight">{t('marketing.howTitle', 'How it works')}</h2>
123
+ <div className="grid gap-8 sm:grid-cols-3">
124
+ {steps.map((step, at) => (
125
+ <Step key={step.title} number={at + 1} {...step} />
126
+ ))}
127
+ </div>
128
+ </div>
129
+ </section>
130
+
131
+ <section className="mx-auto flex max-w-3xl flex-col items-center gap-6 px-4 py-20 text-center sm:px-6">
132
+ <h2 className="text-3xl font-bold tracking-tight">{t('marketing.ctaTitle', 'Start running your organization on Asas')}</h2>
133
+ <p className="text-muted-foreground">{t('marketing.ctaSubtitle', '')}</p>
134
+ <EntryActions screens={screens} size="lg" />
135
+ </section>
136
+ </main>
137
+
138
+ <footer className="border-t py-8">
139
+ <div className="mx-auto max-w-6xl px-4 text-center text-sm text-muted-foreground sm:px-6">
140
+ {t('marketing.footer', 'Asas — one place for an organization\'s records.')}
141
+ </div>
142
+ </footer>
143
+ </div>
144
+ )
145
+ }
146
+ // mithaq-admin 0.0.0 4ea8d722cd59ec1a91778142281c6f7db6ab9abe005dfc5f8c0192ee4e1095aa
@@ -0,0 +1,134 @@
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 { NoOrganization } from '../no-organization.tsx'
8
+ import { ImpersonationBar, loadEnterable } from '../screens.tsx'
9
+ import { useCopy } from './__root.tsx'
10
+
11
+ /**
12
+ * Where someone sent here from a screen their role does not open goes without being asked: the one screen it does
13
+ * open. A page refused inside that screen is said instead, and so is an impersonation, whose bar is the way out.
14
+ */
15
+ export const onwardOf = ({ screens, refused, impersonating }: { screens: readonly { path: string }[]; refused?: unknown; impersonating?: unknown }) =>
16
+ !refused && !impersonating && screens.length === 1 ? screens[0]!.path : undefined
17
+
18
+ export const Route = createFileRoute('/unauthorized')({
19
+ // The path a screen refused; the server only matches it against the build's pages.
20
+ validateSearch: (search: Record<string, unknown>): { from?: string } => (typeof search.from === 'string' ? { from: search.from } : {}),
21
+ loaderDeps: ({ search }) => ({ from: search.from }),
22
+ loader: async ({ deps, context }) => {
23
+ const entered = await loadEnterable({ data: { from: deps.from, locale: context.locale } })
24
+ // Carries this page's own path so a visitor whose session expired lands back here after signing in.
25
+ if (!entered) throw redirect({ to: '/auth/sign-in', search: { redirect: '/unauthorized' } })
26
+ const onward = onwardOf(entered)
27
+ if (onward) throw redirect({ href: onward })
28
+ return entered
29
+ },
30
+ // After the loader, so its data's type is inferred before this reads it.
31
+ head: ({ match, loaderData }) => documentHead(match.context, (t) => (loaderData?.screens.length === 0 ? t('auth.noAccess') : t('auth.unauthorized'))),
32
+ component: Unauthorized,
33
+ })
34
+
35
+ function Unauthorized() {
36
+ const { t } = useCopy()
37
+ const { screens, refused, email, impersonating, organizations = [], others = [] } = Route.useLoaderData()
38
+ const refusal = useRefusal()
39
+ const [switching, setSwitching] = useState<string>()
40
+ const [failed, setFailed] = useState<string>()
41
+ // The slug sits beside the name where another membership shares the name.
42
+ const memberships = [...organizations, ...others]
43
+ const named = (one: { id: string; name: string; slug?: string }): ReactNode =>
44
+ one.slug && memberships.some((other) => other.id !== one.id && other.name === one.name) ? (
45
+ <>
46
+ {one.name} <span dir="ltr" data-slug="" className="text-muted-foreground text-xs font-normal">{one.slug}</span>
47
+ </>
48
+ ) : (
49
+ one.name
50
+ )
51
+ // Each name is isolated so an email or Latin name in an Arabic sentence keeps
52
+ // its order, and is one inline box that wraps whole, so a long address never
53
+ // runs past the card on a phone.
54
+ const whole = 'inline-block max-w-full [overflow-wrap:anywhere]'
55
+ const said = (text: string | undefined) =>
56
+ text!.split(/(\{email\}|\{organization\}|\{page\})/).map((part, at) =>
57
+ part === '{email}' ? <bdi key={at} className={whole}>{email}</bdi>
58
+ : part === '{organization}' ? <bdi key={at}>{organizations.map((one, index) => <Fragment key={one.id}>{index ? ', ' : ''}<span className={whole}>{named(one)}</span></Fragment>)}</bdi>
59
+ : part === '{page}' ? <bdi key={at}>{refused?.page}</bdi>
60
+ : part)
61
+ const link = 'text-foreground underline underline-offset-4'
62
+ // Someone who belongs to no organization is shown the ways into one, which the app draws.
63
+ const unplaced = !refused && !screens.length && !organizations.length && !others.length && !impersonating
64
+ const screenName = (name: string) => t(`screens.${name}`, name) ?? name
65
+ return (
66
+ <AuthFrame
67
+ title={screens.length ? t('auth.unauthorized', 'Choose where to go') : t('auth.noAccess', 'Your account has no access yet')}
68
+ description={
69
+ <span data-no-access={screens.length ? undefined : ''}>
70
+ {refused
71
+ ? said(t('auth.unauthorizedPage', 'Your role does not open that page: {page}.'))
72
+ : screens.length
73
+ ? t('auth.unauthorizedDescription', 'This page isn\'t part of your account. Where would you like to go?')
74
+ : organizations.length
75
+ ? 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.'))
76
+ : said(t('auth.noOrganization', '{email} belongs to no organization yet.'))}
77
+ </span>
78
+ }
79
+ >
80
+ <div className="grid gap-4">
81
+ {/* Stopping impersonation is the way out in place of Sign out. */}
82
+ {impersonating ? <ImpersonationBar name={impersonating} /> : null}
83
+ {refused ? (
84
+ <a className={link} href={refused.screen.path}>
85
+ {(t('auth.backTo', 'Back to {screen}') ?? 'Back to {screen}').replace('{screen}', () => screenName(refused.screen.name))}
86
+ </a>
87
+ ) : screens.length ? (
88
+ <div className="grid gap-2">
89
+ {screens.map((screen) => (
90
+ // An anchor: the path is the screen's own data, not a route the router knows.
91
+ <Button key={screen.name} nativeButton={false} className="max-sm:h-11" render={<a href={screen.path} />}>
92
+ {screenName(screen.name)}
93
+ </Button>
94
+ ))}
95
+ </div>
96
+ ) : unplaced ? (
97
+ <NoOrganization email={email} />
98
+ ) : null}
99
+ {/* So a switch into an organization where the actor holds no role is not a dead end. */}
100
+ {others.map((one) => (
101
+ <Button
102
+ key={one.id}
103
+ type="button"
104
+ variant="secondary"
105
+ data-switch-organization={one.id}
106
+ // Busy while it runs, so a second press sends nothing.
107
+ disabled={switching !== undefined}
108
+ aria-busy={switching === one.id || undefined}
109
+ onClick={() => {
110
+ setSwitching(one.id)
111
+ setFailed(undefined)
112
+ void authArm('organization/set-active', { organizationId: one.id }).then((answer) => {
113
+ if (answer.ok) return window.location.assign('/auth/sign-in')
114
+ setSwitching(undefined)
115
+ setFailed(refusal(answer, t('auth.requestFailed', 'That request did not work.')))
116
+ })
117
+ }}
118
+ >
119
+ {/* One box, so the button's gap does not open beside the name. */}
120
+ <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>
121
+ </Button>
122
+ ))}
123
+ {failed ? <p role="alert" className="text-destructive text-sm">{failed}</p> : null}
124
+ {/* `replace`, so Back does not return to the page signed out of. */}
125
+ {impersonating ? null : (
126
+ <Button type="button" variant="outline" className="max-sm:h-11" onClick={() => void authArm('sign-out').then(() => window.location.replace('/auth/sign-in'))}>
127
+ {t('admin.signOut', 'Sign out')}
128
+ </Button>
129
+ )}
130
+ </div>
131
+ </AuthFrame>
132
+ )
133
+ }
134
+ // mithaq-admin 0.0.0 81d25978cdacbb38254854b0f64b738946ee27baca06a952aed51d35e6fb10af
@@ -0,0 +1,36 @@
1
+ import { useBlocker, useRouter } from '@tanstack/react-router'
2
+ import { useState, type ReactElement } from 'react'
3
+ import { Form } from 'mithaq-admin/form.tsx'
4
+ import type { FormProps } from 'mithaq-admin/form-types.tsx'
5
+
6
+ /**
7
+ * A kayan's form on a screen's page. A create moves to where `edit` sends it, the new row's edit page or, for a page
8
+ * opened over its list, the list; a delete moves to the list.
9
+ * While the form holds a change the router blocks navigation and the form asks its discard question.
10
+ */
11
+ export function ScreenForm({ edit, list, ...props }: Omit<FormProps, 'onDone' | 'onDirty' | 'onCancel' | 'blocked'> & {
12
+ /** The href of a row's edit page, or nothing where the kayan has none. */
13
+ edit: (id: string) => string | undefined
14
+ /** The href of the kayan's list, or nothing where it has none. */
15
+ list?: string
16
+ }): ReactElement {
17
+ const router = useRouter()
18
+ const [dirty, setDirty] = useState(false)
19
+ const blocker = useBlocker({ shouldBlockFn: () => dirty, withResolver: true, enableBeforeUnload: false })
20
+ // `ignoreBlocker`: the form reports clean only on the render after the save.
21
+ const go = (href: string | undefined) => void (href ? router.navigate({ href, ignoreBlocker: true }) : router.invalidate())
22
+ const landing = (row: unknown, effect?: string) => {
23
+ const id = (row as { id?: unknown } | null | undefined)?.id
24
+ return effect === 'delete' ? list : effect === 'create' && id !== undefined ? edit(String(id)) : undefined
25
+ }
26
+ return (
27
+ <Form
28
+ {...props}
29
+ onDirty={setDirty}
30
+ onDone={(row, effect) => go(landing(row, effect))}
31
+ onCancel={list ? () => go(list) : undefined}
32
+ blocked={blocker.status === 'blocked' ? blocker : undefined}
33
+ />
34
+ )
35
+ }
36
+ // mithaq-admin 0.0.0 508fa95987312292d3a5737dd1fd6aba03fbd5a7dbbbf5bb3bea2812b2adfde4