@striae-org/striae 3.0.4

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 (223) hide show
  1. package/.env.example +100 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +18 -0
  4. package/README.md +133 -0
  5. package/app/components/actions/case-export/core-export.ts +328 -0
  6. package/app/components/actions/case-export/data-processing.ts +167 -0
  7. package/app/components/actions/case-export/download-handlers.ts +900 -0
  8. package/app/components/actions/case-export/index.ts +41 -0
  9. package/app/components/actions/case-export/metadata-helpers.ts +107 -0
  10. package/app/components/actions/case-export/types-constants.ts +56 -0
  11. package/app/components/actions/case-export/validation-utils.ts +25 -0
  12. package/app/components/actions/case-export.ts +4 -0
  13. package/app/components/actions/case-import/annotation-import.ts +35 -0
  14. package/app/components/actions/case-import/confirmation-import.ts +363 -0
  15. package/app/components/actions/case-import/image-operations.ts +61 -0
  16. package/app/components/actions/case-import/index.ts +39 -0
  17. package/app/components/actions/case-import/orchestrator.ts +420 -0
  18. package/app/components/actions/case-import/storage-operations.ts +270 -0
  19. package/app/components/actions/case-import/validation.ts +189 -0
  20. package/app/components/actions/case-import/zip-processing.ts +413 -0
  21. package/app/components/actions/case-manage.ts +524 -0
  22. package/app/components/actions/case-review.ts +4 -0
  23. package/app/components/actions/confirm-export.ts +351 -0
  24. package/app/components/actions/generate-pdf.ts +210 -0
  25. package/app/components/actions/image-manage.ts +385 -0
  26. package/app/components/actions/notes-manage.ts +33 -0
  27. package/app/components/actions/signout.module.css +15 -0
  28. package/app/components/actions/signout.tsx +50 -0
  29. package/app/components/audit/user-audit-viewer.tsx +975 -0
  30. package/app/components/audit/user-audit.module.css +568 -0
  31. package/app/components/auth/auth-provider.tsx +78 -0
  32. package/app/components/auth/mfa-enrollment.module.css +268 -0
  33. package/app/components/auth/mfa-enrollment.tsx +398 -0
  34. package/app/components/auth/mfa-verification.module.css +251 -0
  35. package/app/components/auth/mfa-verification.tsx +295 -0
  36. package/app/components/button/button.module.css +63 -0
  37. package/app/components/button/button.tsx +46 -0
  38. package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
  39. package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
  40. package/app/components/canvas/canvas.module.css +314 -0
  41. package/app/components/canvas/canvas.tsx +449 -0
  42. package/app/components/canvas/confirmation/confirmation.module.css +187 -0
  43. package/app/components/canvas/confirmation/confirmation.tsx +214 -0
  44. package/app/components/colors/colors.module.css +59 -0
  45. package/app/components/colors/colors.tsx +68 -0
  46. package/app/components/form/base-form.tsx +21 -0
  47. package/app/components/form/form-button.tsx +28 -0
  48. package/app/components/form/form-field.tsx +53 -0
  49. package/app/components/form/form-message.tsx +17 -0
  50. package/app/components/form/form-toggle.tsx +23 -0
  51. package/app/components/form/form.module.css +427 -0
  52. package/app/components/form/index.ts +6 -0
  53. package/app/components/icon/icon.module.css +3 -0
  54. package/app/components/icon/icon.tsx +27 -0
  55. package/app/components/icon/icons.svg +102 -0
  56. package/app/components/icon/manifest.json +110 -0
  57. package/app/components/sidebar/case-export/case-export.module.css +386 -0
  58. package/app/components/sidebar/case-export/case-export.tsx +317 -0
  59. package/app/components/sidebar/case-import/case-import.module.css +626 -0
  60. package/app/components/sidebar/case-import/case-import.tsx +404 -0
  61. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
  62. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
  63. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
  64. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
  65. package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
  66. package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
  67. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
  68. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
  69. package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
  70. package/app/components/sidebar/case-import/index.ts +18 -0
  71. package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
  72. package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
  73. package/app/components/sidebar/cases/cases-modal.module.css +166 -0
  74. package/app/components/sidebar/cases/cases-modal.tsx +201 -0
  75. package/app/components/sidebar/cases/cases.module.css +713 -0
  76. package/app/components/sidebar/files/files-modal.module.css +209 -0
  77. package/app/components/sidebar/files/files-modal.tsx +239 -0
  78. package/app/components/sidebar/hash/hash-utility.module.css +366 -0
  79. package/app/components/sidebar/hash/hash-utility.tsx +982 -0
  80. package/app/components/sidebar/notes/notes-modal.tsx +51 -0
  81. package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
  82. package/app/components/sidebar/notes/notes.module.css +360 -0
  83. package/app/components/sidebar/sidebar-container.tsx +149 -0
  84. package/app/components/sidebar/sidebar.module.css +321 -0
  85. package/app/components/sidebar/sidebar.tsx +215 -0
  86. package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
  87. package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
  88. package/app/components/theme-provider/theme-provider.tsx +131 -0
  89. package/app/components/theme-provider/theme.ts +155 -0
  90. package/app/components/toast/toast.module.css +137 -0
  91. package/app/components/toast/toast.tsx +56 -0
  92. package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
  93. package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
  94. package/app/components/toolbar/toolbar.module.css +42 -0
  95. package/app/components/toolbar/toolbar.tsx +167 -0
  96. package/app/components/user/delete-account.module.css +274 -0
  97. package/app/components/user/delete-account.tsx +471 -0
  98. package/app/components/user/inactivity-warning.module.css +145 -0
  99. package/app/components/user/inactivity-warning.tsx +84 -0
  100. package/app/components/user/manage-profile.module.css +190 -0
  101. package/app/components/user/manage-profile.tsx +253 -0
  102. package/app/components/user/mfa-phone-update.tsx +739 -0
  103. package/app/config-example/admin-service.json +13 -0
  104. package/app/config-example/config.json +17 -0
  105. package/app/config-example/firebase.ts +21 -0
  106. package/app/config-example/inactivity.ts +13 -0
  107. package/app/config-example/meta-config.json +6 -0
  108. package/app/contexts/auth.context.ts +12 -0
  109. package/app/entry.client.tsx +12 -0
  110. package/app/entry.server.tsx +44 -0
  111. package/app/hooks/useInactivityTimeout.ts +110 -0
  112. package/app/root.tsx +170 -0
  113. package/app/routes/_index.tsx +16 -0
  114. package/app/routes/auth/emailActionHandler.module.css +232 -0
  115. package/app/routes/auth/emailActionHandler.tsx +405 -0
  116. package/app/routes/auth/emailVerification.tsx +120 -0
  117. package/app/routes/auth/login.module.css +523 -0
  118. package/app/routes/auth/login.tsx +654 -0
  119. package/app/routes/auth/passwordReset.module.css +274 -0
  120. package/app/routes/auth/passwordReset.tsx +154 -0
  121. package/app/routes/auth/route.ts +16 -0
  122. package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
  123. package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
  124. package/app/routes/mobile-prevented/route.ts +14 -0
  125. package/app/routes/striae/striae.module.css +30 -0
  126. package/app/routes/striae/striae.tsx +417 -0
  127. package/app/services/audit-export.service.ts +755 -0
  128. package/app/services/audit.service.ts +1454 -0
  129. package/app/services/firebase-errors.ts +106 -0
  130. package/app/services/firebase.ts +15 -0
  131. package/app/styles/legal-pages.module.css +113 -0
  132. package/app/styles/root.module.css +146 -0
  133. package/app/tailwind.css +225 -0
  134. package/app/types/annotations.ts +45 -0
  135. package/app/types/audit.ts +301 -0
  136. package/app/types/case.ts +90 -0
  137. package/app/types/export.ts +8 -0
  138. package/app/types/file.ts +30 -0
  139. package/app/types/import.ts +107 -0
  140. package/app/types/index.ts +24 -0
  141. package/app/types/user.ts +38 -0
  142. package/app/utils/SHA256.ts +461 -0
  143. package/app/utils/annotation-timestamp.ts +25 -0
  144. package/app/utils/audit-export-signature.ts +117 -0
  145. package/app/utils/auth-action-settings.ts +48 -0
  146. package/app/utils/auth.ts +34 -0
  147. package/app/utils/batch-operations.ts +135 -0
  148. package/app/utils/confirmation-signature.ts +193 -0
  149. package/app/utils/data-operations.ts +871 -0
  150. package/app/utils/device-detection.ts +5 -0
  151. package/app/utils/html-sanitizer.ts +80 -0
  152. package/app/utils/id-generator.ts +36 -0
  153. package/app/utils/meta.ts +48 -0
  154. package/app/utils/mfa-phone.ts +97 -0
  155. package/app/utils/mfa.ts +79 -0
  156. package/app/utils/password-policy.ts +28 -0
  157. package/app/utils/permissions.ts +562 -0
  158. package/app/utils/signature-utils.ts +160 -0
  159. package/app/utils/style.ts +83 -0
  160. package/app/utils/version.ts +5 -0
  161. package/firebase.json +11 -0
  162. package/functions/[[path]].ts +10 -0
  163. package/package.json +138 -0
  164. package/postcss.config.js +6 -0
  165. package/public/.well-known/publickey.info@striae.org.asc +17 -0
  166. package/public/.well-known/security.txt +7 -0
  167. package/public/_headers +28 -0
  168. package/public/_routes.json +13 -0
  169. package/public/assets/striae.jpg +0 -0
  170. package/public/clear.jpg +0 -0
  171. package/public/favicon.ico +0 -0
  172. package/public/favicon.svg +9 -0
  173. package/public/icon-256.png +0 -0
  174. package/public/icon-512.png +0 -0
  175. package/public/logo-dark.png +0 -0
  176. package/public/manifest.json +25 -0
  177. package/public/oin-badge.png +0 -0
  178. package/public/shortcut.png +0 -0
  179. package/public/social-image.png +0 -0
  180. package/public/striae-ascii.txt +10 -0
  181. package/scripts/deploy-all.sh +100 -0
  182. package/scripts/deploy-config.sh +940 -0
  183. package/scripts/deploy-pages.sh +34 -0
  184. package/scripts/deploy-worker-secrets.sh +215 -0
  185. package/scripts/dev.cjs +23 -0
  186. package/scripts/install-workers.sh +88 -0
  187. package/scripts/run-eslint.cjs +35 -0
  188. package/scripts/update-compatibility-dates.cjs +124 -0
  189. package/scripts/update-markdown-versions.cjs +43 -0
  190. package/tailwind.config.ts +22 -0
  191. package/tsconfig.json +33 -0
  192. package/vite.config.ts +35 -0
  193. package/worker-configuration.d.ts +7490 -0
  194. package/workers/audit-worker/package.json +17 -0
  195. package/workers/audit-worker/src/audit-worker.example.ts +195 -0
  196. package/workers/audit-worker/worker-configuration.d.ts +7448 -0
  197. package/workers/audit-worker/wrangler.jsonc.example +29 -0
  198. package/workers/data-worker/package.json +17 -0
  199. package/workers/data-worker/src/data-worker.example.ts +267 -0
  200. package/workers/data-worker/src/signature-utils.ts +79 -0
  201. package/workers/data-worker/src/signing-payload-utils.ts +290 -0
  202. package/workers/data-worker/worker-configuration.d.ts +7448 -0
  203. package/workers/data-worker/wrangler.jsonc.example +30 -0
  204. package/workers/image-worker/package.json +17 -0
  205. package/workers/image-worker/src/image-worker.example.ts +180 -0
  206. package/workers/image-worker/worker-configuration.d.ts +7447 -0
  207. package/workers/image-worker/wrangler.jsonc.example +22 -0
  208. package/workers/keys-worker/package.json +17 -0
  209. package/workers/keys-worker/src/keys.example.ts +66 -0
  210. package/workers/keys-worker/src/keys.ts +66 -0
  211. package/workers/keys-worker/worker-configuration.d.ts +7447 -0
  212. package/workers/keys-worker/wrangler.jsonc.example +22 -0
  213. package/workers/pdf-worker/package.json +17 -0
  214. package/workers/pdf-worker/src/format-striae.ts +534 -0
  215. package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
  216. package/workers/pdf-worker/src/report-types.ts +69 -0
  217. package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
  218. package/workers/pdf-worker/wrangler.jsonc.example +26 -0
  219. package/workers/user-worker/package.json +17 -0
  220. package/workers/user-worker/src/user-worker.example.ts +636 -0
  221. package/workers/user-worker/worker-configuration.d.ts +7448 -0
  222. package/workers/user-worker/wrangler.jsonc.example +29 -0
  223. package/wrangler.toml.example +8 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "type": "your-service-account-type",
