@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,321 @@
1
+ /* Layout */
2
+ .sidebar {
3
+ position: relative;
4
+ width: 300px;
5
+ min-width: 250px;
6
+ height: calc(100vh - 60px);
7
+ background-color: #f8f9fa;
8
+ border-right: 1px solid #dee2e6;
9
+ padding: 1.5rem;
10
+ overflow-y: auto;
11
+ flex-shrink: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ /* User Section */
16
+ .userInfo {
17
+ display: flex;
18
+ flex-direction: column;
19
+ align-items: flex-start;
20
+ gap: 1rem;
21
+ padding-bottom: 1rem;
22
+ border-bottom: 2px solid #e9ecef;
23
+ margin-bottom: 1rem;
24
+ }
25
+
26
+ .userTitle {
27
+ font-size: 1.2rem;
28
+ font-weight: 600;
29
+ color: #212529;
30
+ margin: 0;
31
+ }
32
+
33
+ .userActions {
34
+ display: flex;
35
+ gap: 0.5rem;
36
+ width: 100%;
37
+ }
38
+
39
+ .profileButton {
40
+ padding: 0.5rem 1rem;
41
+ background-color: #6c757d;
42
+ color: white;
43
+ border: none;
44
+ border-radius: 6px;
45
+ font-size: 0.875rem;
46
+ cursor: pointer;
47
+ transition: all 0.2s;
48
+ }
49
+
50
+ .profileButton:hover {
51
+ background-color: #5c636a;
52
+ }
53
+
54
+ /* Footer Button */
55
+ .footerButton {
56
+ position: fixed;
57
+ bottom: 1.5rem;
58
+ left: 1.5rem;
59
+ width: calc(300px - 3rem);
60
+ padding: 0.75rem 1rem;
61
+ background-color: #0d6efd;
62
+ color: white;
63
+ border: none;
64
+ border-radius: 6px;
65
+ font-size: 0.875rem;
66
+ font-weight: 500;
67
+ cursor: pointer;
68
+ transition: all 0.2s;
69
+ text-align: center;
70
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
71
+ z-index: 10;
72
+ }
73
+
74
+ /* Footer Section */
75
+ .footerSection {
76
+ width: 300px;
77
+ min-width: 250px;
78
+ height: 60px;
79
+ background-color: #f8f9fa;
80
+ border-right: 1px solid #dee2e6;
81
+ border-top: 1px solid #dee2e6;
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ padding: 0 1.5rem;
86
+ box-sizing: border-box;
87
+ }
88
+
89
+ .footerSectionButton {
90
+ width: 100%;
91
+ padding: 0.75rem 1rem;
92
+ background-color: #6c757d;
93
+ color: white;
94
+ border: none;
95
+ border-radius: 6px;
96
+ font-size: 0.875rem;
97
+ font-weight: 500;
98
+ cursor: pointer;
99
+ transition: all 0.2s;
100
+ text-align: center;
101
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
102
+ }
103
+
104
+ .footerButton:hover {
105
+ background-color: #0b5ed7;
106
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
107
+ }
108
+
109
+ .footerSectionButton:hover {
110
+ background-color: #5c636a;
111
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
112
+ transform: translateY(-1px);
113
+ }
114
+
115
+ /* Footer Modal */
116
+ .footerModalOverlay {
117
+ position: fixed;
118
+ inset: 0;
119
+ background-color: rgba(0, 0, 0, 0.75);
120
+ display: flex;
121
+ justify-content: center;
122
+ align-items: center;
123
+ z-index: 1000;
124
+ }
125
+
126
+ .footerModal {
127
+ background: white;
128
+ border-radius: 8px;
129
+ padding: 2rem;
130
+ max-width: 600px;
131
+ width: 90%;
132
+ max-height: 80vh;
133
+ overflow-y: auto;
134
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
135
+ }
136
+
137
+ .footerModalHeader {
138
+ display: flex;
139
+ justify-content: space-between;
140
+ align-items: center;
141
+ margin-bottom: 2rem;
142
+ border-bottom: 1px solid #dee2e6;
143
+ padding-bottom: 1rem;
144
+ }
145
+
146
+ .footerModalTitle {
147
+ font-size: 1.5rem;
148
+ font-weight: 600;
149
+ color: #212529;
150
+ margin: 0;
151
+ }
152
+
153
+ .footerModalClose {
154
+ background: none;
155
+ border: none;
156
+ font-size: 1.5rem;
157
+ cursor: pointer;
158
+ color: #6c757d;
159
+ padding: 0.5rem;
160
+ border-radius: 4px;
161
+ transition: background-color 0.2s;
162
+ }
163
+
164
+ .footerModalClose:hover {
165
+ background-color: #f8f9fa;
166
+ transform: translateY(-1px);
167
+ }
168
+
169
+ .footerModalContent {
170
+ display: flex;
171
+ flex-direction: column;
172
+ gap: 1.5rem;
173
+ }
174
+
175
+ .footerModalLinks {
176
+ display: grid;
177
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
178
+ gap: 1rem;
179
+ }
180
+
181
+ .footerModalLink {
182
+ display: block;
183
+ padding: 1rem;
184
+ background-color: #f8f9fa;
185
+ border: 1px solid #dee2e6;
186
+ border-radius: 6px;
187
+ text-decoration: none;
188
+ color: #212529;
189
+ transition: all 0.2s;
190
+ text-align: center;
191
+ font-weight: 500;
192
+ }
193
+
194
+ .footerModalLink:hover {
195
+ background-color: #e9ecef;
196
+ color: #495057;
197
+ text-decoration: none;
198
+ transform: translateY(-1px);
199
+ }
200
+
201
+ .footerModalCopyright {
202
+ text-align: center;
203
+ color: #6c757d;
204
+ font-size: 0.875rem;
205
+ padding-top: 1rem;
206
+ border-top: 1px solid #dee2e6;
207
+ margin-top: 1rem;
208
+ }
209
+
210
+ .link {
211
+ color: #6c757d;
212
+ text-decoration: none;
213
+ }
214
+
215
+ .link:hover {
216
+ color: #495057;
217
+ text-decoration: underline;
218
+ text-decoration-style: dotted;
219
+ text-underline-offset: 3px;
220
+ }
221
+
222
+ .licenseLinkButton {
223
+ border: none;
224
+ background: none;
225
+ padding: 0;
226
+ margin: 0;
227
+ color: #6c757d;
228
+ text-decoration: none;
229
+ font-size: 0.875rem;
230
+ font-family: inherit;
231
+ cursor: pointer;
232
+ transition: color var(--durationS) var(--bezierFastoutSlowin);
233
+ }
234
+
235
+ .licenseLinkButton:hover {
236
+ color: #495057;
237
+ text-decoration: underline;
238
+ text-decoration-style: dotted;
239
+ text-underline-offset: 3px;
240
+ }
241
+
242
+ .badgeContainer {
243
+ display: flex;
244
+ justify-content: center;
245
+ align-items: center;
246
+ gap: var(--spaceM, 1rem);
247
+ margin-top: var(--spaceXS, 0.5rem);
248
+ flex-wrap: wrap;
249
+ width: 100%;
250
+ }
251
+
252
+ .oinBadge {
253
+ text-align: center;
254
+ }
255
+
256
+ .oinBadgeLink {
257
+ display: inline-block;
258
+ transition: opacity var(--durationS, 0.2s) var(--bezierFastoutSlowin, ease-out);
259
+ }
260
+
261
+ .oinBadgeLink:hover {
262
+ opacity: 0.8;
263
+ transform: translateY(-1px);
264
+ }
265
+
266
+ .oinBadgeImage {
267
+ height: 40px;
268
+ width: auto;
269
+ border-radius: 4px;
270
+ }
271
+
272
+ /* Import Section */
273
+ .importSection {
274
+ margin-top: auto;
275
+ padding: 1rem 0;
276
+ border-top: 1px solid var(--divider);
277
+ }
278
+
279
+ .importButton {
280
+ width: 100%;
281
+ padding: 0.75rem 1rem;
282
+ background: var(--success);
283
+ color: white;
284
+ border: none;
285
+ border-radius: var(--spaceXS);
286
+ font-size: var(--fontSizeBodyS);
287
+ font-weight: var(--fontWeightMedium);
288
+ cursor: pointer;
289
+ transition: all var(--durationS) var(--bezierFastoutSlowin);
290
+ box-shadow: 0 1px 3px color-mix(in lab, var(--success) 30%, transparent);
291
+ box-sizing: border-box;
292
+ }
293
+
294
+ .importButton:hover {
295
+ background: color-mix(in lab, var(--success) 85%, var(--black));
296
+ box-shadow: 0 2px 6px color-mix(in lab, var(--success) 40%, transparent);
297
+ }
298
+
299
+ /* Hash Button */
300
+ .hashButton {
301
+ width: 100%;
302
+ padding: 0.75rem 1rem;
303
+ background: #6c757d;
304
+ color: white;
305
+ border: none;
306
+ border-radius: var(--spaceXS);
307
+ font-size: var(--fontSizeBodyS);
308
+ font-weight: var(--fontWeightMedium);
309
+ cursor: pointer;
310
+ transition: all var(--durationS) var(--bezierFastoutSlowin);
311
+ box-shadow: 0 1px 3px color-mix(in lab, #6c757d 30%, transparent);
312
+ box-sizing: border-box;
313
+ margin-top: 0.5rem;
314
+ }
315
+
316
+ .hashButton:hover {
317
+ background: #5c636a;
318
+ box-shadow: 0 2px 6px color-mix(in lab, #6c757d 40%, transparent);
319
+ }
320
+
321
+
@@ -0,0 +1,215 @@
1
+ import { User } from 'firebase/auth';
2
+ import { useState, useCallback } from 'react';
3
+ import styles from './sidebar.module.css';
4
+ import { ManageProfile } from '../user/manage-profile';
5
+ import { SignOut } from '../actions/signout';
6
+ import { CaseSidebar } from './cases/case-sidebar';
7
+ import { NotesSidebar } from './notes/notes-sidebar';
8
+ import { CaseImport } from './case-import/case-import';
9
+ import { HashUtility } from './hash/hash-utility';
10
+ import { Toast } from '../toast/toast';
11
+ import { FileData } from '~/types';
12
+ import { ImportResult, ConfirmationImportResult } from '~/types';
13
+
14
+ interface SidebarProps {
15
+ user: User;
16
+ onImageSelect: (file: FileData) => void;
17
+ imageId?: string;
18
+ onCaseChange: (caseNumber: string) => void;
19
+ currentCase: string;
20
+ setCurrentCase: (caseNumber: string) => void;
21
+ files: FileData[];
22
+ setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
23
+ imageLoaded: boolean;
24
+ setImageLoaded: (loaded: boolean) => void;
25
+ caseNumber: string;
26
+ setCaseNumber: (caseNumber: string) => void;
27
+ error: string;
28
+ setError: (error: string) => void;
29
+ successAction: 'loaded' | 'created' | 'deleted' | null;
30
+ setSuccessAction: (action: 'loaded' | 'created' | 'deleted' | null) => void;
31
+ showNotes: boolean;
32
+ setShowNotes: (show: boolean) => void;
33
+ onAnnotationRefresh?: () => void;
34
+ isReadOnly?: boolean;
35
+ isConfirmed?: boolean;
36
+ confirmationSaveVersion?: number;
37
+ isUploading?: boolean;
38
+ }
39
+
40
+ export const Sidebar = ({
41
+ user,
42
+ onImageSelect,
43
+ imageId,
44
+ onCaseChange,
45
+ currentCase,
46
+ setCurrentCase,
47
+ imageLoaded,
48
+ setImageLoaded,
49
+ files,
50
+ setFiles,
51
+ caseNumber,
52
+ setCaseNumber,
53
+ error,
54
+ setError,
55
+ successAction,
56
+ setSuccessAction,
57
+ showNotes,
58
+ setShowNotes,
59
+ onAnnotationRefresh,
60
+ isReadOnly = false,
61
+ isConfirmed = false,
62
+ confirmationSaveVersion = 0,
63
+ isUploading: initialIsUploading = false,
64
+ }: SidebarProps) => {
65
+ const [isProfileModalOpen, setIsProfileModalOpen] = useState(false);
66
+ const [isImportModalOpen, setIsImportModalOpen] = useState(false);
67
+ const [isHashModalOpen, setIsHashModalOpen] = useState(false);
68
+ const [isUploading, setIsUploading] = useState(initialIsUploading);
69
+ const [toastMessage, setToastMessage] = useState('');
70
+ const [toastType, setToastType] = useState<'success' | 'error' | 'warning'>('success');
71
+ const [isToastVisible, setIsToastVisible] = useState(false);
72
+
73
+ const handleImportComplete = useCallback((result: ImportResult | ConfirmationImportResult) => {
74
+ if (result.success) {
75
+ // For case imports, load the imported case automatically
76
+ if ('isReadOnly' in result) {
77
+ // This is an ImportResult (case import)
78
+ if (result.caseNumber && result.isReadOnly) {
79
+ // Successful read-only case import - load the case
80
+ onCaseChange(result.caseNumber);
81
+ setCurrentCase(result.caseNumber);
82
+ setCaseNumber(result.caseNumber);
83
+ setSuccessAction('loaded');
84
+ } else if (!result.caseNumber && !result.isReadOnly) {
85
+ // Read-only case cleared - reset all UI state
86
+ setCurrentCase('');
87
+ setCaseNumber('');
88
+ setFiles([]);
89
+ onImageSelect({ id: 'clear', originalFilename: '/clear.jpg', uploadedAt: '' });
90
+ setImageLoaded(false);
91
+ onCaseChange(''); // This will trigger canvas/annotation state reset in main component
92
+ setShowNotes(false); // Close notes sidebar
93
+ setSuccessAction(null);
94
+ }
95
+ }
96
+ // For confirmation imports, no action needed - the confirmations are already loaded
97
+ }
98
+ }, [onCaseChange, setCurrentCase, setCaseNumber, setSuccessAction, setFiles, onImageSelect, setImageLoaded, setShowNotes]);
99
+
100
+ const handleUploadComplete = useCallback((result: { successCount: number; failedFiles: string[] }) => {
101
+ if (result.successCount === 0 && result.failedFiles.length > 0) {
102
+ // All files failed
103
+ setToastType('error');
104
+ const errorList = result.failedFiles.map(fn => `${fn} was not uploaded`).join(', ');
105
+ setToastMessage(`Errors: ${errorList}`);
106
+ } else if (result.failedFiles.length > 0) {
107
+ // Some files succeeded, some failed
108
+ const errorList = result.failedFiles.map(fn => `${fn} was not uploaded`).join(', ');
109
+ setToastType('warning');
110
+ setToastMessage(`${result.successCount} file${result.successCount !== 1 ? 's' : ''} successfully uploaded! Errors: ${errorList}`);
111
+ } else if (result.successCount > 0) {
112
+ // All files succeeded
113
+ setToastType('success');
114
+ setToastMessage(`${result.successCount} file${result.successCount !== 1 ? 's' : ''} uploaded!`);
115
+ }
116
+ setIsToastVisible(true);
117
+ }, []);
118
+
119
+ return (
120
+ <div className={styles.sidebar}>
121
+ <div className={styles.userInfo}>
122
+ <h3 className={styles.userTitle}>
123
+ {`${user.displayName?.split(' ')[0] || 'User'}'s Striae`}
124
+ </h3>
125
+ <div className={styles.userActions}>
126
+ <button
127
+ onClick={() => setIsProfileModalOpen(true)}
128
+ className={styles.profileButton}
129
+ disabled={isUploading}
130
+ title={isUploading ? 'Cannot manage profile while uploading files' : undefined}
131
+ >
132
+ Manage Profile
133
+ </button>
134
+ <SignOut disabled={isUploading} />
135
+ </div>
136
+ </div>
137
+ <ManageProfile
138
+ isOpen={isProfileModalOpen}
139
+ onClose={() => setIsProfileModalOpen(false)}
140
+ />
141
+ <CaseImport
142
+ isOpen={isImportModalOpen}
143
+ onClose={() => setIsImportModalOpen(false)}
144
+ onImportComplete={handleImportComplete}
145
+ />
146
+ <HashUtility
147
+ isOpen={isHashModalOpen}
148
+ onClose={() => setIsHashModalOpen(false)}
149
+ />
150
+ {showNotes ? (
151
+ <NotesSidebar
152
+ currentCase={currentCase}
153
+ onReturn={() => setShowNotes(false)}
154
+ user={user}
155
+ imageId={imageId || ''}
156
+ onAnnotationRefresh={onAnnotationRefresh}
157
+ originalFileName={files.find(file => file.id === imageId)?.originalFilename}
158
+ isUploading={isUploading}
159
+ />
160
+ ) : (
161
+ <>
162
+ <CaseSidebar
163
+ user={user}
164
+ onImageSelect={onImageSelect}
165
+ onCaseChange={onCaseChange}
166
+ currentCase={currentCase}
167
+ setCurrentCase={setCurrentCase}
168
+ imageLoaded={imageLoaded}
169
+ setImageLoaded={setImageLoaded}
170
+ files={files}
171
+ setFiles={setFiles}
172
+ caseNumber={caseNumber}
173
+ setCaseNumber={setCaseNumber}
174
+ error={error}
175
+ setError={setError}
176
+ successAction={successAction}
177
+ setSuccessAction={setSuccessAction}
178
+ onNotesClick={() => setShowNotes(true)}
179
+ isReadOnly={isReadOnly}
180
+ isConfirmed={isConfirmed}
181
+ confirmationSaveVersion={confirmationSaveVersion}
182
+ selectedFileId={imageId}
183
+ isUploading={isUploading}
184
+ onUploadStatusChange={setIsUploading}
185
+ onUploadComplete={handleUploadComplete}
186
+ />
187
+ <div className={styles.importSection}>
188
+ <button
189
+ onClick={() => setIsImportModalOpen(true)}
190
+ className={styles.importButton}
191
+ disabled={isUploading}
192
+ title={isUploading ? 'Cannot import while uploading files' : undefined}
193
+ >
194
+ Import/Clear RO Case
195
+ </button>
196
+ <button
197
+ onClick={() => setIsHashModalOpen(true)}
198
+ className={styles.hashButton}
199
+ disabled={isUploading}
200
+ title={isUploading ? 'Cannot open hash utility while uploading files' : undefined}
201
+ >
202
+ Hash Utility
203
+ </button>
204
+ </div>
205
+ </>
206
+ )}
207
+ <Toast
208
+ message={toastMessage}
209
+ type={toastType}
210
+ isVisible={isToastVisible}
211
+ onClose={() => setIsToastVisible(false)}
212
+ />
213
+ </div>
214
+ );
215
+ };
@@ -0,0 +1,123 @@
1
+ /* Image Upload Zone */
2
+ .imageUploadZone {
3
+ margin: 1rem 0;
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 0.5rem;
7
+ }
8
+
9
+ .imageUploadZone label {
10
+ font-size: 0.9rem;
11
+ font-weight: 500;
12
+ color: #000000;
13
+ margin-bottom: 0.5rem;
14
+ }
15
+
16
+ .fileInput {
17
+ width: 100%;
18
+ padding: 0.5rem;
19
+ border: 1px solid #dee2e6;
20
+ border-radius: 4px;
21
+ font-size: 0.875rem;
22
+ box-sizing: border-box;
23
+ cursor: pointer;
24
+ }
25
+
26
+ .fileInput:disabled {
27
+ background-color: #f5f5f5;
28
+ cursor: not-allowed;
29
+ opacity: 0.6;
30
+ }
31
+
32
+ .dragDropHint {
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ padding: 2rem 1rem;
37
+ border: 2px dashed #dee2e6;
38
+ border-radius: 6px;
39
+ background-color: #fafafa;
40
+ transition: all 0.2s ease;
41
+ pointer-events: none;
42
+ min-height: 100px;
43
+ }
44
+
45
+ .dragDropText {
46
+ margin: 0;
47
+ font-size: 0.9rem;
48
+ color: #6c757d;
49
+ text-align: center;
50
+ font-weight: 500;
51
+ }
52
+
53
+ .imageUploadZone.dragActive {
54
+ background-color: rgba(13, 110, 253, 0.05);
55
+ border-radius: 6px;
56
+ }
57
+
58
+ .imageUploadZone.dragActive .dragDropHint {
59
+ border-color: #0d6efd;
60
+ background-color: rgba(13, 110, 253, 0.1);
61
+ box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.1);
62
+ }
63
+
64
+ .imageUploadZone.dragActive .dragDropText {
65
+ color: #0d6efd;
66
+ font-weight: 600;
67
+ }
68
+
69
+ .progressBar {
70
+ width: 100%;
71
+ height: 6px;
72
+ background-color: #e9ecef;
73
+ border-radius: 3px;
74
+ overflow: hidden;
75
+ margin-top: 0.5rem;
76
+ }
77
+
78
+ .progressFill {
79
+ height: 100%;
80
+ background-color: #198754;
81
+ transition: width 0.3s ease;
82
+ }
83
+
84
+ .uploadingText {
85
+ font-size: 0.875rem;
86
+ color: var(--textBody);
87
+ font-weight: 500;
88
+ }
89
+
90
+ .uploadStatusContainer {
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ gap: 0.5rem;
95
+ width: 100%;
96
+ margin-top: 0.25rem;
97
+ }
98
+
99
+ .fileCountText {
100
+ font-size: 0.75rem;
101
+ color: var(--textLight);
102
+ font-weight: 400;
103
+ }
104
+
105
+ .currentFileName {
106
+ font-size: 0.875rem;
107
+ color: var(--textBody);
108
+ margin: 0.25rem 0 0 0;
109
+ overflow: hidden;
110
+ text-overflow: ellipsis;
111
+ white-space: nowrap;
112
+ max-width: 100%;
113
+ }
114
+
115
+ /* Status Messages */
116
+ .error {
117
+ font-size: 0.875rem;
118
+ color: #dc3545;
119
+ background-color: rgba(220, 53, 69, 0.1);
120
+ margin-top: 0.5rem;
121
+ padding: 0.5rem;
122
+ border-radius: 4px;
123
+ }