3
+ "project_id": "your-project-id",
4
+ "private_key_id": "your-private-key-id",
5
+ "private_key": "your-private-key",
6
+ "client_email": "your-client-email",
7
+ "client_id": "your-client-id",
8
+ "auth_uri": "your-auth-uri",
9
+ "token_uri": "your-token-uri",
10
+ "auth_provider_x509_cert_url": "your-auth-provider-cert-url",
11
+ "client_x509_cert_url": "your-client-cert-url",
12
+ "universe_domain": "your-universe-domain"
13
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "url": "PAGES_CUSTOM_DOMAIN",
3
+ "data_worker_url": "DATA_WORKER_CUSTOM_DOMAIN",
4
+ "keys_url": "KEYS_WORKER_CUSTOM_DOMAIN",
5
+ "image_worker_url": "IMAGE_WORKER_CUSTOM_DOMAIN",
6
+ "user_worker_url": "USER_WORKER_CUSTOM_DOMAIN",
7
+ "pdf_worker_url": "PDF_WORKER_CUSTOM_DOMAIN",
8
+ "audit_worker_url": "AUDIT_WORKER_CUSTOM_DOMAIN",
9
+ "keys_auth": "YOUR_KEYS_AUTH_TOKEN",
10
+ "manifest_signing_key_id": "MANIFEST_SIGNING_KEY_ID",
11
+ "manifest_signing_public_key": "MANIFEST_SIGNING_PUBLIC_KEY",
12
+ "manifest_signing_public_keys": {
13
+ "MANIFEST_SIGNING_KEY_ID": "MANIFEST_SIGNING_PUBLIC_KEY"
14
+ },
15
+ "max_cases_review": 0,
16
+ "max_files_per_case_review": 0
17
+ }
@@ -0,0 +1,21 @@
1
+ interface FirebaseConfig {
2
+ apiKey: string;
3
+ authDomain: string;
4
+ projectId: string;
5
+ storageBucket: string;
6
+ messagingSenderId: string;
7
+ appId: string;
8
+ measurementId: string;
9
+ }
10
+
11
+ const firebaseConfig: FirebaseConfig = {
12
+ apiKey: "YOUR_FIREBASE_API_KEY",
13
+ authDomain: "YOUR_FIREBASE_AUTH_DOMAIN",
14
+ projectId: "YOUR_FIREBASE_PROJECT_ID",
15
+ storageBucket: "YOUR_FIREBASE_STORAGE_BUCKET",
16
+ messagingSenderId: "YOUR_FIREBASE_MESSAGING_SENDER_ID",
17
+ appId: "YOUR_FIREBASE_APP_ID",
18
+ measurementId: "YOUR_FIREBASE_MEASUREMENT_ID"
19
+ };
20
+
21
+ export default firebaseConfig;
@@ -0,0 +1,13 @@
1
+ export const INACTIVITY_CONFIG = {
2
+ TIMEOUT_MINUTES: 60,
3
+ WARNING_MINUTES: 5,
4
+ TRACKED_ACTIVITIES: [
5
+ 'mousedown',
6
+ 'mousemove',
7
+ 'keypress',
8
+ 'scroll',
9
+ 'touchstart',
10
+ 'click',
11
+ 'keydown'
12
+ ] as const
13
+ };
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "Striae",
3
+ "author": "Stephen J. Lu",
4
+ "title": "A Firearms Examiner's Comparison Companion",
5
+ "url": "PAGES_CUSTOM_DOMAIN"
6
+ }
@@ -0,0 +1,12 @@
1
+ import { createContext } from 'react';
2
+ import { User } from 'firebase/auth';
3
+
4
+ interface AuthContextType {
5
+ user: User | null;
6
+ setUser: (user: User | null) => void;
7
+ }
8
+
9
+ export const AuthContext = createContext<AuthContextType>({
10
+ user: null,
11
+ setUser: () => {},
12
+ });
@@ -0,0 +1,12 @@
1
+ import { RemixBrowser } from "@remix-run/react";
2
+ import { startTransition, StrictMode } from "react";
3
+ import { hydrateRoot } from "react-dom/client";
4
+
5
+ startTransition(() => {
6
+ hydrateRoot(
7
+ document,
8
+ <StrictMode>
9
+ <RemixBrowser />
10
+ </StrictMode>
11
+ );
12
+ });
@@ -0,0 +1,44 @@
1
+ import type { AppLoadContext, EntryContext } from "@remix-run/cloudflare";
2
+ import { RemixServer } from "@remix-run/react";
3
+ import { isbot } from "isbot";
4
+ import { renderToReadableStream } from "react-dom/server";
5
+
6
+ export default async function handleRequest(
7
+ request: Request,
8
+ responseStatusCode: number,
9
+ responseHeaders: Headers,
10
+ remixContext: EntryContext,
11
+ loadContext: AppLoadContext
12
+ ) {
13
+ const body = await renderToReadableStream(
14
+ <RemixServer context={remixContext} url={request.url} />,
15
+ {
16
+ // If you wish to abort the rendering process, you can pass a signal here.
17
+ // Please refer to the templates for example son how to configure this.
18
+ // signal: controller.signal,
19
+ onError(error: unknown) {
20
+ // Log streaming rendering errors from inside the shell
21
+ console.error(error);
22
+ responseStatusCode = 500;
23
+ },
24
+ }
25
+ );
26
+
27
+ if (isBotRequest(request.headers.get("user-agent"))) {
28
+ await body.allReady;
29
+ }
30
+
31
+ responseHeaders.set("Content-Type", "text/html");
32
+ return new Response(body, {
33
+ headers: responseHeaders,
34
+ status: responseStatusCode,
35
+ });
36
+ }
37
+
38
+ function isBotRequest(userAgent: string | null) {
39
+ if (!userAgent) {
40
+ return false;
41
+ }
42
+
43
+ return isbot(userAgent);
44
+ }
@@ -0,0 +1,110 @@
1
+ import { useEffect, useRef, useCallback } from 'react';
2
+ import { useLocation } from '@remix-run/react';
3
+ import { signOut } from 'firebase/auth';
4
+ import { auth } from '~/services/firebase';
5
+ import { INACTIVITY_CONFIG } from '~/config/inactivity';
6
+
7
+ interface UseInactivityTimeoutOptions {
8
+ timeoutMinutes?: number;
9
+ warningMinutes?: number;
10
+ onWarning?: () => void;
11
+ onTimeout?: () => void;
12
+ enabled?: boolean;
13
+ }
14
+
15
+ export const useInactivityTimeout = ({
16
+ timeoutMinutes = INACTIVITY_CONFIG.TIMEOUT_MINUTES,
17
+ warningMinutes = INACTIVITY_CONFIG.WARNING_MINUTES,
18
+ onWarning,
19
+ onTimeout,
20
+ enabled = true
21
+ }: UseInactivityTimeoutOptions = {}) => {
22
+ const location = useLocation();
23
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
24
+ const warningTimeoutRef = useRef<NodeJS.Timeout | null>(null);
25
+ const lastActivityRef = useRef<number>(Date.now());
26
+
27
+ const isAuthRoute = location.pathname.startsWith('/auth');
28
+ const shouldEnable = enabled && isAuthRoute;
29
+
30
+ const clearTimeouts = useCallback(() => {
31
+ if (timeoutRef.current) {
32
+ clearTimeout(timeoutRef.current);
33
+ timeoutRef.current = null;
34
+ }
35
+ if (warningTimeoutRef.current) {
36
+ clearTimeout(warningTimeoutRef.current);
37
+ warningTimeoutRef.current = null;
38
+ }
39
+ }, []);
40
+
41
+ const handleSignOut = useCallback(async () => {
42
+ try {
43
+ await signOut(auth);
44
+ onTimeout?.();
45
+ } catch (error) {
46
+ console.error('Error signing out:', error);
47
+ }
48
+ }, [onTimeout]);
49
+
50
+ const handleWarning = useCallback(() => {
51
+ onWarning?.();
52
+ }, [onWarning]);
53
+
54
+ const resetTimer = useCallback(() => {
55
+ if (!shouldEnable) return;
56
+
57
+ lastActivityRef.current = Date.now();
58
+ clearTimeouts();
59
+
60
+ const warningMs = (timeoutMinutes - warningMinutes) * 60 * 1000;
61
+ if (warningMs > 0) {
62
+ warningTimeoutRef.current = setTimeout(handleWarning, warningMs);
63
+ }
64
+
65
+ const timeoutMs = timeoutMinutes * 60 * 1000;
66
+ timeoutRef.current = setTimeout(handleSignOut, timeoutMs);
67
+ }, [shouldEnable, timeoutMinutes, warningMinutes, handleWarning, handleSignOut, clearTimeouts]);
68
+
69
+ const extendSession = useCallback(() => {
70
+ resetTimer();
71
+ }, [resetTimer]);
72
+
73
+ const getRemainingTime = useCallback(() => {
74
+ const elapsed = Date.now() - lastActivityRef.current;
75
+ const remaining = (timeoutMinutes * 60 * 1000) - elapsed;
76
+ return Math.max(0, Math.floor(remaining / 1000));
77
+ }, [timeoutMinutes]);
78
+
79
+ useEffect(() => {
80
+ if (!shouldEnable) {
81
+ clearTimeouts();
82
+ return;
83
+ }
84
+
85
+ const activities = INACTIVITY_CONFIG.TRACKED_ACTIVITIES;
86
+
87
+ const handleActivity = () => {
88
+ resetTimer();
89
+ };
90
+
91
+ activities.forEach(activity => {
92
+ document.addEventListener(activity, handleActivity, true);
93
+ });
94
+
95
+ resetTimer();
96
+
97
+ return () => {
98
+ activities.forEach(activity => {
99
+ document.removeEventListener(activity, handleActivity, true);
100
+ });
101
+ clearTimeouts();
102
+ };
103
+ }, [shouldEnable, resetTimer, clearTimeouts, location.pathname]);
104
+
105
+ return {
106
+ extendSession,
107
+ getRemainingTime,
108
+ clearTimeouts
109
+ };
110
+ };
package/app/root.tsx ADDED
@@ -0,0 +1,170 @@
1
+ import type { LinksFunction } from "@remix-run/cloudflare";
2
+ import {
3
+ Links,
4
+ Meta,
5
+ Outlet,
6
+ Scripts,
7
+ ScrollRestoration,
8
+ isRouteErrorResponse,
9
+ useRouteError,
10
+ Link,
11
+ useLocation,
12
+ useMatches
13
+ } from "@remix-run/react";
14
+ import {
15
+ ThemeProvider,
16
+ themeStyles
17
+ } from '~/components/theme-provider/theme-provider';
18
+ import { AuthProvider } from '~/components/auth/auth-provider';
19
+ import styles from '~/styles/root.module.css';
20
+ import './tailwind.css';
21
+
22
+ export const links: LinksFunction = () => [
23
+ { rel: "preconnect", href: "https://fonts.googleapis.com" },
24
+ {
25
+ rel: "preconnect",
26
+ href: "https://fonts.gstatic.com",
27
+ crossOrigin: "anonymous" as const,
28
+ },
29
+ {
30
+ rel: "stylesheet",
31
+ href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
32
+ },
33
+ { rel: 'manifest', href: '/manifest.json' },
34
+ { rel: 'icon', href: '/favicon.ico' },
35
+ { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' },
36
+ { rel: 'shortcut_icon', href: '/shortcut.png', type: 'image/png', sizes: '64x64' },
37
+ { rel: 'apple-touch-icon', href: '/icon-256.png', sizes: '256x256' },
38
+ ];
39
+
40
+ type AppTheme = 'dark' | 'light';
41
+
42
+ interface ThemeHandle {
43
+ theme?: AppTheme;
44
+ }
45
+
46
+ const DEFAULT_THEME: AppTheme = 'light';
47
+
48
+ const isAppTheme = (value: unknown): value is AppTheme => {
49
+ return value === 'dark' || value === 'light';
50
+ };
51
+
52
+ const resolveRouteTheme = (matches: ReturnType<typeof useMatches>): AppTheme => {
53
+ for (let index = matches.length - 1; index >= 0; index -= 1) {
54
+ const routeHandle = matches[index].handle as ThemeHandle | undefined;
55
+
56
+ if (isAppTheme(routeHandle?.theme)) {
57
+ return routeHandle.theme;
58
+ }
59
+ }
60
+
61
+ return DEFAULT_THEME;
62
+ };
63
+
64
+ export function Layout({ children }: { children: React.ReactNode }) {
65
+ const matches = useMatches();
66
+ const theme = resolveRouteTheme(matches);
67
+ const themeColor = theme === 'dark' ? '#000000' : '#f5f5f5';
68
+
69
+ return (
70
+ <html lang="en" data-theme={theme}>
71
+ <head>
72
+ <meta charSet="utf-8" />
73
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
74
+ <meta name="theme-color" content={themeColor} />
75
+ <meta name="color-scheme" content={theme} />
76
+ <style dangerouslySetInnerHTML={{ __html: themeStyles }} />
77
+ <Meta />
78
+ <Links />
79
+ </head>
80
+ <body className="flex flex-col h-screen w-full overflow-x-hidden">
81
+ <ThemeProvider theme={theme} className="">
82
+ <main>
83
+ {children}
84
+ </main>
85
+ </ThemeProvider>
86
+ <Scripts />
87
+ <ScrollRestoration />
88
+ </body>
89
+ </html>
90
+ );
91
+ }
92
+
93
+ export default function App() {
94
+ const matches = useMatches();
95
+ const location = useLocation();
96
+ const isAuthRoute = matches.some(match =>
97
+ match.id.includes('auth') ||
98
+ match.pathname?.includes('/auth')
99
+ ) || location.pathname === '/';
100
+
101
+ if (isAuthRoute) {
102
+ return (
103
+ <AuthProvider>
104
+ <Outlet />
105
+ </AuthProvider>
106
+ );
107
+ }
108
+
109
+ return <Outlet />;
110
+ }
111
+
112
+ export function ErrorBoundary() {
113
+ const error = useRouteError();
114
+
115
+ if (isRouteErrorResponse(error)) {
116
+ return (
117
+ <html lang="en">
118
+ <head>
119
+ <title>{`${error.status} ${error.statusText}`}</title>
120
+ </head>
121
+ <body className="flex flex-col h-screen">
122
+ <ThemeProvider theme="light" className="">
123
+ <main>
124
+ <div className={styles.errorContainer}>
125
+ <div className={styles.errorTitle}>{error.status}</div>
126
+ <p className={styles.errorMessage}>{error.statusText}</p>
127
+ <Link
128
+ viewTransition
129
+ prefetch="intent"
130
+ to="https://striae.org"
131
+ className={styles.errorLink}>
132
+ Return Home
133
+ </Link>
134
+ </div>
135
+ </main>
136
+ </ThemeProvider>
137
+ <ScrollRestoration />
138
+ <Scripts />
139
+ </body>
140
+ </html>
141
+ );
142
+ }
143
+
144
+ return (
145
+ <html lang="en">
146
+ <head>
147
+ <title>Oops! Something went wrong</title>
148
+ </head>
149
+ <body className="flex flex-col h-screen">
150
+ <ThemeProvider theme="light" className="">
151
+ <main>
152
+ <div className={styles.errorContainer}>
153
+ <div className={styles.errorTitle}>500</div>
154
+ <p className={styles.errorMessage}>Something went wrong. Please try again later.</p>
155
+ <Link
156
+ viewTransition
157
+ prefetch="intent"
158
+ to="https://striae.org"
159
+ className={styles.errorLink}>
160
+ Return Home
161
+ </Link>
162
+ </div>
163
+ </main>
164
+ </ThemeProvider>
165
+ <ScrollRestoration />
166
+ <Scripts />
167
+ </body>
168
+ </html>
169
+ );
170
+ }
@@ -0,0 +1,16 @@
1
+ import { redirect, type LoaderFunctionArgs } from '@remix-run/cloudflare';
2
+ import { isMobileOrTabletUserAgent } from '~/utils/device-detection';
3
+
4
+ export const loader = async ({ request }: LoaderFunctionArgs) => {
5
+ const requestUrl = new URL(request.url);
6
+ const search = requestUrl.search ?? '';
7
+ const userAgent = request.headers.get('user-agent') ?? '';
8
+
9
+ if (isMobileOrTabletUserAgent(userAgent)) {
10
+ throw redirect(`/mobile-prevented${search}`);
11
+ }
12
+
13
+ return null;
14
+ };
15
+
16
+ export { Login as default, meta } from './auth/login';
@@ -0,0 +1,232 @@
1
+ .container {
2
+ position: relative;
3
+ display: flex;
4
+ justify-content: center;
5
+ align-items: center;
6
+ min-height: 100vh;
7
+ padding: var(--spaceL);
8
+ z-index: var(--zIndex0);
9
+ }
10
+
11
+ .logoLink {
12
+ display: block;
13
+ }
14
+
15
+ .logo {
16
+ position: fixed;
17
+ top: var(--spaceL);
18
+ left: var(--spaceL);
19
+ width: 150px;
20
+ height: 150px;
21
+ background-image: url("/logo-dark.png");
22
+ background-size: contain;
23
+ background-repeat: no-repeat;
24
+ background-position: center;
25
+ z-index: var(--zIndex2);
26
+ transition: transform var(--durationM) var(--bezierFastoutSlowin);
27
+ }
28
+
29
+ .logo:hover {
30
+ transform: scale(1.05);
31
+ }
32
+
33
+ .formWrapper {
34
+ background-color: color-mix(in lab, var(--backgroundLight) 95%, transparent);
35
+ padding: var(--space2XL);
36
+ border-radius: var(--spaceXS);
37
+ box-shadow: 0 var(--spaceXS) var(--spaceM)
38
+ color-mix(in lab, var(--black) 10%, transparent);
39
+ width: 100%;
40
+ max-width: var(--maxWidthS);
41
+ }
42
+
43
+ .title {
44
+ text-align: center;
45
+ color: var(--textTitle);
46
+ margin-bottom: var(--spaceL);
47
+ font-size: var(--fontSizeH4);
48
+ }
49
+
50
+ .description {
51
+ color: var(--textBody);
52
+ text-align: center;
53
+ margin-bottom: var(--spaceM);
54
+ font-size: var(--fontSizeBodyS);
55
+ }
56
+
57
+ .hint {
58
+ color: var(--textLight);
59
+ text-align: center;
60
+ margin-bottom: var(--spaceM);
61
+ font-size: var(--fontSizeBodyXS);
62
+ }
63
+
64
+ .form {
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: var(--spaceM);
68
+ }
69
+
70
+ .input {
71
+ width: 100%;
72
+ padding: var(--spaceL);
73
+ border: 1.5px solid color-mix(in lab, var(--text) 20%, transparent);
74
+ border-radius: var(--spaceXS);
75
+ font-size: var(--fontSizeBodyM);
76
+ transition: all var(--durationS) var(--bezierFastoutSlowin);
77
+ box-sizing: border-box;
78
+ }
79
+
80
+ .input:focus {
81
+ outline: none;
82
+ border-color: var(--primary);
83
+ box-shadow: 0 0 0 2px color-mix(in lab, var(--primary) 25%, transparent);
84
+ }
85
+
86
+ .passwordField {
87
+ position: relative;
88
+ display: flex;
89
+ align-items: center;
90
+ width: 100%;
91
+ }
92
+
93
+ .passwordField .input {
94
+ padding-right: var(--space2XL);
95
+ }
96
+
97
+ .passwordToggle {
98
+ position: absolute;
99
+ right: var(--spaceM);
100
+ background: none;
101
+ border: none;
102
+ cursor: pointer;
103
+ font-size: 16px;
104
+ padding: var(--spaceXS);
105
+ border-radius: var(--spaceXS);
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ transition: background-color var(--durationS) var(--bezierFastoutSlowin);
110
+ z-index: 1;
111
+ height: auto;
112
+ min-width: 24px;
113
+ }
114
+
115
+ .passwordToggle:hover {
116
+ background-color: color-mix(in lab, var(--text) 10%, transparent);
117
+ }
118
+
119
+ .passwordToggle:focus {
120
+ outline: 2px solid var(--primary);
121
+ outline-offset: 2px;
122
+ }
123
+
124
+ .passwordFeedback {
125
+ color: var(--textLight);
126
+ font-size: var(--fontSizeBodyXS);
127
+ margin-top: var(--spaceS);
128
+ }
129
+
130
+ .passwordFeedback pre {
131
+ margin: 0;
132
+ font-family: inherit;
133
+ white-space: pre-line;
134
+ }
135
+
136
+ .button {
137
+ width: 100%;
138
+ padding: var(--spaceL);
139
+ background-color: var(--primary);
140
+ color: var(--white);
141
+ border: none;
142
+ border-radius: var(--spaceXS);
143
+ font-weight: var(--fontWeightMedium);
144
+ font-size: var(--fontSizeBodyM);
145
+ cursor: pointer;
146
+ transition: all var(--durationS) var(--bezierFastoutSlowin);
147
+ box-sizing: border-box;
148
+ }
149
+
150
+ .button:hover:not(:disabled) {
151
+ background-color: color-mix(in lab, var(--primary) 85%, var(--black));
152
+ }
153
+
154
+ .button:disabled {
155
+ background-color: color-mix(in lab, var(--background) 95%, transparent);
156
+ color: var(--textLight);
157
+ cursor: not-allowed;
158
+ }
159
+
160
+ .secondaryButton {
161
+ width: 100%;
162
+ padding: var(--spaceL);
163
+ background-color: transparent;
164
+ border: 1.5px solid var(--primary);
165
+ color: var(--primary);
166
+ border-radius: var(--spaceXS);
167
+ font-weight: var(--fontWeightMedium);
168
+ font-size: var(--fontSizeBodyM);
169
+ cursor: pointer;
170
+ transition: all var(--durationS) var(--bezierFastoutSlowin);
171
+ box-sizing: border-box;
172
+ }
173
+
174
+ .secondaryButton:hover {
175
+ background-color: color-mix(in lab, var(--primary) 10%, transparent);
176
+ }
177
+
178
+ .loginToStriaeButton {
179
+ width: 100%;
180
+ padding: var(--spaceL);
181
+ background-color: #1e7e34;
182
+ color: var(--white);
183
+ border: none;
184
+ border-radius: var(--spaceXS);
185
+ font-weight: var(--fontWeightMedium);
186
+ font-size: var(--fontSizeBodyM);
187
+ cursor: pointer;
188
+ transition: all var(--durationS) var(--bezierFastoutSlowin);
189
+ box-sizing: border-box;
190
+ }
191
+
192
+ .loginToStriaeButton:hover {
193
+ background-color: #155724;
194
+ }
195
+
196
+ .actions {
197
+ display: flex;
198
+ flex-direction: column;
199
+ gap: var(--spaceM);
200
+ }
201
+
202
+ .error {
203
+ color: color-mix(in lab, var(--error) 90%, var(--black));
204
+ text-align: left;
205
+ margin: var(--spaceXS) 0 var(--spaceS) 0;
206
+ font-size: var(--fontSizeBodyXS);
207
+ padding: var(--spaceS) var(--spaceM);
208
+ background: linear-gradient(
209
+ 135deg,
210
+ color-mix(in lab, var(--error) 12%, transparent),
211
+ color-mix(in lab, var(--error) 8%, transparent)
212
+ );
213
+ border-radius: var(--spaceXS);
214
+ border: 1px solid color-mix(in lab, var(--error) 30%, transparent);
215
+ border-left: 3px solid var(--error);
216
+ }
217
+
218
+ .success {
219
+ color: color-mix(in lab, var(--success) 90%, var(--black));
220
+ text-align: left;
221
+ margin: var(--spaceXS) 0 var(--spaceS) 0;
222
+ font-size: var(--fontSizeBodyXS);
223
+ padding: var(--spaceS) var(--spaceM);
224
+ background: linear-gradient(
225
+ 135deg,
226
+ color-mix(in lab, var(--success) 12%, transparent),
227
+ color-mix(in lab, var(--success) 8%, transparent)
228
+ );
229
+ border-radius: var(--spaceXS);
230
+ border: 1px solid color-mix(in lab, var(--success) 30%, transparent);
231
+ border-left: 3px solid var(--success);
232
+ }