SCGR commited on
Commit
362f973
·
1 Parent(s): 7b68420
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. frontend/src/App.tsx +0 -2
  2. frontend/src/components/HeaderNav.tsx +0 -2
  3. frontend/src/pages/DemoPage.tsx +0 -953
  4. frontend/src/pages/ExplorePage/ExplorePage.tsx +12 -39
  5. frontend/src/pages/MapDetailsPage/MapDetailPage.tsx +107 -173
  6. frontend/src/pages/UploadPage/UploadPage.tsx +3 -53
  7. go-web-app-develop/.changeset/README.md +0 -8
  8. go-web-app-develop/.changeset/config.json +0 -15
  9. go-web-app-develop/.changeset/lovely-kids-boil.md +0 -5
  10. go-web-app-develop/.changeset/pre.json +0 -15
  11. go-web-app-develop/.changeset/solid-clubs-care.md +0 -8
  12. go-web-app-develop/.changeset/sweet-gifts-cheer.md +0 -9
  13. go-web-app-develop/.changeset/whole-lions-guess.md +0 -7
  14. go-web-app-develop/.dockerignore +0 -148
  15. go-web-app-develop/.github/ISSUE_TEMPLATE/01_bug_report.yml +0 -92
  16. go-web-app-develop/.github/ISSUE_TEMPLATE/02_feature_request.yml +0 -39
  17. go-web-app-develop/.github/ISSUE_TEMPLATE/03_epic_request.yml +0 -37
  18. go-web-app-develop/.github/ISSUE_TEMPLATE/config.yml +0 -5
  19. go-web-app-develop/.github/dependabot.yml +0 -27
  20. go-web-app-develop/.github/pull_request_template.md +0 -30
  21. go-web-app-develop/.github/workflows/add-issue-to-backlog.yml +0 -16
  22. go-web-app-develop/.github/workflows/chromatic.yml +0 -127
  23. go-web-app-develop/.github/workflows/ci.yml +0 -304
  24. go-web-app-develop/.github/workflows/publish-nginx-serve.yml +0 -147
  25. go-web-app-develop/.github/workflows/publish-storybook-nginx-serve.yml +0 -127
  26. go-web-app-develop/.gitignore +0 -43
  27. go-web-app-develop/.npmrc +0 -1
  28. go-web-app-develop/COLLABORATING.md +0 -18
  29. go-web-app-develop/CONTRIBUTING.md +0 -81
  30. go-web-app-develop/LICENSE +0 -21
  31. go-web-app-develop/README.md +0 -117
  32. go-web-app-develop/app/CHANGELOG.md +0 -729
  33. go-web-app-develop/app/env.ts +0 -29
  34. go-web-app-develop/app/eslint.config.js +0 -165
  35. go-web-app-develop/app/index.html +0 -69
  36. go-web-app-develop/app/package.json +0 -119
  37. go-web-app-develop/app/postcss.config.cjs +0 -8
  38. go-web-app-develop/app/public/go-icon.svg +0 -4
  39. go-web-app-develop/app/scripts/translatte/README.md +0 -59
  40. go-web-app-develop/app/scripts/translatte/commands/applyMigrations.test.ts +0 -104
  41. go-web-app-develop/app/scripts/translatte/commands/applyMigrations.ts +0 -177
  42. go-web-app-develop/app/scripts/translatte/commands/exportMigration.ts +0 -62
  43. go-web-app-develop/app/scripts/translatte/commands/generateMigration.test.ts +0 -102
  44. go-web-app-develop/app/scripts/translatte/commands/generateMigration.ts +0 -195
  45. go-web-app-develop/app/scripts/translatte/commands/lint.test.ts +0 -75
  46. go-web-app-develop/app/scripts/translatte/commands/lint.ts +0 -106
  47. go-web-app-develop/app/scripts/translatte/commands/listMigrations.test.ts +0 -48
  48. go-web-app-develop/app/scripts/translatte/commands/listMigrations.ts +0 -11
  49. go-web-app-develop/app/scripts/translatte/commands/mergeMigrations.test.ts +0 -371
  50. go-web-app-develop/app/scripts/translatte/commands/mergeMigrations.ts +0 -220
frontend/src/App.tsx CHANGED
@@ -8,7 +8,6 @@ import AnalyticsPage from './pages/AnalyticsPage';
8
  import ExplorePage from './pages/ExplorePage';
9
  import HelpPage from './pages/HelpPage';
10
  import MapDetailPage from './pages/MapDetailsPage';
11
- import DemoPage from './pages/DemoPage';
12
 
13
  import AdminPage from './pages/AdminPage/AdminPage';
14
  import { FilterProvider } from './contexts/FilterContext';
@@ -23,7 +22,6 @@ const router = createHashRouter([
23
  { path: '/analytics', element: <AnalyticsPage /> },
24
  { path: '/explore', element: <ExplorePage /> },
25
  { path: '/help', element: <HelpPage /> },
26
- { path: '/demo', element: <DemoPage /> },
27
 
28
  { path: '/admin', element: <AdminPage /> },
29
  { path: '/map/:mapId', element: <MapDetailPage /> },
 
8
  import ExplorePage from './pages/ExplorePage';
9
  import HelpPage from './pages/HelpPage';
10
  import MapDetailPage from './pages/MapDetailsPage';
 
11
 
12
  import AdminPage from './pages/AdminPage/AdminPage';
13
  import { FilterProvider } from './contexts/FilterContext';
 
22
  { path: '/analytics', element: <AnalyticsPage /> },
23
  { path: '/explore', element: <ExplorePage /> },
24
  { path: '/help', element: <HelpPage /> },
 
25
 
26
  { path: '/admin', element: <AdminPage /> },
27
  { path: '/map/:mapId', element: <MapDetailPage /> },
frontend/src/components/HeaderNav.tsx CHANGED
@@ -67,7 +67,6 @@ export default function HeaderNav() {
67
  (to === '/upload' && location.pathname === '/') ||
68
  (to === '/explore' && location.pathname.startsWith('/map/'));
69
 
70
- // Don't allow navigation to upload or home when already on upload page
71
  const isUploadPage = location.pathname === "/upload" || location.pathname === "/";
72
  const isUploadOrHomeNav = to === "/upload" || to === "/";
73
 
@@ -84,7 +83,6 @@ export default function HeaderNav() {
84
  : 'hover:bg-white hover:shadow-md hover:scale-105'
85
  }`}
86
  onClick={() => {
87
- // Prevent navigation to upload/home when already on upload page
88
  if (isUploadPage && isUploadOrHomeNav) {
89
  return;
90
  }
 
67
  (to === '/upload' && location.pathname === '/') ||
68
  (to === '/explore' && location.pathname.startsWith('/map/'));
69
 
 
70
  const isUploadPage = location.pathname === "/upload" || location.pathname === "/";
71
  const isUploadOrHomeNav = to === "/upload" || to === "/";
72
 
 
83
  : 'hover:bg-white hover:shadow-md hover:scale-105'
84
  }`}
85
  onClick={() => {
 
86
  if (isUploadPage && isUploadOrHomeNav) {
87
  return;
88
  }
frontend/src/pages/DemoPage.tsx DELETED
@@ -1,953 +0,0 @@
1
- import { useState } from 'react';
2
- import {
3
- PageContainer,
4
- Heading,
5
- Button,
6
- TextInput,
7
- SelectInput,
8
- MultiSelectInput,
9
- SearchSelectInput,
10
- SearchMultiSelectInput,
11
- TextArea,
12
- Checkbox,
13
- Switch,
14
- DateInput,
15
- NumberInput,
16
- PasswordInput,
17
- RawFileInput,
18
- Container,
19
- Spinner,
20
- ProgressBar,
21
- StackedProgressBar,
22
- KeyFigure,
23
- PieChart,
24
- BarChart,
25
- IconButton,
26
- ConfirmButton,
27
- InputLabel,
28
- InputHint,
29
- InputSection,
30
- BooleanInput,
31
- BooleanOutput,
32
- DateOutput,
33
- NumberOutput,
34
- TextOutput,
35
- Footer,
36
- NavigationTabList,
37
- SegmentInput,
38
- BlockLoading,
39
- } from '@ifrc-go/ui';
40
- import {
41
- UploadCloudLineIcon,
42
- SearchLineIcon,
43
- ChevronDownLineIcon,
44
- CloseLineIcon,
45
- EditLineIcon,
46
- DeleteBinLineIcon,
47
- DownloadLineIcon,
48
- ShareLineIcon,
49
- LocationIcon,
50
- CheckLineIcon,
51
- AlertLineIcon,
52
- InfoIcon,
53
- } from '@ifrc-go/icons';
54
-
55
- export default function DemoPage() {
56
- const [showModal, setShowModal] = useState(false);
57
- const [showPopup, setShowPopup] = useState(false);
58
- const [loading, setLoading] = useState(false);
59
- const [textValue, setTextValue] = useState('');
60
- const [selectValue, setSelectValue] = useState('');
61
- const [multiSelectValue, setMultiSelectValue] = useState<string[]>([]);
62
- const [checkboxValue, setCheckboxValue] = useState(false);
63
- const [radioValue, setRadioValue] = useState('option1');
64
- const [switchValue, setSwitchValue] = useState(false);
65
- const [dateValue, setDateValue] = useState<string>('');
66
- const [numberValue, setNumberValue] = useState<number | undefined>();
67
- const [passwordValue, setPasswordValue] = useState('');
68
- const [booleanValue, setBooleanValue] = useState(false);
69
- const [segmentValue, setSegmentValue] = useState('option1');
70
-
71
-
72
- const dummyOptions = [
73
- { key: 'option1', label: 'Option 1' },
74
- { key: 'option2', label: 'Option 2' },
75
- { key: 'option3', label: 'Option 3' },
76
- { key: 'option4', label: 'Option 4' },
77
- ];
78
-
79
- const dummyCountries = [
80
- { c_code: 'US', label: 'United States', r_code: 'NAM' },
81
- { c_code: 'CA', label: 'Canada', r_code: 'NAM' },
82
- { c_code: 'MX', label: 'Mexico', r_code: 'NAM' },
83
- { c_code: 'BR', label: 'Brazil', r_code: 'SAM' },
84
- { c_code: 'AR', label: 'Argentina', r_code: 'SAM' },
85
- { c_code: 'UK', label: 'United Kingdom', r_code: 'EUR' },
86
- { c_code: 'DE', label: 'Germany', r_code: 'EUR' },
87
- { c_code: 'FR', label: 'France', r_code: 'EUR' },
88
- ];
89
-
90
-
91
-
92
- const dummyChartData = [
93
- { name: 'Red Cross', value: 45 },
94
- { name: 'UNICEF', value: 30 },
95
- { name: 'WHO', value: 15 },
96
- { name: 'WFP', value: 10 },
97
- ];
98
-
99
-
100
-
101
- const dummyBarData = [
102
- { name: 'Q1', value: 100 },
103
- { name: 'Q2', value: 150 },
104
- { name: 'Q3', value: 120 },
105
- { name: 'Q4', value: 180 },
106
- ];
107
-
108
- const handleLoading = () => {
109
- setLoading(true);
110
- setTimeout(() => setLoading(false), 2000);
111
- };
112
-
113
- const handleTextChange = (value: string | undefined) => {
114
- setTextValue(value || '');
115
- };
116
-
117
- const handlePasswordChange = (value: string | undefined) => {
118
- setPasswordValue(value || '');
119
- };
120
-
121
- const handleNumberChange = (value: number | undefined) => {
122
- setNumberValue(value);
123
- };
124
-
125
- const handleDateChange = (value: string | undefined) => {
126
- setDateValue(value || '');
127
- };
128
-
129
- const handleSelectChange = (value: string | undefined) => {
130
- setSelectValue(value || '');
131
- };
132
-
133
- const handleMultiSelectChange = (value: string[]) => {
134
- setMultiSelectValue(value);
135
- };
136
-
137
- const handleCheckboxChange = (value: boolean) => {
138
- setCheckboxValue(value);
139
- };
140
-
141
- const handleRadioChange = (value: string) => {
142
- setRadioValue(value);
143
- };
144
-
145
- const handleSwitchChange = (value: boolean) => {
146
- setSwitchValue(value);
147
- };
148
-
149
- const handleBooleanChange = (value: boolean) => {
150
- setBooleanValue(value);
151
- };
152
-
153
- const handleSegmentChange = (value: string) => {
154
- setSegmentValue(value);
155
- };
156
-
157
- return (
158
- <PageContainer>
159
- <div className="space-y-8">
160
- {/* Header Section */}
161
- <Container heading="Navigation & Header Components" headingLevel={2} withHeaderBorder withInternalPadding>
162
- <div className="space-y-6">
163
- {/* Navigation Tabs */}
164
- <div>
165
- <Heading level={3} className="mb-4">Navigation Tab List</Heading>
166
- <NavigationTabList variant="primary">
167
- <Button name="upload" variant="primary">Upload</Button>
168
- <Button name="analytics" variant="secondary">Analytics</Button>
169
- <Button name="explore" variant="secondary">Explore</Button>
170
- <Button name="help" variant="secondary">Help</Button>
171
- </NavigationTabList>
172
- </div>
173
-
174
- {/* Top Banner */}
175
- <div>
176
- <Heading level={3} className="mb-4">Top Banner</Heading>
177
- <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
178
- <div className="flex justify-between items-start">
179
- <div>
180
- <h4 className="font-semibold text-blue-900">Important Notice</h4>
181
- <p className="text-blue-700 mt-1">This is a top banner component for important announcements.</p>
182
- </div>
183
- <Button name="dismiss" variant="secondary" size={1}>
184
- Dismiss
185
- </Button>
186
- </div>
187
- </div>
188
- </div>
189
-
190
- {/* Breadcrumbs */}
191
- <div>
192
- <Heading level={3} className="mb-4">Breadcrumbs</Heading>
193
- <nav className="flex" aria-label="Breadcrumb">
194
- <ol className="flex items-center space-x-2">
195
- <li>
196
- <a href="/" className="text-gray-500 hover:text-gray-700">Home</a>
197
- </li>
198
- <li>
199
- <span className="mx-2 text-gray-400">/</span>
200
- </li>
201
- <li>
202
- <a href="/analytics" className="text-gray-500 hover:text-gray-700">Analytics</a>
203
- </li>
204
- <li>
205
- <span className="mx-2 text-gray-400">/</span>
206
- </li>
207
- <li>
208
- <span className="text-gray-900">Reports</span>
209
- </li>
210
- </ol>
211
- </nav>
212
- </div>
213
- </div>
214
- </Container>
215
-
216
- {/* Basic Components */}
217
- <Container heading="Basic Components" headingLevel={2} withHeaderBorder withInternalPadding>
218
- <div className="space-y-6">
219
- {/* Buttons */}
220
- <div>
221
- <Heading level={3} className="mb-4">Buttons</Heading>
222
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
223
- <Button name="primary" variant="primary">Primary Button</Button>
224
- <Button name="secondary" variant="secondary">Secondary Button</Button>
225
- <Button name="tertiary" variant="tertiary">Tertiary Button</Button>
226
- <Button name="disabled" disabled>Disabled Button</Button>
227
- <Button name="loading" onClick={handleLoading} disabled={loading}>
228
- {loading ? <Spinner /> : 'Loading Button'}
229
- </Button>
230
- <ConfirmButton name="confirm" onConfirm={() => alert('Confirmed!')}>
231
- Confirm Button
232
- </ConfirmButton>
233
- <Button name="with-icon" variant="primary">
234
- <UploadCloudLineIcon className="w-4 h-4 mr-2" />
235
- Upload File
236
- </Button>
237
- </div>
238
- </div>
239
-
240
- {/* Icon Buttons */}
241
- <div>
242
- <Heading level={3} className="mb-4">Icon Buttons</Heading>
243
- <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
244
- <IconButton name="upload" variant="primary" title="Upload" ariaLabel="Upload">
245
- <UploadCloudLineIcon />
246
- </IconButton>
247
- <IconButton name="search" variant="secondary" title="Search" ariaLabel="Search">
248
- <SearchLineIcon />
249
- </IconButton>
250
- <IconButton name="edit" variant="tertiary" title="Edit" ariaLabel="Edit">
251
- <EditLineIcon />
252
- </IconButton>
253
- <IconButton name="delete" variant="tertiary" title="Delete" ariaLabel="Delete">
254
- <DeleteBinLineIcon />
255
- </IconButton>
256
- <IconButton name="download" variant="tertiary" title="Download" ariaLabel="Download">
257
- <DownloadLineIcon />
258
- </IconButton>
259
- <IconButton name="share" variant="tertiary" title="Share" ariaLabel="Share">
260
- <ShareLineIcon />
261
- </IconButton>
262
- </div>
263
- </div>
264
-
265
- {/* Chips */}
266
- <div>
267
- <h3 className="text-lg font-semibold mb-4">Chips</h3>
268
- <div className="flex flex-wrap gap-2">
269
- <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
270
- Primary Chip
271
- </span>
272
- <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
273
- Secondary Chip
274
- </span>
275
- <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
276
- Tertiary Chip
277
- </span>
278
- </div>
279
- </div>
280
-
281
- {/* Tooltips */}
282
- <div>
283
- <h3 className="text-lg font-semibold mb-4">Tooltips</h3>
284
- <div className="flex gap-4">
285
- <div className="relative group">
286
- <Button name="tooltip">Hover me</Button>
287
- <div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 text-xs text-white bg-gray-900 rounded opacity-0 group-hover:opacity-100 transition-opacity">
288
- This is a tooltip
289
- </div>
290
- </div>
291
- <div className="relative group">
292
- <IconButton name="tooltip-icon" variant="tertiary" title="Info" ariaLabel="Info">
293
- <InfoIcon />
294
- </IconButton>
295
- <div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 text-xs text-white bg-gray-900 rounded opacity-0 group-hover:opacity-100 transition-opacity">
296
- Another tooltip
297
- </div>
298
- </div>
299
- </div>
300
- </div>
301
- </div>
302
- </Container>
303
-
304
- {/* Form Elements */}
305
- <Container heading="Form Elements" headingLevel={2} withHeaderBorder withInternalPadding>
306
- <div className="space-y-6">
307
- {/* Text Inputs */}
308
- <div>
309
- <h3 className="text-lg font-semibold mb-4">Text Inputs</h3>
310
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
311
- <InputSection>
312
- <InputLabel>Text Input</InputLabel>
313
- <TextInput
314
- name="text"
315
- value={textValue}
316
- onChange={handleTextChange}
317
- placeholder="Enter text..."
318
- />
319
- <InputHint>This is a hint text</InputHint>
320
- </InputSection>
321
-
322
- <InputSection>
323
- <InputLabel>Password Input</InputLabel>
324
- <PasswordInput
325
- name="password"
326
- value={passwordValue}
327
- onChange={handlePasswordChange}
328
- placeholder="Enter password..."
329
- />
330
- </InputSection>
331
-
332
- <InputSection>
333
- <InputLabel>Number Input</InputLabel>
334
- <NumberInput
335
- name="number"
336
- value={numberValue}
337
- onChange={handleNumberChange}
338
- placeholder="Enter number..."
339
- />
340
- </InputSection>
341
-
342
- <InputSection>
343
- <InputLabel>Date Input</InputLabel>
344
- <DateInput
345
- name="date"
346
- value={dateValue}
347
- onChange={handleDateChange}
348
- placeholder="Select date..."
349
- />
350
- </InputSection>
351
-
352
- <InputSection>
353
- <InputLabel>Text Area</InputLabel>
354
- <TextArea
355
- name="textarea"
356
- value=""
357
- onChange={() => {}}
358
- placeholder="Enter long text..."
359
- rows={4}
360
- />
361
- </InputSection>
362
-
363
- <InputSection>
364
- <InputLabel>File Input</InputLabel>
365
- <RawFileInput
366
- name="file"
367
- accept="image/*"
368
- onChange={() => {}}
369
- />
370
- </InputSection>
371
- </div>
372
- </div>
373
-
374
- {/* Select Inputs */}
375
- <div>
376
- <h3 className="text-lg font-semibold mb-4">Select Inputs</h3>
377
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
378
- <InputSection>
379
- <InputLabel>Select Input</InputLabel>
380
- <SelectInput
381
- name="select"
382
- value={selectValue}
383
- onChange={handleSelectChange}
384
- options={dummyOptions}
385
- keySelector={(o) => o.key}
386
- labelSelector={(o) => o.label}
387
- placeholder="Select an option..."
388
- />
389
- </InputSection>
390
-
391
- <InputSection>
392
- <InputLabel>Multi Select Input</InputLabel>
393
- <MultiSelectInput
394
- name="multiselect"
395
- value={multiSelectValue}
396
- onChange={handleMultiSelectChange}
397
- options={dummyCountries}
398
- keySelector={(o) => o.c_code}
399
- labelSelector={(o) => o.label}
400
- placeholder="Select countries..."
401
- />
402
- </InputSection>
403
-
404
- <InputSection>
405
- <InputLabel>Search Select Input</InputLabel>
406
- <SearchSelectInput
407
- name="searchselect"
408
- value=""
409
- onChange={() => {}}
410
- options={dummyCountries}
411
- keySelector={(o) => o.c_code}
412
- labelSelector={(o) => o.label}
413
- placeholder="Search countries..."
414
- selectedOnTop
415
- />
416
- </InputSection>
417
-
418
- <InputSection>
419
- <InputLabel>Search Multi Select Input</InputLabel>
420
- <SearchMultiSelectInput
421
- name="searchmultiselect"
422
- value={[]}
423
- onChange={() => {}}
424
- options={dummyCountries}
425
- keySelector={(o) => o.c_code}
426
- labelSelector={(o) => o.label}
427
- placeholder="Search and select countries..."
428
- selectedOnTop
429
- />
430
- </InputSection>
431
- </div>
432
- </div>
433
-
434
- {/* Checkboxes & Radios */}
435
- <div>
436
- <h3 className="text-lg font-semibold mb-4">Checkboxes & Radios</h3>
437
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
438
- <InputSection>
439
- <InputLabel>Checkbox</InputLabel>
440
- <Checkbox
441
- name="checkbox"
442
- value={checkboxValue}
443
- onChange={handleCheckboxChange}
444
- label="Accept terms and conditions"
445
- />
446
- </InputSection>
447
-
448
- <InputSection>
449
- <InputLabel>Radio Buttons</InputLabel>
450
- <div className="space-y-2">
451
- <label className="flex items-center">
452
- <input
453
- type="radio"
454
- name="radio"
455
- value="option1"
456
- checked={radioValue === 'option1'}
457
- onChange={(e) => handleRadioChange(e.target.value)}
458
- className="mr-2"
459
- />
460
- <span className="text-sm">Option 1</span>
461
- </label>
462
- <label className="flex items-center">
463
- <input
464
- type="radio"
465
- name="radio"
466
- value="option2"
467
- checked={radioValue === 'option2'}
468
- onChange={(e) => handleRadioChange(e.target.value)}
469
- className="mr-2"
470
- />
471
- <span className="text-sm">Option 2</span>
472
- </label>
473
- <label className="flex items-center">
474
- <input
475
- type="radio"
476
- name="radio"
477
- value="option3"
478
- checked={radioValue === 'option3'}
479
- onChange={(e) => handleRadioChange(e.target.value)}
480
- className="mr-2"
481
- />
482
- <span className="text-sm">Option 3</span>
483
- </label>
484
- </div>
485
- </InputSection>
486
-
487
- <InputSection>
488
- <InputLabel>Switch</InputLabel>
489
- <Switch
490
- name="switch"
491
- value={switchValue}
492
- onChange={handleSwitchChange}
493
- label="Enable notifications"
494
- />
495
- </InputSection>
496
-
497
- <InputSection>
498
- <InputLabel>Boolean Input</InputLabel>
499
- <BooleanInput
500
- name="boolean"
501
- value={booleanValue}
502
- onChange={handleBooleanChange}
503
- label="Enable feature"
504
- />
505
- </InputSection>
506
- </div>
507
- </div>
508
-
509
- {/* Segment Input */}
510
- <div>
511
- <h3 className="text-lg font-semibold mb-4">Segment Input</h3>
512
- <InputSection>
513
- <InputLabel>Segment Input</InputLabel>
514
- <SegmentInput
515
- name="segment"
516
- value={segmentValue}
517
- onChange={handleSegmentChange}
518
- options={dummyOptions}
519
- keySelector={(o) => o.key}
520
- labelSelector={(o) => o.label}
521
- />
522
- </InputSection>
523
- </div>
524
- </div>
525
- </Container>
526
-
527
- {/* Data Display */}
528
- <Container heading="Data Display" headingLevel={2} withHeaderBorder withInternalPadding>
529
- <div className="space-y-6">
530
- {/* Key Figures */}
531
- <div>
532
- <h3 className="text-lg font-semibold mb-4">Key Figures</h3>
533
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
534
- <KeyFigure value={1234} label="Total Users" />
535
- <KeyFigure value={567} label="Active Projects" />
536
- <KeyFigure value={89} label="Countries" />
537
- <KeyFigure value={12.5} label="Growth Rate" suffix="%" />
538
- </div>
539
- </div>
540
-
541
- {/* Charts */}
542
- <div>
543
- <h3 className="text-lg font-semibold mb-4">Charts</h3>
544
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
545
- <div>
546
- <h4 className="text-md font-semibold mb-4">Pie Chart</h4>
547
- <PieChart
548
- data={dummyChartData}
549
- valueSelector={(d) => d.value}
550
- labelSelector={(d) => d.name}
551
- keySelector={(d) => d.name}
552
- colorSelector={() => '#dc2626'}
553
- showPercentageInLegend
554
- />
555
- </div>
556
- <div>
557
- <h4 className="text-md font-semibold mb-4">Bar Chart</h4>
558
- <BarChart
559
- data={dummyBarData}
560
- valueSelector={(d) => d.value}
561
- labelSelector={(d) => d.name}
562
- keySelector={(d) => d.name}
563
- />
564
- </div>
565
- </div>
566
- <div className="mt-8">
567
- <h4 className="text-md font-semibold mb-4">Time Series Chart</h4>
568
- <div className="h-64 bg-gray-50 rounded border-2 border-dashed border-gray-400 flex items-center justify-center">
569
- <div className="text-center">
570
- <div className="w-16 h-16 mx-auto mb-2 bg-gray-200 rounded flex items-center justify-center">
571
- <div className="w-8 h-8 bg-gray-400 rounded"></div>
572
- </div>
573
- <p className="text-gray-600">Time Series Chart Component</p>
574
- <p className="text-sm text-gray-500">This would render a time series chart with data points over time</p>
575
- </div>
576
- </div>
577
- </div>
578
- </div>
579
-
580
-
581
-
582
- {/* Lists */}
583
- <div>
584
- <h3 className="text-lg font-semibold mb-4">Lists</h3>
585
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
586
- <div>
587
- <h4 className="text-md font-semibold mb-4">Basic List</h4>
588
- <ul className="space-y-2">
589
- {dummyCountries.slice(0, 5).map((country) => (
590
- <li key={country.c_code} className="flex items-center justify-between p-2 bg-gray-50 rounded">
591
- <span>{country.label}</span>
592
- <span className="text-sm text-gray-500">{country.c_code}</span>
593
- </li>
594
- ))}
595
- </ul>
596
- </div>
597
- <div>
598
- <h4 className="text-md font-semibold mb-4">Raw List</h4>
599
- <ul className="space-y-1">
600
- {dummyCountries.slice(0, 5).map((country) => (
601
- <li key={country.c_code} className="text-sm">
602
- {country.label}
603
- </li>
604
- ))}
605
- </ul>
606
- </div>
607
- </div>
608
- </div>
609
-
610
- {/* Output Components */}
611
- <div>
612
- <h3 className="text-lg font-semibold mb-4">Output Components</h3>
613
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
614
- <div>
615
- <h4 className="text-md font-semibold mb-4">Text Output</h4>
616
- <TextOutput value="This is some text output" />
617
- </div>
618
- <div>
619
- <h4 className="text-md font-semibold mb-4">Number Output</h4>
620
- <NumberOutput value={1234.56} />
621
- </div>
622
- <div>
623
- <h4 className="text-md font-semibold mb-4">Date Output</h4>
624
- <DateOutput value={new Date()} />
625
- </div>
626
- <div>
627
- <h4 className="text-md font-semibold mb-4">Boolean Output</h4>
628
- <BooleanOutput value={true} />
629
- </div>
630
- </div>
631
- </div>
632
- </div>
633
- </Container>
634
-
635
- {/* Feedback */}
636
- <Container heading="Feedback" headingLevel={2} withHeaderBorder withInternalPadding>
637
- <div className="space-y-6">
638
- {/* Alerts & Messages */}
639
- <div>
640
- <h3 className="text-lg font-semibold mb-4">Alerts & Messages</h3>
641
- <div className="space-y-4">
642
- <div className="bg-green-50 border border-green-200 rounded-md p-4">
643
- <div className="flex">
644
- <div className="flex-shrink-0">
645
- <CheckLineIcon className="h-5 w-5 text-green-400" />
646
- </div>
647
- <div className="ml-3">
648
- <p className="text-sm text-green-800">This is a success alert message.</p>
649
- </div>
650
- </div>
651
- </div>
652
- <div className="bg-yellow-50 border border-yellow-200 rounded-md p-4">
653
- <div className="flex">
654
- <div className="flex-shrink-0">
655
- <AlertLineIcon className="h-5 w-5 text-yellow-400" />
656
- </div>
657
- <div className="ml-3">
658
- <p className="text-sm text-yellow-800">This is a warning alert message.</p>
659
- </div>
660
- </div>
661
- </div>
662
- <div className="bg-red-50 border border-red-200 rounded-md p-4">
663
- <div className="flex">
664
- <div className="flex-shrink-0">
665
- <AlertLineIcon className="h-5 w-5 text-red-400" />
666
- </div>
667
- <div className="ml-3">
668
- <p className="text-sm text-red-800">This is an error alert message.</p>
669
- </div>
670
- </div>
671
- </div>
672
- <div className="bg-blue-50 border border-blue-200 rounded-md p-4">
673
- <div className="flex">
674
- <div className="flex-shrink-0">
675
- <InfoIcon className="h-5 w-5 text-blue-400" />
676
- </div>
677
- <div className="ml-3">
678
- <p className="text-sm text-blue-800">This is an info alert message.</p>
679
- </div>
680
- </div>
681
- </div>
682
- <div className="bg-gray-50 border border-gray-200 rounded-md p-4">
683
- <div className="flex">
684
- <div className="flex-shrink-0">
685
- <InfoIcon className="h-5 w-5 text-gray-400" />
686
- </div>
687
- <div className="ml-3">
688
- <h4 className="text-sm font-medium text-gray-800">Information Message</h4>
689
- <p className="text-sm text-gray-600 mt-1">This is a message component with a title.</p>
690
- </div>
691
- </div>
692
- </div>
693
- </div>
694
- </div>
695
-
696
- {/* Progress Bars */}
697
- <div>
698
- <h3 className="text-lg font-semibold mb-4">Progress Bars</h3>
699
- <div className="space-y-4">
700
- <div>
701
- <h4 className="text-sm font-medium mb-2">Basic Progress Bar</h4>
702
- <ProgressBar value={75} totalValue={100} />
703
- </div>
704
- <div>
705
- <h4 className="text-sm font-medium mb-2">Stacked Progress Bar</h4>
706
- <StackedProgressBar
707
- data={[
708
- { key: 'completed', value: 60, color: '#dc2626' },
709
- { key: 'in-progress', value: 25, color: '#f59e0b' },
710
- { key: 'pending', value: 15, color: '#6b7280' },
711
- ]}
712
- valueSelector={(d) => d.value}
713
- labelSelector={(d) => d.key}
714
- colorSelector={(d) => d.color}
715
- />
716
- </div>
717
- </div>
718
- </div>
719
-
720
- {/* Loading States */}
721
- <div>
722
- <h3 className="text-lg font-semibold mb-4">Loading States</h3>
723
- <div className="space-y-4">
724
- <div>
725
- <h4 className="text-sm font-medium mb-2">Spinner</h4>
726
- <Spinner />
727
- </div>
728
- <div>
729
- <h4 className="text-sm font-medium mb-2">Block Loading</h4>
730
- <BlockLoading />
731
- </div>
732
- </div>
733
- </div>
734
-
735
- {/* Modals & Popups */}
736
- <div>
737
- <h3 className="text-lg font-semibold mb-4">Modals & Popups</h3>
738
- <div className="space-y-4">
739
- <Button name="modal" onClick={() => setShowModal(true)}>
740
- Open Modal
741
- </Button>
742
- <Button name="popup" onClick={() => setShowPopup(true)}>
743
- Open Popup
744
- </Button>
745
- <Button name="info-popup" onClick={() => {}}>
746
- Info Popup
747
- </Button>
748
- </div>
749
- </div>
750
-
751
- {/* Dropdown Menu */}
752
- <div>
753
- <h3 className="text-lg font-semibold mb-4">Dropdown Menu</h3>
754
- <div className="relative inline-block text-left">
755
- <Button name="dropdown">
756
- Actions <ChevronDownLineIcon className="w-4 h-4 ml-2" />
757
- </Button>
758
- <div className="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5">
759
- <div className="py-1">
760
- <button className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
761
- <EditLineIcon className="w-4 h-4 mr-2" />
762
- Edit
763
- </button>
764
- <button className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
765
- <DeleteBinLineIcon className="w-4 h-4 mr-2" />
766
- Delete
767
- </button>
768
- <button className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
769
- <DownloadLineIcon className="w-4 h-4 mr-2" />
770
- Download
771
- </button>
772
- </div>
773
- </div>
774
- </div>
775
- </div>
776
- </div>
777
- </Container>
778
-
779
- {/* Layout */}
780
- <Container heading="Layout" headingLevel={2} withHeaderBorder withInternalPadding>
781
- <div className="space-y-6">
782
- {/* Grid System */}
783
- <div>
784
- <h3 className="text-lg font-semibold mb-4">Grid System</h3>
785
- <div className="grid grid-cols-3 gap-4">
786
- <div className="bg-gray-100 p-4 rounded">Grid Item 1</div>
787
- <div className="bg-gray-100 p-4 rounded">Grid Item 2</div>
788
- <div className="bg-gray-100 p-4 rounded">Grid Item 3</div>
789
- <div className="bg-gray-100 p-4 rounded">Grid Item 4</div>
790
- <div className="bg-gray-100 p-4 rounded">Grid Item 5</div>
791
- <div className="bg-gray-100 p-4 rounded">Grid Item 6</div>
792
- </div>
793
- </div>
794
-
795
- {/* Expandable Container */}
796
- <div>
797
- <h3 className="text-lg font-semibold mb-4">Expandable Container</h3>
798
- <div className="border border-gray-200 rounded-lg">
799
- <button className="w-full px-4 py-3 text-left bg-gray-50 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500">
800
- <div className="flex items-center justify-between">
801
- <span className="font-medium">Expandable Section</span>
802
- <ChevronDownLineIcon className="w-5 h-5 text-gray-500" />
803
- </div>
804
- </button>
805
- <div className="px-4 py-3 border-t border-gray-200">
806
- <p>This is the content inside the expandable container. It can contain any components or text.</p>
807
- <div className="mt-4">
808
- <Button name="inside-expandable">Button inside expandable</Button>
809
- </div>
810
- </div>
811
- </div>
812
- </div>
813
-
814
- {/* Tabs */}
815
- <div>
816
- <h3 className="text-lg font-semibold mb-4">Tabs</h3>
817
- <div className="border-b border-gray-200">
818
- <nav className="-mb-px flex space-x-8">
819
- <button className="border-b-2 border-blue-500 py-2 px-1 text-sm font-medium text-blue-600">
820
- Tab 1
821
- </button>
822
- <button className="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 py-2 px-1 text-sm font-medium">
823
- Tab 2
824
- </button>
825
- <button className="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 py-2 px-1 text-sm font-medium">
826
- Tab 3
827
- </button>
828
- </nav>
829
- <div className="mt-4">
830
- <p>Content for tab 1</p>
831
- </div>
832
- </div>
833
- </div>
834
-
835
- {/* Pager */}
836
- <div>
837
- <h3 className="text-lg font-semibold mb-4">Pager</h3>
838
- <div className="flex items-center justify-between">
839
- <div className="flex-1 flex justify-between sm:hidden">
840
- <button className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
841
- Previous
842
- </button>
843
- <button className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
844
- Next
845
- </button>
846
- </div>
847
- <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
848
- <div>
849
- <p className="text-sm text-gray-700">
850
- Showing <span className="font-medium">1</span> to <span className="font-medium">10</span> of <span className="font-medium">97</span> results
851
- </p>
852
- </div>
853
- <div>
854
- <nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
855
- <button className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
856
- Previous
857
- </button>
858
- <button className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
859
- 1
860
- </button>
861
- <button className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-blue-50 text-sm font-medium text-blue-600 hover:bg-blue-100">
862
- 2
863
- </button>
864
- <button className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
865
- 3
866
- </button>
867
- <button className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
868
- Next
869
- </button>
870
- </nav>
871
- </div>
872
- </div>
873
- </div>
874
- </div>
875
- </div>
876
- </Container>
877
-
878
- {/* Maps Section */}
879
- <Container heading="Maps & Geographic Components" headingLevel={2} withHeaderBorder withInternalPadding>
880
- <div className="space-y-6">
881
- <div>
882
- <h3 className="text-lg font-semibold mb-4">Map Container</h3>
883
- <div className="h-64 bg-gray-200 rounded border-2 border-dashed border-gray-400 flex items-center justify-center">
884
- <div className="text-center">
885
- <LocationIcon className="w-12 h-12 mx-auto text-gray-400 mb-2" />
886
- <p className="text-gray-600">Map Container Component</p>
887
- <p className="text-sm text-gray-500">This would render a map with MapContainer, MapSource, MapLayer components</p>
888
- </div>
889
- </div>
890
- </div>
891
-
892
- <div>
893
- <h3 className="text-lg font-semibold mb-4">Map Popup</h3>
894
- <div className="bg-white border rounded-lg p-4 shadow-lg max-w-sm">
895
- <div className="flex justify-between items-start mb-2">
896
- <h4 className="font-semibold">Country Name</h4>
897
- <Button name="close" variant="tertiary" size={1}>
898
- <CloseLineIcon />
899
- </Button>
900
- </div>
901
- <p className="text-sm text-gray-600">This represents a MapPopup component with country information.</p>
902
- </div>
903
- </div>
904
- </div>
905
- </Container>
906
-
907
- {/* Modals */}
908
- {showModal && (
909
- <div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
910
- <div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
911
- <div className="mt-3">
912
- <h3 className="text-lg font-medium text-gray-900 mb-4">Modal Example</h3>
913
- <p className="text-sm text-gray-500">This is a modal dialog. It can contain any content.</p>
914
- <div className="mt-4 flex gap-2">
915
- <Button name="modal-close" onClick={() => setShowModal(false)}>
916
- Close
917
- </Button>
918
- <Button name="modal-action" variant="secondary">
919
- Action
920
- </Button>
921
- </div>
922
- </div>
923
- </div>
924
- </div>
925
- )}
926
-
927
- {showPopup && (
928
- <div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
929
- <div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
930
- <div className="mt-3">
931
- <h3 className="text-lg font-medium text-gray-900 mb-4">Popup Example</h3>
932
- <p className="text-sm text-gray-500">This is a popup. It's similar to a modal but with different styling.</p>
933
- <div className="mt-4">
934
- <Button name="popup-close" onClick={() => setShowPopup(false)}>
935
- Close
936
- </Button>
937
- </div>
938
- </div>
939
- </div>
940
- </div>
941
- )}
942
-
943
- {/* Footer */}
944
- <Footer>
945
- <div className="text-center text-gray-600">
946
- <p>IFRC GO UI Components Demo Page</p>
947
- <p className="text-sm">Built with IFRC GO Design System</p>
948
- </div>
949
- </Footer>
950
- </div>
951
- </PageContainer>
952
- );
953
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/ExplorePage/ExplorePage.tsx CHANGED
@@ -33,7 +33,6 @@ export default function ExplorePage() {
33
  const [view, setView] = useState<'explore' | 'mapDetails'>('explore');
34
  const [captions, setCaptions] = useState<ImageWithCaptionOut[]>([]);
35
 
36
- // Use shared filter context instead of local state
37
  const {
38
  search, setSearch,
39
  srcFilter, setSrcFilter,
@@ -176,7 +175,6 @@ export default function ExplorePage() {
176
  }
177
 
178
  try {
179
- // Create a JSZip instance
180
  const JSZip = (await import('jszip')).default;
181
  const zip = new JSZip();
182
 
@@ -184,13 +182,11 @@ export default function ExplorePage() {
184
  const crisisMaps = images.filter(img => img.image_type === 'crisis_map');
185
  const droneImages = images.filter(img => img.image_type === 'drone_image');
186
 
187
- // Create crisis_maps dataset
188
  if (crisisMaps.length > 0) {
189
  const crisisFolder = zip.folder('crisis_maps_dataset');
190
  const crisisImagesFolder = crisisFolder?.folder('images');
191
 
192
  if (crisisImagesFolder) {
193
- // Download crisis map images and add to zip
194
  const crisisImagePromises = crisisMaps.map(async (image, index) => {
195
  try {
196
  const response = await fetch(image.image_url);
@@ -212,12 +208,10 @@ export default function ExplorePage() {
212
  const successfulCrisisImages = crisisImageResults.filter(result => result.success);
213
 
214
  if (mode === 'fine-tuning') {
215
- // Create train.jsonl, test.jsonl, and val.jsonl with stratified sampling
216
  const crisisTrainData: any[] = [];
217
  const crisisTestData: any[] = [];
218
  const crisisValData: any[] = [];
219
 
220
- // Group crisis images by source for stratified sampling
221
  const crisisImagesBySource = new Map<string, any[]>();
222
  successfulCrisisImages.forEach(result => {
223
  const source = result.image.source || 'unknown';
@@ -227,17 +221,13 @@ export default function ExplorePage() {
227
  crisisImagesBySource.get(source)!.push(result);
228
  });
229
 
230
- // Distribute images from each source proportionally across splits
231
- crisisImagesBySource.forEach((images, source) => {
232
  const totalImages = images.length;
233
  const trainCount = Math.floor(totalImages * (trainSplit / 100));
234
  const testCount = Math.floor(totalImages * (testSplit / 100));
235
- const valCount = totalImages - trainCount - testCount;
236
 
237
- // Shuffle images within each source group for randomness
238
  const shuffledImages = [...images].sort(() => Math.random() - 0.5);
239
 
240
- // Add to train set
241
  crisisTrainData.push(...shuffledImages.slice(0, trainCount).map(result => ({
242
  image: `images/${result.fileName}`,
243
  caption: result.image.edited || result.image.generated || '',
@@ -252,7 +242,6 @@ export default function ExplorePage() {
252
  }
253
  })));
254
 
255
- // Add to test set
256
  crisisTestData.push(...shuffledImages.slice(trainCount, trainCount + testCount).map(result => ({
257
  image: `images/${result.fileName}`,
258
  caption: result.image.edited || result.image.generated || '',
@@ -267,7 +256,6 @@ export default function ExplorePage() {
267
  }
268
  })));
269
 
270
- // Add to validation set
271
  crisisValData.push(...shuffledImages.slice(trainCount + testCount).map(result => ({
272
  image: `images/${result.fileName}`,
273
  caption: result.image.edited || result.image.generated || '',
@@ -290,7 +278,6 @@ export default function ExplorePage() {
290
  crisisFolder.file('val.jsonl', JSON.stringify(crisisValData, null, 2));
291
  }
292
  } else {
293
- // Standard mode: create individual JSON files for each image
294
  successfulCrisisImages.forEach((result, index) => {
295
  const jsonData = {
296
  image: `images/${result.fileName}`,
@@ -320,7 +307,6 @@ export default function ExplorePage() {
320
  const droneImagesFolder = droneFolder?.folder('images');
321
 
322
  if (droneImagesFolder) {
323
- // Download drone images and add to zip
324
  const droneImagePromises = droneImages.map(async (image, index) => {
325
  try {
326
  const response = await fetch(image.image_url);
@@ -342,12 +328,10 @@ export default function ExplorePage() {
342
  const successfulDroneImages = droneImageResults.filter(result => result.success);
343
 
344
  if (mode === 'fine-tuning') {
345
- // Create train.jsonl, test.jsonl, and val.jsonl with stratified sampling
346
  const droneTrainData: any[] = [];
347
  const droneTestData: any[] = [];
348
  const droneValData: any[] = [];
349
 
350
- // Group drone images by event type for stratified sampling
351
  const droneImagesByEventType = new Map<string, any[]>();
352
  successfulDroneImages.forEach(result => {
353
  const eventType = result.image.event_type || 'unknown';
@@ -357,17 +341,13 @@ export default function ExplorePage() {
357
  droneImagesByEventType.get(eventType)!.push(result);
358
  });
359
 
360
- // Distribute images from each event type proportionally across splits
361
- droneImagesByEventType.forEach((images, eventType) => {
362
  const totalImages = images.length;
363
  const trainCount = Math.floor(totalImages * (trainSplit / 100));
364
  const testCount = Math.floor(totalImages * (testSplit / 100));
365
- const valCount = totalImages - trainCount - testCount;
366
 
367
- // Shuffle images within each event type group for randomness
368
  const shuffledImages = [...images].sort(() => Math.random() - 0.5);
369
 
370
- // Add to train set
371
  droneTrainData.push(...shuffledImages.slice(0, trainCount).map(result => ({
372
  image: `images/${result.fileName}`,
373
  caption: result.image.edited || result.image.generated || '',
@@ -382,7 +362,6 @@ export default function ExplorePage() {
382
  }
383
  })));
384
 
385
- // Add to test set
386
  droneTestData.push(...shuffledImages.slice(trainCount, trainCount + testCount).map(result => ({
387
  image: `images/${result.fileName}`,
388
  caption: result.image.edited || result.image.generated || '',
@@ -397,7 +376,6 @@ export default function ExplorePage() {
397
  }
398
  })));
399
 
400
- // Add to validation set
401
  droneValData.push(...shuffledImages.slice(trainCount + testCount).map(result => ({
402
  image: `images/${result.fileName}`,
403
  caption: result.image.edited || result.image.generated || '',
@@ -413,14 +391,12 @@ export default function ExplorePage() {
413
  })));
414
  });
415
 
416
- // Add JSONL files to drone folder
417
  if (droneFolder) {
418
  droneFolder.file('train.jsonl', JSON.stringify(droneTrainData, null, 2));
419
  droneFolder.file('test.jsonl', JSON.stringify(droneTestData, null, 2));
420
  droneFolder.file('val.jsonl', JSON.stringify(droneValData, null, 2));
421
  }
422
  } else {
423
- // Standard mode: create individual JSON files for each image
424
  successfulDroneImages.forEach((result, index) => {
425
  const jsonData = {
426
  image: `images/${result.fileName}`,
@@ -444,7 +420,6 @@ export default function ExplorePage() {
444
  }
445
  }
446
 
447
- // Generate and download zip
448
  const zipBlob = await zip.generateAsync({ type: 'blob' });
449
  const url = URL.createObjectURL(zipBlob);
450
  const link = document.createElement('a');
@@ -489,15 +464,14 @@ export default function ExplorePage() {
489
  <Button
490
  name="export-dataset"
491
  variant="secondary"
492
- onClick={() => {
493
- setShowExportModal(true);
494
- // Skip to export stage if no filters are applied
495
- if (search || srcFilter || catFilter || regionFilter || countryFilter || imageTypeFilter || showReferenceExamples) {
496
- setExportModalStage('filters');
497
- } else {
498
- setExportModalStage('export');
499
- }
500
- }}
501
  >
502
  Export Dataset
503
  </Button>
@@ -834,7 +808,6 @@ export default function ExplorePage() {
834
  const remaining = 100 - newTrain;
835
  if (remaining >= 0) {
836
  setTrainSplit(newTrain);
837
- // Distribute remaining between test and val
838
  if (testSplit + valSplit > remaining) {
839
  setTestSplit(Math.floor(remaining / 2));
840
  setValSplit(remaining - Math.floor(remaining / 2));
@@ -900,7 +873,7 @@ export default function ExplorePage() {
900
  name="crisis-maps"
901
  label={`Crisis Maps (${filtered.filter(img => img.image_type === 'crisis_map').length} images)`}
902
  value={crisisMapsSelected}
903
- onChange={(value, name) => setCrisisMapsSelected(value)}
904
  disabled={isLoadingFilters}
905
  />
906
  </div>
@@ -910,7 +883,7 @@ export default function ExplorePage() {
910
  name="drone-images"
911
  label={`Drone Images (${filtered.filter(img => img.image_type === 'drone_image').length} images)`}
912
  value={droneImagesSelected}
913
- onChange={(value, name) => setDroneImagesSelected(value)}
914
  disabled={isLoadingFilters}
915
  />
916
  </div>
 
33
  const [view, setView] = useState<'explore' | 'mapDetails'>('explore');
34
  const [captions, setCaptions] = useState<ImageWithCaptionOut[]>([]);
35
 
 
36
  const {
37
  search, setSearch,
38
  srcFilter, setSrcFilter,
 
175
  }
176
 
177
  try {
 
178
  const JSZip = (await import('jszip')).default;
179
  const zip = new JSZip();
180
 
 
182
  const crisisMaps = images.filter(img => img.image_type === 'crisis_map');
183
  const droneImages = images.filter(img => img.image_type === 'drone_image');
184
 
 
185
  if (crisisMaps.length > 0) {
186
  const crisisFolder = zip.folder('crisis_maps_dataset');
187
  const crisisImagesFolder = crisisFolder?.folder('images');
188
 
189
  if (crisisImagesFolder) {
 
190
  const crisisImagePromises = crisisMaps.map(async (image, index) => {
191
  try {
192
  const response = await fetch(image.image_url);
 
208
  const successfulCrisisImages = crisisImageResults.filter(result => result.success);
209
 
210
  if (mode === 'fine-tuning') {
 
211
  const crisisTrainData: any[] = [];
212
  const crisisTestData: any[] = [];
213
  const crisisValData: any[] = [];
214
 
 
215
  const crisisImagesBySource = new Map<string, any[]>();
216
  successfulCrisisImages.forEach(result => {
217
  const source = result.image.source || 'unknown';
 
221
  crisisImagesBySource.get(source)!.push(result);
222
  });
223
 
224
+ crisisImagesBySource.forEach((images, _source) => {
 
225
  const totalImages = images.length;
226
  const trainCount = Math.floor(totalImages * (trainSplit / 100));
227
  const testCount = Math.floor(totalImages * (testSplit / 100));
 
228
 
 
229
  const shuffledImages = [...images].sort(() => Math.random() - 0.5);
230
 
 
231
  crisisTrainData.push(...shuffledImages.slice(0, trainCount).map(result => ({
232
  image: `images/${result.fileName}`,
233
  caption: result.image.edited || result.image.generated || '',
 
242
  }
243
  })));
244
 
 
245
  crisisTestData.push(...shuffledImages.slice(trainCount, trainCount + testCount).map(result => ({
246
  image: `images/${result.fileName}`,
247
  caption: result.image.edited || result.image.generated || '',
 
256
  }
257
  })));
258
 
 
259
  crisisValData.push(...shuffledImages.slice(trainCount + testCount).map(result => ({
260
  image: `images/${result.fileName}`,
261
  caption: result.image.edited || result.image.generated || '',
 
278
  crisisFolder.file('val.jsonl', JSON.stringify(crisisValData, null, 2));
279
  }
280
  } else {
 
281
  successfulCrisisImages.forEach((result, index) => {
282
  const jsonData = {
283
  image: `images/${result.fileName}`,
 
307
  const droneImagesFolder = droneFolder?.folder('images');
308
 
309
  if (droneImagesFolder) {
 
310
  const droneImagePromises = droneImages.map(async (image, index) => {
311
  try {
312
  const response = await fetch(image.image_url);
 
328
  const successfulDroneImages = droneImageResults.filter(result => result.success);
329
 
330
  if (mode === 'fine-tuning') {
 
331
  const droneTrainData: any[] = [];
332
  const droneTestData: any[] = [];
333
  const droneValData: any[] = [];
334
 
 
335
  const droneImagesByEventType = new Map<string, any[]>();
336
  successfulDroneImages.forEach(result => {
337
  const eventType = result.image.event_type || 'unknown';
 
341
  droneImagesByEventType.get(eventType)!.push(result);
342
  });
343
 
344
+ droneImagesByEventType.forEach((images, _eventType) => {
 
345
  const totalImages = images.length;
346
  const trainCount = Math.floor(totalImages * (trainSplit / 100));
347
  const testCount = Math.floor(totalImages * (testSplit / 100));
 
348
 
 
349
  const shuffledImages = [...images].sort(() => Math.random() - 0.5);
350
 
 
351
  droneTrainData.push(...shuffledImages.slice(0, trainCount).map(result => ({
352
  image: `images/${result.fileName}`,
353
  caption: result.image.edited || result.image.generated || '',
 
362
  }
363
  })));
364
 
 
365
  droneTestData.push(...shuffledImages.slice(trainCount, trainCount + testCount).map(result => ({
366
  image: `images/${result.fileName}`,
367
  caption: result.image.edited || result.image.generated || '',
 
376
  }
377
  })));
378
 
 
379
  droneValData.push(...shuffledImages.slice(trainCount + testCount).map(result => ({
380
  image: `images/${result.fileName}`,
381
  caption: result.image.edited || result.image.generated || '',
 
391
  })));
392
  });
393
 
 
394
  if (droneFolder) {
395
  droneFolder.file('train.jsonl', JSON.stringify(droneTrainData, null, 2));
396
  droneFolder.file('test.jsonl', JSON.stringify(droneTestData, null, 2));
397
  droneFolder.file('val.jsonl', JSON.stringify(droneValData, null, 2));
398
  }
399
  } else {
 
400
  successfulDroneImages.forEach((result, index) => {
401
  const jsonData = {
402
  image: `images/${result.fileName}`,
 
420
  }
421
  }
422
 
 
423
  const zipBlob = await zip.generateAsync({ type: 'blob' });
424
  const url = URL.createObjectURL(zipBlob);
425
  const link = document.createElement('a');
 
464
  <Button
465
  name="export-dataset"
466
  variant="secondary"
467
+ onClick={() => {
468
+ setShowExportModal(true);
469
+ if (search || srcFilter || catFilter || regionFilter || countryFilter || imageTypeFilter || showReferenceExamples) {
470
+ setExportModalStage('filters');
471
+ } else {
472
+ setExportModalStage('export');
473
+ }
474
+ }}
 
475
  >
476
  Export Dataset
477
  </Button>
 
808
  const remaining = 100 - newTrain;
809
  if (remaining >= 0) {
810
  setTrainSplit(newTrain);
 
811
  if (testSplit + valSplit > remaining) {
812
  setTestSplit(Math.floor(remaining / 2));
813
  setValSplit(remaining - Math.floor(remaining / 2));
 
873
  name="crisis-maps"
874
  label={`Crisis Maps (${filtered.filter(img => img.image_type === 'crisis_map').length} images)`}
875
  value={crisisMapsSelected}
876
+ onChange={(value, _name) => setCrisisMapsSelected(value)}
877
  disabled={isLoadingFilters}
878
  />
879
  </div>
 
883
  name="drone-images"
884
  label={`Drone Images (${filtered.filter(img => img.image_type === 'drone_image').length} images)`}
885
  value={droneImagesSelected}
886
+ onChange={(value, _name) => setDroneImagesSelected(value)}
887
  disabled={isLoadingFilters}
888
  />
889
  </div>
frontend/src/pages/MapDetailsPage/MapDetailPage.tsx CHANGED
@@ -53,7 +53,6 @@ export default function MapDetailPage() {
53
  const [hasNext, setHasNext] = useState(false);
54
  const [isNavigating, setIsNavigating] = useState(false);
55
 
56
- // Add delete confirmation state
57
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
58
  const [showExportModal, setShowExportModal] = useState(false);
59
  const [exportMode, setExportMode] = useState<'standard' | 'fine-tuning'>('standard');
@@ -63,10 +62,8 @@ export default function MapDetailPage() {
63
  const [crisisMapsSelected, setCrisisMapsSelected] = useState(true);
64
  const [droneImagesSelected, setDroneImagesSelected] = useState(true);
65
 
66
- // Add flag to prevent auto-navigation during delete operations
67
  const [isDeleting, setIsDeleting] = useState(false);
68
 
69
- // Use shared filter context instead of local state
70
  const {
71
  search, setSearch,
72
  srcFilter, setSrcFilter,
@@ -116,9 +113,8 @@ export default function MapDetailPage() {
116
 
117
  // Auto-navigate to first matching item when filters change
118
  useEffect(() => {
119
- if (!map || loading || isDeleting) return; // Skip if deleting
120
 
121
- // Check if current map matches current filters
122
  const currentMapMatches = () => {
123
  const matchesSearch = !search ||
124
  map.title?.toLowerCase().includes(search.toLowerCase()) ||
@@ -176,7 +172,6 @@ export default function MapDetailPage() {
176
  if (response.ok) {
177
  const images = await response.json();
178
 
179
- // Filter images based on current filter criteria
180
  const filteredImages = images.filter((img: any) => {
181
  const matchesSearch = !search ||
182
  img.title?.toLowerCase().includes(search.toLowerCase()) ||
@@ -209,45 +204,54 @@ export default function MapDetailPage() {
209
  const navigateToItem = async (direction: 'previous' | 'next') => {
210
  if (isNavigating) return;
211
 
 
212
  try {
213
  const response = await fetch('/api/images');
214
- if (!response.ok) return;
215
-
216
- const images = await response.json();
217
-
218
- // Filter images based on current filter criteria
219
- const filteredImages = images.filter((img: any) => {
220
- const matchesSearch = !search ||
221
- img.title?.toLowerCase().includes(search.toLowerCase()) ||
222
- img.generated?.toLowerCase().includes(search.toLowerCase()) ||
223
- img.source?.toLowerCase().includes(search.toLowerCase()) ||
224
- img.event_type?.toLowerCase().includes(search.toLowerCase());
225
 
226
- const matchesSource = !srcFilter || img.source === srcFilter;
227
- const matchesCategory = !catFilter || img.event_type === catFilter;
228
- const matchesRegion = !regionFilter ||
229
- img.countries?.some((country: any) => country.r_code === regionFilter);
230
- const matchesCountry = !countryFilter ||
231
- img.countries?.some((country: any) => country.c_code === countryFilter);
232
- const matchesImageType = !imageTypeFilter || img.image_type === imageTypeFilter;
233
- const matchesReferenceExamples = !showReferenceExamples || img.starred === true;
 
 
 
 
 
 
 
 
 
 
234
 
235
- return matchesSearch && matchesSource && matchesCategory && matchesRegion && matchesCountry && matchesImageType && matchesReferenceExamples;
236
- });
237
-
238
- const currentIndex = filteredImages.findIndex((img: { image_id: string }) => img.image_id === mapId);
239
-
240
- let targetIndex: number;
241
- if (direction === 'previous') {
242
- targetIndex = currentIndex === 0 ? filteredImages.length - 1 : currentIndex - 1;
243
- } else {
244
- targetIndex = currentIndex === filteredImages.length - 1 ? 0 : currentIndex + 1;
 
 
 
 
 
 
 
 
245
  }
246
-
247
- const targetId = filteredImages[targetIndex].image_id;
248
- navigate(`/map/${targetId}`);
249
  } catch (error) {
250
- console.error('Navigation failed:', error);
 
 
251
  }
252
  };
253
 
@@ -269,7 +273,7 @@ export default function MapDetailPage() {
269
 
270
  const [isGenerating, setIsGenerating] = useState(false);
271
 
272
- // Add delete function
273
  const handleDelete = async () => {
274
  if (!map) return;
275
 
@@ -291,7 +295,6 @@ export default function MapDetailPage() {
291
  });
292
 
293
  if (response.ok) {
294
- // Update local state
295
  setMap(prev => prev ? { ...prev, starred: !prev.starred } : null);
296
  } else {
297
  console.error('Failed to toggle starred status');
@@ -304,101 +307,82 @@ export default function MapDetailPage() {
304
  const confirmDelete = async () => {
305
  if (!map) return;
306
 
307
- setIsDeleting(true); // Set flag to true
308
  try {
309
  console.log('Deleting image with ID:', map.image_id);
310
- const res = await fetch(`/api/images/${map.image_id}?content_management=true`, {
311
- method: "DELETE",
312
  });
313
 
314
- if (!res.ok) {
315
- const json = await res.json();
316
- throw new Error((json.error as string) || `Delete failed with status ${res.status}`);
317
- }
318
-
319
- setShowDeleteConfirm(false);
320
-
321
- // Navigate to next item in filtered list instead of explore page
322
- try {
323
- const response = await fetch('/api/images');
324
- if (response.ok) {
325
- const images = await response.json();
326
-
327
- // Filter images based on current filter criteria (same logic as navigateToItem)
328
- const filteredImages = images.filter((img: any) => {
329
- const matchesSearch = !search ||
330
- img.title?.toLowerCase().includes(search.toLowerCase()) ||
331
- img.generated?.toLowerCase().includes(search.toLowerCase()) ||
332
- img.source?.toLowerCase().includes(search.toLowerCase()) ||
333
- img.event_type?.toLowerCase().includes(search.toLowerCase());
334
-
335
- const matchesSource = !srcFilter || img.source === srcFilter;
336
- const matchesCategory = !catFilter || img.event_type === catFilter;
337
- const matchesRegion = !regionFilter ||
338
- img.countries?.some((country: any) => country.r_code === regionFilter);
339
- const matchesCountry = !countryFilter ||
340
- img.countries?.some((country: any) => country.c_code === countryFilter);
341
- const matchesImageType = !imageTypeFilter || img.image_type === imageTypeFilter;
342
- const matchesReferenceExamples = !showReferenceExamples || img.starred === true;
343
-
344
- return matchesSearch && matchesSource && matchesCategory && matchesRegion && matchesCountry && matchesImageType && matchesReferenceExamples;
345
- });
346
-
347
- // Remove the current item from the filtered list since it was deleted
348
- const remainingImages = filteredImages.filter((img: any) => img.image_id !== map.image_id);
349
-
350
- console.log('Delete navigation debug:', {
351
- originalFilteredCount: filteredImages.length,
352
- remainingCount: remainingImages.length,
353
- currentIndex: filteredImages.findIndex((img: any) => img.image_id === map.image_id),
354
- remainingImages: remainingImages.map((img: any) => ({ id: img.image_id, title: img.title }))
355
- });
356
-
357
- if (remainingImages.length > 0) {
358
- // Find the current item's position in the original filtered list
359
- const currentIndex = filteredImages.findIndex((img: any) => img.image_id === map.image_id);
360
 
361
- // Navigate to the next item, or the previous if we're at the end
362
- let targetIndex: number;
363
- if (currentIndex === filteredImages.length - 1) {
364
- // We were at the last item, go to the previous one
365
- targetIndex = currentIndex - 1;
366
- } else {
367
- // Go to the next item
368
- targetIndex = currentIndex;
369
- }
 
 
 
 
 
 
 
 
 
370
 
371
- console.log('Navigation target:', { currentIndex, targetIndex, targetId: remainingImages[targetIndex]?.image_id });
372
 
373
- // Make sure the target index is valid
374
- if (targetIndex >= 0 && targetIndex < remainingImages.length) {
375
- console.log('Navigating to:', remainingImages[targetIndex].image_id);
376
- navigate(`/map/${remainingImages[targetIndex].image_id}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  } else {
378
- // Fallback to first remaining item
379
- console.log('Fallback navigation to first item:', remainingImages[0].image_id);
380
- navigate(`/map/${remainingImages[0].image_id}`);
381
  }
382
  } else {
383
- // No more items in filtered list, go to explore page
384
- console.log('No remaining items, going to explore page');
385
  navigate('/explore');
386
  }
387
- } else {
388
- // Fallback to explore page if API call fails
389
  navigate('/explore');
 
 
390
  }
391
- } catch (error) {
392
- console.error('Failed to navigate to next item:', error);
393
- // Fallback to explore page
394
- navigate('/explore');
395
- } finally {
396
- setIsDeleting(false); // Reset flag
397
  }
398
  } catch (error) {
399
  console.error('Delete failed:', error);
400
- alert(`Delete failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
401
- setShowDeleteConfirm(false);
402
  }
403
  };
404
 
@@ -481,7 +465,6 @@ export default function MapDetailPage() {
481
  }
482
  };
483
 
484
- // Helper function to create image data
485
  const createImageData = (map: any, fileName: string) => ({
486
  image: `images/${fileName}`,
487
  caption: map.edited || map.generated || '',
@@ -497,23 +480,17 @@ export default function MapDetailPage() {
497
  });
498
 
499
  const exportDataset = async (mode: 'standard' | 'fine-tuning') => {
500
- if (!map) {
501
- alert('No map to export');
502
- return;
503
- }
504
-
505
  try {
506
- // Create a JSZip instance
507
  const JSZip = (await import('jszip')).default;
508
  const zip = new JSZip();
509
 
510
- // Determine which dataset to create based on image type
511
  if (map.image_type === 'crisis_map') {
512
  const crisisFolder = zip.folder('crisis_maps_dataset');
513
  const crisisImagesFolder = crisisFolder?.folder('images');
514
 
515
  if (crisisImagesFolder) {
516
- // Download the current crisis map image and add to zip
517
  try {
518
  const response = await fetch(map.image_url);
519
  if (!response.ok) throw new Error(`Failed to fetch image ${map.image_id}`);
@@ -525,17 +502,11 @@ export default function MapDetailPage() {
525
  crisisImagesFolder.file(fileName, blob);
526
 
527
  if (mode === 'fine-tuning') {
528
- // Create train.jsonl, test.jsonl, and val.jsonl with stratified sampling
529
  const trainData: any[] = [];
530
  const testData: any[] = [];
531
  const valData: any[] = [];
532
 
533
  if (String(map?.image_type) === 'crisis_map') {
534
- // For crisis maps, group by source
535
- const source = map.source || 'unknown';
536
- const totalImages = 1; // Only one image in MapDetailPage
537
-
538
- // Since we only have one image, distribute it based on the split
539
  const random = Math.random();
540
  if (random < trainSplit / 100) {
541
  trainData.push(createImageData(map, '0001'));
@@ -545,11 +516,6 @@ export default function MapDetailPage() {
545
  valData.push(createImageData(map, '0001'));
546
  }
547
  } else if (String(map?.image_type) === 'drone_image') {
548
- // For drone images, group by event type
549
- const eventType = map.event_type || 'unknown';
550
- const totalImages = 1; // Only one image in MapDetailPage
551
-
552
- // Since we only have one image, distribute it based on the split
553
  const random = Math.random();
554
  if (random < trainSplit / 100) {
555
  trainData.push(createImageData(map, '0001'));
@@ -560,14 +526,12 @@ export default function MapDetailPage() {
560
  }
561
  }
562
 
563
- // Add JSONL files to dataset folder
564
  if (crisisFolder) {
565
  crisisFolder.file('train.jsonl', JSON.stringify(trainData, null, 2));
566
  crisisFolder.file('test.jsonl', JSON.stringify(testData, null, 2));
567
  crisisFolder.file('val.jsonl', JSON.stringify(valData, null, 2));
568
  }
569
  } else {
570
- // Standard mode: create individual JSON file
571
  const jsonData = {
572
  image: `images/${fileName}`,
573
  caption: map.edited || map.generated || '',
@@ -596,7 +560,6 @@ export default function MapDetailPage() {
596
  const droneImagesFolder = droneFolder?.folder('images');
597
 
598
  if (droneImagesFolder) {
599
- // Download the current drone image and add to zip
600
  try {
601
  const response = await fetch(map.image_url);
602
  if (!response.ok) throw new Error(`Failed to fetch image ${map.image_id}`);
@@ -608,17 +571,11 @@ export default function MapDetailPage() {
608
  droneImagesFolder.file(fileName, blob);
609
 
610
  if (mode === 'fine-tuning') {
611
- // Create train.jsonl, test.jsonl, and val.jsonl with stratified sampling
612
  const trainData: any[] = [];
613
  const testData: any[] = [];
614
  const valData: any[] = [];
615
 
616
  if (String(map?.image_type) === 'crisis_map') {
617
- // For crisis maps, group by source
618
- const source = map.source || 'unknown';
619
- const totalImages = 1; // Only one image in MapDetailPage
620
-
621
- // Since we only have one image, distribute it based on the split
622
  const random = Math.random();
623
  if (random < trainSplit / 100) {
624
  trainData.push(createImageData(map, '0001'));
@@ -628,11 +585,6 @@ export default function MapDetailPage() {
628
  valData.push(createImageData(map, '0001'));
629
  }
630
  } else if (String(map?.image_type) === 'drone_image') {
631
- // For drone images, group by event type
632
- const eventType = map.event_type || 'unknown';
633
- const totalImages = 1; // Only one image in MapDetailPage
634
-
635
- // Since we only have one image, distribute it based on the split
636
  const random = Math.random();
637
  if (random < trainSplit / 100) {
638
  trainData.push(createImageData(map, '0001'));
@@ -643,14 +595,12 @@ export default function MapDetailPage() {
643
  }
644
  }
645
 
646
- // Add JSONL files to dataset folder
647
  if (droneFolder) {
648
  droneFolder.file('train.jsonl', JSON.stringify(trainData, null, 2));
649
  droneFolder.file('test.jsonl', JSON.stringify(testData, null, 2));
650
  droneFolder.file('val.jsonl', JSON.stringify(valData, null, 2));
651
  }
652
  } else {
653
- // Standard mode: create individual JSON file
654
  const jsonData = {
655
  image: `images/${fileName}`,
656
  caption: map.edited || map.generated || '',
@@ -675,7 +625,6 @@ export default function MapDetailPage() {
675
  }
676
  }
677
  } else {
678
- // For other image types, create a generic dataset
679
  const genericFolder = zip.folder('generic_dataset');
680
  const genericImagesFolder = genericFolder?.folder('images');
681
 
@@ -691,17 +640,11 @@ export default function MapDetailPage() {
691
  genericImagesFolder.file(fileName, blob);
692
 
693
  if (mode === 'fine-tuning') {
694
- // Create train.jsonl, test.jsonl, and val.jsonl with stratified sampling
695
  const trainData: any[] = [];
696
  const testData: any[] = [];
697
  const valData: any[] = [];
698
 
699
  if (String(map?.image_type) === 'crisis_map') {
700
- // For crisis maps, group by source
701
- const source = map.source || 'unknown';
702
- const totalImages = 1; // Only one image in MapDetailPage
703
-
704
- // Since we only have one image, distribute it based on the split
705
  const random = Math.random();
706
  if (random < trainSplit / 100) {
707
  trainData.push(createImageData(map, '0001'));
@@ -711,11 +654,6 @@ export default function MapDetailPage() {
711
  valData.push(createImageData(map, '0001'));
712
  }
713
  } else if (String(map?.image_type) === 'drone_image') {
714
- // For drone images, group by event type
715
- const eventType = map.event_type || 'unknown';
716
- const totalImages = 1; // Only one image in MapDetailPage
717
-
718
- // Since we only have one image, distribute it based on the split
719
  const random = Math.random();
720
  if (random < trainSplit / 100) {
721
  trainData.push(createImageData(map, '0001'));
@@ -726,14 +664,12 @@ export default function MapDetailPage() {
726
  }
727
  }
728
 
729
- // Add JSONL files to dataset folder
730
  if (genericFolder) {
731
  genericFolder.file('train.jsonl', JSON.stringify(trainData, null, 2));
732
  genericFolder.file('test.jsonl', JSON.stringify(testData, null, 2));
733
  genericFolder.file('val.jsonl', JSON.stringify(valData, null, 2));
734
  }
735
  } else {
736
- // Standard mode: create individual JSON file
737
  const jsonData = {
738
  image: `images/${fileName}`,
739
  caption: map.edited || map.generated || '',
@@ -1241,7 +1177,6 @@ export default function MapDetailPage() {
1241
  const remaining = 100 - newTrain;
1242
  if (remaining >= 0) {
1243
  setTrainSplit(newTrain);
1244
- // Distribute remaining between test and val
1245
  if (testSplit + valSplit > remaining) {
1246
  setTestSplit(Math.floor(remaining / 2));
1247
  setValSplit(remaining - Math.floor(remaining / 2));
@@ -1307,7 +1242,7 @@ export default function MapDetailPage() {
1307
  name="crisis-maps"
1308
  label="Crisis Maps"
1309
  value={crisisMapsSelected}
1310
- onChange={(value, name) => setCrisisMapsSelected(value)}
1311
  />
1312
  </div>
1313
 
@@ -1316,7 +1251,7 @@ export default function MapDetailPage() {
1316
  name="drone-images"
1317
  label="Drone Images"
1318
  value={droneImagesSelected}
1319
- onChange={(value, name) => setDroneImagesSelected(value)}
1320
  />
1321
  </div>
1322
  </div>
@@ -1330,7 +1265,6 @@ export default function MapDetailPage() {
1330
  return;
1331
  }
1332
 
1333
- // For MapDetailPage, we only export the current image if its type is selected
1334
  if ((map?.image_type === 'crisis_map' && crisisMapsSelected) ||
1335
  (map?.image_type === 'drone_image' && droneImagesSelected)) {
1336
  exportDataset(exportMode);
 
53
  const [hasNext, setHasNext] = useState(false);
54
  const [isNavigating, setIsNavigating] = useState(false);
55
 
 
56
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
57
  const [showExportModal, setShowExportModal] = useState(false);
58
  const [exportMode, setExportMode] = useState<'standard' | 'fine-tuning'>('standard');
 
62
  const [crisisMapsSelected, setCrisisMapsSelected] = useState(true);
63
  const [droneImagesSelected, setDroneImagesSelected] = useState(true);
64
 
 
65
  const [isDeleting, setIsDeleting] = useState(false);
66
 
 
67
  const {
68
  search, setSearch,
69
  srcFilter, setSrcFilter,
 
113
 
114
  // Auto-navigate to first matching item when filters change
115
  useEffect(() => {
116
+ if (!map || loading || isDeleting) return;
117
 
 
118
  const currentMapMatches = () => {
119
  const matchesSearch = !search ||
120
  map.title?.toLowerCase().includes(search.toLowerCase()) ||
 
172
  if (response.ok) {
173
  const images = await response.json();
174
 
 
175
  const filteredImages = images.filter((img: any) => {
176
  const matchesSearch = !search ||
177
  img.title?.toLowerCase().includes(search.toLowerCase()) ||
 
204
  const navigateToItem = async (direction: 'previous' | 'next') => {
205
  if (isNavigating) return;
206
 
207
+ setIsNavigating(true);
208
  try {
209
  const response = await fetch('/api/images');
210
+ if (response.ok) {
211
+ const images = await response.json();
 
 
 
 
 
 
 
 
 
212
 
213
+ const filteredImages = images.filter((img: any) => {
214
+ const matchesSearch = !search ||
215
+ img.title?.toLowerCase().includes(search.toLowerCase()) ||
216
+ img.generated?.toLowerCase().includes(search.toLowerCase()) ||
217
+ img.source?.toLowerCase().includes(search.toLowerCase()) ||
218
+ img.event_type?.toLowerCase().includes(search.toLowerCase());
219
+
220
+ const matchesSource = !srcFilter || img.source === srcFilter;
221
+ const matchesCategory = !catFilter || img.event_type === catFilter;
222
+ const matchesRegion = !regionFilter ||
223
+ img.countries?.some((country: any) => country.r_code === regionFilter);
224
+ const matchesCountry = !countryFilter ||
225
+ img.countries?.some((country: any) => country.c_code === countryFilter);
226
+ const matchesImageType = !imageTypeFilter || img.image_type === imageTypeFilter;
227
+ const matchesReferenceExamples = !showReferenceExamples || img.starred === true;
228
+
229
+ return matchesSearch && matchesSource && matchesCategory && matchesRegion && matchesCountry && matchesImageType && matchesReferenceExamples;
230
+ });
231
 
232
+ const currentIndex = filteredImages.findIndex((img: { image_id: string }) => img.image_id === mapId);
233
+
234
+ if (currentIndex === -1) {
235
+ console.error('Current image not found in filtered list');
236
+ return;
237
+ }
238
+
239
+ let targetIndex: number;
240
+ if (direction === 'previous') {
241
+ targetIndex = currentIndex > 0 ? currentIndex - 1 : filteredImages.length - 1;
242
+ } else {
243
+ targetIndex = currentIndex < filteredImages.length - 1 ? currentIndex + 1 : 0;
244
+ }
245
+
246
+ const targetImage = filteredImages[targetIndex];
247
+ if (targetImage) {
248
+ navigate(`/map/${targetImage.image_id}`);
249
+ }
250
  }
 
 
 
251
  } catch (error) {
252
+ console.error('Failed to navigate to item:', error);
253
+ } finally {
254
+ setIsNavigating(false);
255
  }
256
  };
257
 
 
273
 
274
  const [isGenerating, setIsGenerating] = useState(false);
275
 
276
+ // delete function
277
  const handleDelete = async () => {
278
  if (!map) return;
279
 
 
295
  });
296
 
297
  if (response.ok) {
 
298
  setMap(prev => prev ? { ...prev, starred: !prev.starred } : null);
299
  } else {
300
  console.error('Failed to toggle starred status');
 
307
  const confirmDelete = async () => {
308
  if (!map) return;
309
 
310
+ setIsDeleting(true);
311
  try {
312
  console.log('Deleting image with ID:', map.image_id);
313
+ const response = await fetch(`/api/images/${map.image_id}`, {
314
+ method: 'DELETE',
315
  });
316
 
317
+ if (response.ok) {
318
+ setMap(prev => prev ? { ...prev, starred: !prev.starred } : null);
319
+ setShowDeleteConfirm(false);
320
+
321
+ try {
322
+ const response = await fetch('/api/images');
323
+ if (response.ok) {
324
+ const images = await response.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
+ const filteredImages = images.filter((img: any) => {
327
+ const matchesSearch = !search ||
328
+ img.title?.toLowerCase().includes(search.toLowerCase()) ||
329
+ img.generated?.toLowerCase().includes(search.toLowerCase()) ||
330
+ img.source?.toLowerCase().includes(search.toLowerCase()) ||
331
+ img.event_type?.toLowerCase().includes(search.toLowerCase());
332
+
333
+ const matchesSource = !srcFilter || img.source === srcFilter;
334
+ const matchesCategory = !catFilter || img.event_type === catFilter;
335
+ const matchesRegion = !regionFilter ||
336
+ img.countries?.some((country: any) => country.r_code === regionFilter);
337
+ const matchesCountry = !countryFilter ||
338
+ img.countries?.some((country: any) => country.c_code === countryFilter);
339
+ const matchesImageType = !imageTypeFilter || img.image_type === imageTypeFilter;
340
+ const matchesReferenceExamples = !showReferenceExamples || img.starred === true;
341
+
342
+ return matchesSearch && matchesSource && matchesCategory && matchesRegion && matchesCountry && matchesImageType && matchesReferenceExamples;
343
+ });
344
 
345
+ const remainingImages = filteredImages.filter((img: any) => img.image_id !== map.image_id);
346
 
347
+ if (remainingImages.length > 0) {
348
+ const currentIndex = filteredImages.findIndex((img: any) => img.image_id === map.image_id);
349
+
350
+ let targetIndex: number;
351
+ if (currentIndex === filteredImages.length - 1) {
352
+ targetIndex = currentIndex - 1;
353
+ } else {
354
+ targetIndex = currentIndex;
355
+ }
356
+
357
+ console.log('Navigation target:', { currentIndex, targetIndex, targetId: remainingImages[targetIndex]?.image_id });
358
+
359
+ if (targetIndex >= 0 && targetIndex < remainingImages.length) {
360
+ console.log('Navigating to:', remainingImages[targetIndex].image_id);
361
+ navigate(`/map/${remainingImages[targetIndex].image_id}`);
362
+ } else {
363
+ console.log('Fallback navigation to first item:', remainingImages[0].image_id);
364
+ navigate(`/map/${remainingImages[0].image_id}`);
365
+ }
366
  } else {
367
+ console.log('No remaining items, going to explore page');
368
+ navigate('/explore');
 
369
  }
370
  } else {
 
 
371
  navigate('/explore');
372
  }
373
+ } catch (error) {
374
+ console.error('Failed to navigate to next item:', error);
375
  navigate('/explore');
376
+ } finally {
377
+ setIsDeleting(false);
378
  }
379
+ } else {
380
+ console.error('Delete failed');
381
+ setIsDeleting(false);
 
 
 
382
  }
383
  } catch (error) {
384
  console.error('Delete failed:', error);
385
+ setIsDeleting(false);
 
386
  }
387
  };
388
 
 
465
  }
466
  };
467
 
 
468
  const createImageData = (map: any, fileName: string) => ({
469
  image: `images/${fileName}`,
470
  caption: map.edited || map.generated || '',
 
480
  });
481
 
482
  const exportDataset = async (mode: 'standard' | 'fine-tuning') => {
483
+ if (!map) return;
484
+
 
 
 
485
  try {
 
486
  const JSZip = (await import('jszip')).default;
487
  const zip = new JSZip();
488
 
 
489
  if (map.image_type === 'crisis_map') {
490
  const crisisFolder = zip.folder('crisis_maps_dataset');
491
  const crisisImagesFolder = crisisFolder?.folder('images');
492
 
493
  if (crisisImagesFolder) {
 
494
  try {
495
  const response = await fetch(map.image_url);
496
  if (!response.ok) throw new Error(`Failed to fetch image ${map.image_id}`);
 
502
  crisisImagesFolder.file(fileName, blob);
503
 
504
  if (mode === 'fine-tuning') {
 
505
  const trainData: any[] = [];
506
  const testData: any[] = [];
507
  const valData: any[] = [];
508
 
509
  if (String(map?.image_type) === 'crisis_map') {
 
 
 
 
 
510
  const random = Math.random();
511
  if (random < trainSplit / 100) {
512
  trainData.push(createImageData(map, '0001'));
 
516
  valData.push(createImageData(map, '0001'));
517
  }
518
  } else if (String(map?.image_type) === 'drone_image') {
 
 
 
 
 
519
  const random = Math.random();
520
  if (random < trainSplit / 100) {
521
  trainData.push(createImageData(map, '0001'));
 
526
  }
527
  }
528
 
 
529
  if (crisisFolder) {
530
  crisisFolder.file('train.jsonl', JSON.stringify(trainData, null, 2));
531
  crisisFolder.file('test.jsonl', JSON.stringify(testData, null, 2));
532
  crisisFolder.file('val.jsonl', JSON.stringify(valData, null, 2));
533
  }
534
  } else {
 
535
  const jsonData = {
536
  image: `images/${fileName}`,
537
  caption: map.edited || map.generated || '',
 
560
  const droneImagesFolder = droneFolder?.folder('images');
561
 
562
  if (droneImagesFolder) {
 
563
  try {
564
  const response = await fetch(map.image_url);
565
  if (!response.ok) throw new Error(`Failed to fetch image ${map.image_id}`);
 
571
  droneImagesFolder.file(fileName, blob);
572
 
573
  if (mode === 'fine-tuning') {
 
574
  const trainData: any[] = [];
575
  const testData: any[] = [];
576
  const valData: any[] = [];
577
 
578
  if (String(map?.image_type) === 'crisis_map') {
 
 
 
 
 
579
  const random = Math.random();
580
  if (random < trainSplit / 100) {
581
  trainData.push(createImageData(map, '0001'));
 
585
  valData.push(createImageData(map, '0001'));
586
  }
587
  } else if (String(map?.image_type) === 'drone_image') {
 
 
 
 
 
588
  const random = Math.random();
589
  if (random < trainSplit / 100) {
590
  trainData.push(createImageData(map, '0001'));
 
595
  }
596
  }
597
 
 
598
  if (droneFolder) {
599
  droneFolder.file('train.jsonl', JSON.stringify(trainData, null, 2));
600
  droneFolder.file('test.jsonl', JSON.stringify(testData, null, 2));
601
  droneFolder.file('val.jsonl', JSON.stringify(valData, null, 2));
602
  }
603
  } else {
 
604
  const jsonData = {
605
  image: `images/${fileName}`,
606
  caption: map.edited || map.generated || '',
 
625
  }
626
  }
627
  } else {
 
628
  const genericFolder = zip.folder('generic_dataset');
629
  const genericImagesFolder = genericFolder?.folder('images');
630
 
 
640
  genericImagesFolder.file(fileName, blob);
641
 
642
  if (mode === 'fine-tuning') {
 
643
  const trainData: any[] = [];
644
  const testData: any[] = [];
645
  const valData: any[] = [];
646
 
647
  if (String(map?.image_type) === 'crisis_map') {
 
 
 
 
 
648
  const random = Math.random();
649
  if (random < trainSplit / 100) {
650
  trainData.push(createImageData(map, '0001'));
 
654
  valData.push(createImageData(map, '0001'));
655
  }
656
  } else if (String(map?.image_type) === 'drone_image') {
 
 
 
 
 
657
  const random = Math.random();
658
  if (random < trainSplit / 100) {
659
  trainData.push(createImageData(map, '0001'));
 
664
  }
665
  }
666
 
 
667
  if (genericFolder) {
668
  genericFolder.file('train.jsonl', JSON.stringify(trainData, null, 2));
669
  genericFolder.file('test.jsonl', JSON.stringify(testData, null, 2));
670
  genericFolder.file('val.jsonl', JSON.stringify(valData, null, 2));
671
  }
672
  } else {
 
673
  const jsonData = {
674
  image: `images/${fileName}`,
675
  caption: map.edited || map.generated || '',
 
1177
  const remaining = 100 - newTrain;
1178
  if (remaining >= 0) {
1179
  setTrainSplit(newTrain);
 
1180
  if (testSplit + valSplit > remaining) {
1181
  setTestSplit(Math.floor(remaining / 2));
1182
  setValSplit(remaining - Math.floor(remaining / 2));
 
1242
  name="crisis-maps"
1243
  label="Crisis Maps"
1244
  value={crisisMapsSelected}
1245
+ onChange={(value, _name) => setCrisisMapsSelected(value)}
1246
  />
1247
  </div>
1248
 
 
1251
  name="drone-images"
1252
  label="Drone Images"
1253
  value={droneImagesSelected}
1254
+ onChange={(value, _name) => setDroneImagesSelected(value)}
1255
  />
1256
  </div>
1257
  </div>
 
1265
  return;
1266
  }
1267
 
 
1268
  if ((map?.image_type === 'crisis_map' && crisisMapsSelected) ||
1269
  (map?.image_type === 'drone_image' && droneImagesSelected)) {
1270
  exportDataset(exportMode);
frontend/src/pages/UploadPage/UploadPage.tsx CHANGED
@@ -32,7 +32,7 @@ export default function UploadPage() {
32
  const [countries, setCountries] = useState<string[]>([]);
33
  const [title, setTitle] = useState('');
34
 
35
- // Drone-specific metadata fields
36
  const [centerLon, setCenterLon] = useState<string>('');
37
  const [centerLat, setCenterLat] = useState<string>('');
38
  const [amslM, setAmslM] = useState<string>('');
@@ -98,7 +98,6 @@ export default function UploadPage() {
98
  if (sourcesData.length > 0) setSource(sourcesData[0].s_code);
99
  setEventType('OTHER');
100
  setEpsg('OTHER');
101
- // Only set default imageType if we don't have one from URL parameter and don't already have a default
102
  if (imageTypesData.length > 0 && !searchParams.get('imageType') && !imageType) {
103
  setImageType(imageTypesData[0].image_type);
104
  }
@@ -106,7 +105,6 @@ export default function UploadPage() {
106
  }, [searchParams]);
107
 
108
  const handleNavigation = useCallback((to: string) => {
109
- // Prevent navigation to upload/home when already on upload page
110
  if (to === '/upload' || to === '/') {
111
  return;
112
  }
@@ -141,7 +139,6 @@ export default function UploadPage() {
141
 
142
  const handleCleanup = () => {
143
  if (uploadedImageIdRef.current) {
144
- // This is a user dissatisfaction delete (abandoning upload), so don't pass content_management=true
145
  fetch(`/api/images/${uploadedImageIdRef.current}`, { method: "DELETE" }).catch(console.error);
146
  }
147
  };
@@ -186,7 +183,6 @@ export default function UploadPage() {
186
  setIsLoadingContribution(true);
187
  setUploadedImageId(imageIdParam);
188
 
189
- // Set imageType from URL parameter if available
190
  if (imageTypeParam) {
191
  console.log('Setting imageType from URL parameter:', imageTypeParam);
192
  setImageType(imageTypeParam);
@@ -196,14 +192,12 @@ export default function UploadPage() {
196
  .then(res => res.json())
197
  .then(data => {
198
  console.log('API response data.image_type:', data.image_type);
199
- // Only set imageType from API if we don't have it from URL parameter
200
  if (data.image_type && !imageTypeParam) {
201
  console.log('Setting imageType from API response:', data.image_type);
202
  setImageType(data.image_type);
203
  }
204
 
205
  if (data.generated) {
206
- // Handle both plain text and JSON responses
207
  try {
208
  const parsedGenerated = JSON.parse(data.generated);
209
  if (parsedGenerated.analysis) {
@@ -212,7 +206,6 @@ export default function UploadPage() {
212
  setDraft(data.generated);
213
  }
214
  } catch (e) {
215
- // Not JSON, treat as plain text
216
  setDraft(data.generated);
217
  }
218
  }
@@ -269,7 +262,6 @@ export default function UploadPage() {
269
  }
270
  }, [searchParams]);
271
 
272
- // Debug useEffect to track imageType changes
273
  useEffect(() => {
274
  console.log('imageType changed to:', imageType);
275
  }, [imageType]);
@@ -337,7 +329,6 @@ export default function UploadPage() {
337
  error?: string;
338
  } | null>(null);
339
 
340
- // Enhanced preprocessing flow state
341
  const [showPreprocessingModal, setShowPreprocessingModal] = useState(false);
342
  const [preprocessingFile, setPreprocessingFile] = useState<File | null>(null);
343
  const [isPreprocessing, setIsPreprocessing] = useState(false);
@@ -348,7 +339,6 @@ export default function UploadPage() {
348
  e.preventDefault();
349
  const dropped = e.dataTransfer.files?.[0];
350
  if (dropped) {
351
- // Use onFileChange to trigger preprocessing detection
352
  onFileChange(dropped);
353
  }
354
  };
@@ -369,33 +359,20 @@ export default function UploadPage() {
369
  }
370
  };
371
 
372
- // Check if file needs preprocessing (non-JPEG/PNG)
373
  const needsPreprocessing = (file: File): boolean => {
374
  const supportedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
375
  const supportedExtensions = ['.jpg', '.jpeg', '.png'];
376
 
377
- // Check MIME type first
378
  let needsPreprocess = !supportedTypes.includes(file.type);
379
 
380
- // If MIME type check is inconclusive, check file extension
381
  if (!needsPreprocess && file.name) {
382
  const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
383
  needsPreprocess = !supportedExtensions.includes(fileExtension);
384
  }
385
 
386
- console.log('Preprocessing check:', {
387
- fileName: file.name,
388
- fileType: file.type,
389
- fileExtension: file.name ? file.name.toLowerCase().substring(file.name.lastIndexOf('.')) : 'none',
390
- supportedTypes,
391
- supportedExtensions,
392
- needsPreprocess
393
- });
394
-
395
  return needsPreprocess;
396
  };
397
 
398
- // Handle preprocessing confirmation
399
  const handlePreprocessingConfirm = async () => {
400
  if (!preprocessingFile) return;
401
 
@@ -403,14 +380,12 @@ export default function UploadPage() {
403
  setPreprocessingProgress('Starting file conversion...');
404
 
405
  try {
406
- // Create FormData for preprocessing
407
  const formData = new FormData();
408
  formData.append('file', preprocessingFile);
409
- formData.append('preprocess_only', 'true'); // Flag to indicate preprocessing only
410
 
411
  setPreprocessingProgress('Converting file format...');
412
 
413
- // Call preprocessing endpoint
414
  const response = await fetch('/api/images/preprocess', {
415
  method: 'POST',
416
  body: formData
@@ -424,30 +399,25 @@ export default function UploadPage() {
424
 
425
  setPreprocessingProgress('Finalizing conversion...');
426
 
427
- // Decode base64 content
428
  const processedContent = atob(result.processed_content);
429
  const processedBytes = new Uint8Array(processedContent.length);
430
  for (let i = 0; i < processedContent.length; i++) {
431
  processedBytes[i] = processedContent.charCodeAt(i);
432
  }
433
 
434
- // Create a new File object from the processed data
435
  const processedFile = new File(
436
  [processedBytes],
437
  result.processed_filename,
438
  { type: result.processed_mime_type }
439
  );
440
 
441
- // Create preview URL
442
  const previewUrl = URL.createObjectURL(processedFile);
443
 
444
- // Update the main file state
445
  setFile(processedFile);
446
  setPreview(previewUrl);
447
 
448
  setPreprocessingProgress('Conversion complete!');
449
 
450
- // Close modal after a brief delay
451
  setTimeout(() => {
452
  setShowPreprocessingModal(false);
453
  setPreprocessingFile(null);
@@ -467,7 +437,6 @@ export default function UploadPage() {
467
  }
468
  };
469
 
470
- // Handle preprocessing cancellation
471
  const handlePreprocessingCancel = () => {
472
  setShowPreprocessingModal(false);
473
  setPreprocessingFile(null);
@@ -511,7 +480,6 @@ export default function UploadPage() {
511
  if (imageType === 'drone_image') {
512
  fd.append('event_type', eventType || 'OTHER');
513
  fd.append('epsg', epsg || 'OTHER');
514
- // Add drone-specific metadata
515
  if (centerLon) fd.append('center_lon', centerLon);
516
  if (centerLat) fd.append('center_lat', centerLat);
517
  if (amslM) fd.append('amsl_m', amslM);
@@ -543,7 +511,6 @@ export default function UploadPage() {
543
  if (!mapRes.ok) throw new Error((mapJson.error as string) || 'Upload failed');
544
  setImageUrl(mapJson.image_url as string);
545
 
546
- // Check for preprocessing info and show notification if needed
547
  if (mapJson.preprocessing_info &&
548
  typeof mapJson.preprocessing_info === 'object' &&
549
  'was_preprocessed' in mapJson.preprocessing_info &&
@@ -574,7 +541,6 @@ export default function UploadPage() {
574
  if (!capRes.ok) throw new Error((capJson.error as string) || 'Caption failed');
575
  setUploadedImageId(mapIdVal);
576
 
577
- // Check for model fallback information
578
  const fallbackInfo = (capJson.raw_json as Record<string, unknown>)?.fallback_info;
579
  if (fallbackInfo) {
580
  setFallbackInfo({
@@ -611,7 +577,6 @@ export default function UploadPage() {
611
  }
612
  }
613
 
614
- // Handle both plain text and JSON responses
615
  if (capJson.generated) {
616
  try {
617
  const parsedGenerated = JSON.parse(capJson.generated as string);
@@ -621,7 +586,6 @@ export default function UploadPage() {
621
  setDraft(capJson.generated as string);
622
  }
623
  } catch (e) {
624
- // Not JSON, treat as plain text
625
  setDraft(capJson.generated as string);
626
  }
627
  }
@@ -665,7 +629,6 @@ export default function UploadPage() {
665
  const json = await readJsonSafely(res);
666
  if (!res.ok) throw new Error((json.error as string) || 'Upload failed');
667
 
668
- // Check for preprocessing info and show notification if needed
669
  if (json.preprocessing_info &&
670
  typeof json.preprocessing_info === 'object' &&
671
  'was_preprocessed' in json.preprocessing_info &&
@@ -691,7 +654,6 @@ export default function UploadPage() {
691
  const capJson = await readJsonSafely(capRes);
692
  if (!capRes.ok) throw new Error((capJson.error as string) || 'Caption failed');
693
 
694
- // Check for model fallback information
695
  const fallbackInfo = (capJson.raw_json as Record<string, unknown>)?.fallback_info;
696
  if (fallbackInfo) {
697
  setFallbackInfo({
@@ -712,7 +674,6 @@ export default function UploadPage() {
712
  if ((metadata as Record<string, unknown>).countries && Array.isArray((metadata as Record<string, unknown>).countries)) {
713
  setCountries((metadata as Record<string, unknown>).countries as string[]);
714
  }
715
- // Extract drone metadata if available
716
  if (imageType === 'drone_image') {
717
  if ((metadata as Record<string, unknown>).center_lon) setCenterLon((metadata as Record<string, unknown>).center_lon as string);
718
  if ((metadata as Record<string, unknown>).center_lat) setCenterLat((metadata as Record<string, unknown>).center_lat as string);
@@ -728,7 +689,6 @@ export default function UploadPage() {
728
  }
729
  }
730
 
731
- // Handle both plain text and JSON responses
732
  if (capJson.generated) {
733
  try {
734
  const parsedGenerated = JSON.parse(capJson.generated as string);
@@ -738,7 +698,6 @@ export default function UploadPage() {
738
  setDraft(capJson.generated as string);
739
  }
740
  } catch (e) {
741
- // Not JSON, treat as plain text
742
  setDraft(capJson.generated as string);
743
  }
744
  }
@@ -921,7 +880,6 @@ export default function UploadPage() {
921
  </>
922
  )}
923
 
924
- {/* File-picker button - always visible */}
925
  <label className="inline-block cursor-pointer">
926
  <input
927
  type="file"
@@ -941,7 +899,6 @@ export default function UploadPage() {
941
  </div>
942
  )}
943
 
944
- {/* Loading state */}
945
  {isLoading && (
946
  <div className={styles.loadingContainer}>
947
  <Spinner className="text-ifrcRed" />
@@ -949,7 +906,6 @@ export default function UploadPage() {
949
  </div>
950
  )}
951
 
952
- {/* Loading contribution data */}
953
  {isLoadingContribution && (
954
  <div className={styles.loadingContainer}>
955
  <Spinner className="text-ifrcRed" />
@@ -957,7 +913,6 @@ export default function UploadPage() {
957
  </div>
958
  )}
959
 
960
- {/* Generate button */}
961
  {step === 1 && !isLoading && (
962
  <div className={styles.generateButtonContainer}>
963
  {imageUrl ? (
@@ -981,7 +936,6 @@ export default function UploadPage() {
981
 
982
  {step === '2a' && (
983
  <div className={styles.step2Layout}>
984
- {/* Left Column - Map */}
985
  <div className={styles.mapColumn}>
986
  <Container heading="Uploaded Image" headingLevel={3} withHeaderBorder withInternalPadding>
987
  <div className={styles.uploadedMapContainer}>
@@ -1005,7 +959,6 @@ export default function UploadPage() {
1005
  </Container>
1006
  </div>
1007
 
1008
- {/* Right Column - Metadata Form */}
1009
  <div className={styles.contentColumn}>
1010
  <div className={styles.metadataSectionCard}>
1011
  <Container
@@ -1079,7 +1032,6 @@ export default function UploadPage() {
1079
  placeholder="Select one or more"
1080
  />
1081
 
1082
- {/* Drone-specific metadata fields */}
1083
  {imageType === 'drone_image' && (
1084
  <>
1085
  <div className={styles.droneMetadataSection}>
@@ -1214,7 +1166,6 @@ export default function UploadPage() {
1214
 
1215
  {step === '2b' && (
1216
  <div className={styles.step2Layout}>
1217
- {/* Left Column - Map */}
1218
  <div className={styles.mapColumn}>
1219
  <Container heading="Uploaded Image" headingLevel={3} withHeaderBorder withInternalPadding>
1220
  <div className={styles.uploadedMapContainer}>
@@ -1294,7 +1245,6 @@ export default function UploadPage() {
1294
  </Container>
1295
  </div>
1296
 
1297
- {/* ────── AI‑GENERATED CAPTION ────── */}
1298
  <div className={styles.metadataSectionCard}>
1299
  <Container
1300
  heading="Generated Text"
@@ -1542,7 +1492,7 @@ export default function UploadPage() {
1542
  </div>
1543
  )}
1544
 
1545
- {/* Success page - outside the upload container */}
1546
  {step === 3 && (
1547
  <div className={styles.successContainer}>
1548
  <Heading level={2} className={styles.successHeading}>Saved!</Heading>
 
32
  const [countries, setCountries] = useState<string[]>([]);
33
  const [title, setTitle] = useState('');
34
 
35
+ // Drone metadata fields
36
  const [centerLon, setCenterLon] = useState<string>('');
37
  const [centerLat, setCenterLat] = useState<string>('');
38
  const [amslM, setAmslM] = useState<string>('');
 
98
  if (sourcesData.length > 0) setSource(sourcesData[0].s_code);
99
  setEventType('OTHER');
100
  setEpsg('OTHER');
 
101
  if (imageTypesData.length > 0 && !searchParams.get('imageType') && !imageType) {
102
  setImageType(imageTypesData[0].image_type);
103
  }
 
105
  }, [searchParams]);
106
 
107
  const handleNavigation = useCallback((to: string) => {
 
108
  if (to === '/upload' || to === '/') {
109
  return;
110
  }
 
139
 
140
  const handleCleanup = () => {
141
  if (uploadedImageIdRef.current) {
 
142
  fetch(`/api/images/${uploadedImageIdRef.current}`, { method: "DELETE" }).catch(console.error);
143
  }
144
  };
 
183
  setIsLoadingContribution(true);
184
  setUploadedImageId(imageIdParam);
185
 
 
186
  if (imageTypeParam) {
187
  console.log('Setting imageType from URL parameter:', imageTypeParam);
188
  setImageType(imageTypeParam);
 
192
  .then(res => res.json())
193
  .then(data => {
194
  console.log('API response data.image_type:', data.image_type);
 
195
  if (data.image_type && !imageTypeParam) {
196
  console.log('Setting imageType from API response:', data.image_type);
197
  setImageType(data.image_type);
198
  }
199
 
200
  if (data.generated) {
 
201
  try {
202
  const parsedGenerated = JSON.parse(data.generated);
203
  if (parsedGenerated.analysis) {
 
206
  setDraft(data.generated);
207
  }
208
  } catch (e) {
 
209
  setDraft(data.generated);
210
  }
211
  }
 
262
  }
263
  }, [searchParams]);
264
 
 
265
  useEffect(() => {
266
  console.log('imageType changed to:', imageType);
267
  }, [imageType]);
 
329
  error?: string;
330
  } | null>(null);
331
 
 
332
  const [showPreprocessingModal, setShowPreprocessingModal] = useState(false);
333
  const [preprocessingFile, setPreprocessingFile] = useState<File | null>(null);
334
  const [isPreprocessing, setIsPreprocessing] = useState(false);
 
339
  e.preventDefault();
340
  const dropped = e.dataTransfer.files?.[0];
341
  if (dropped) {
 
342
  onFileChange(dropped);
343
  }
344
  };
 
359
  }
360
  };
361
 
 
362
  const needsPreprocessing = (file: File): boolean => {
363
  const supportedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
364
  const supportedExtensions = ['.jpg', '.jpeg', '.png'];
365
 
 
366
  let needsPreprocess = !supportedTypes.includes(file.type);
367
 
 
368
  if (!needsPreprocess && file.name) {
369
  const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
370
  needsPreprocess = !supportedExtensions.includes(fileExtension);
371
  }
372
 
 
 
 
 
 
 
 
 
 
373
  return needsPreprocess;
374
  };
375
 
 
376
  const handlePreprocessingConfirm = async () => {
377
  if (!preprocessingFile) return;
378
 
 
380
  setPreprocessingProgress('Starting file conversion...');
381
 
382
  try {
 
383
  const formData = new FormData();
384
  formData.append('file', preprocessingFile);
385
+ formData.append('preprocess_only', 'true');
386
 
387
  setPreprocessingProgress('Converting file format...');
388
 
 
389
  const response = await fetch('/api/images/preprocess', {
390
  method: 'POST',
391
  body: formData
 
399
 
400
  setPreprocessingProgress('Finalizing conversion...');
401
 
 
402
  const processedContent = atob(result.processed_content);
403
  const processedBytes = new Uint8Array(processedContent.length);
404
  for (let i = 0; i < processedContent.length; i++) {
405
  processedBytes[i] = processedContent.charCodeAt(i);
406
  }
407
 
 
408
  const processedFile = new File(
409
  [processedBytes],
410
  result.processed_filename,
411
  { type: result.processed_mime_type }
412
  );
413
 
 
414
  const previewUrl = URL.createObjectURL(processedFile);
415
 
 
416
  setFile(processedFile);
417
  setPreview(previewUrl);
418
 
419
  setPreprocessingProgress('Conversion complete!');
420
 
 
421
  setTimeout(() => {
422
  setShowPreprocessingModal(false);
423
  setPreprocessingFile(null);
 
437
  }
438
  };
439
 
 
440
  const handlePreprocessingCancel = () => {
441
  setShowPreprocessingModal(false);
442
  setPreprocessingFile(null);
 
480
  if (imageType === 'drone_image') {
481
  fd.append('event_type', eventType || 'OTHER');
482
  fd.append('epsg', epsg || 'OTHER');
 
483
  if (centerLon) fd.append('center_lon', centerLon);
484
  if (centerLat) fd.append('center_lat', centerLat);
485
  if (amslM) fd.append('amsl_m', amslM);
 
511
  if (!mapRes.ok) throw new Error((mapJson.error as string) || 'Upload failed');
512
  setImageUrl(mapJson.image_url as string);
513
 
 
514
  if (mapJson.preprocessing_info &&
515
  typeof mapJson.preprocessing_info === 'object' &&
516
  'was_preprocessed' in mapJson.preprocessing_info &&
 
541
  if (!capRes.ok) throw new Error((capJson.error as string) || 'Caption failed');
542
  setUploadedImageId(mapIdVal);
543
 
 
544
  const fallbackInfo = (capJson.raw_json as Record<string, unknown>)?.fallback_info;
545
  if (fallbackInfo) {
546
  setFallbackInfo({
 
577
  }
578
  }
579
 
 
580
  if (capJson.generated) {
581
  try {
582
  const parsedGenerated = JSON.parse(capJson.generated as string);
 
586
  setDraft(capJson.generated as string);
587
  }
588
  } catch (e) {
 
589
  setDraft(capJson.generated as string);
590
  }
591
  }
 
629
  const json = await readJsonSafely(res);
630
  if (!res.ok) throw new Error((json.error as string) || 'Upload failed');
631
 
 
632
  if (json.preprocessing_info &&
633
  typeof json.preprocessing_info === 'object' &&
634
  'was_preprocessed' in json.preprocessing_info &&
 
654
  const capJson = await readJsonSafely(capRes);
655
  if (!capRes.ok) throw new Error((capJson.error as string) || 'Caption failed');
656
 
 
657
  const fallbackInfo = (capJson.raw_json as Record<string, unknown>)?.fallback_info;
658
  if (fallbackInfo) {
659
  setFallbackInfo({
 
674
  if ((metadata as Record<string, unknown>).countries && Array.isArray((metadata as Record<string, unknown>).countries)) {
675
  setCountries((metadata as Record<string, unknown>).countries as string[]);
676
  }
 
677
  if (imageType === 'drone_image') {
678
  if ((metadata as Record<string, unknown>).center_lon) setCenterLon((metadata as Record<string, unknown>).center_lon as string);
679
  if ((metadata as Record<string, unknown>).center_lat) setCenterLat((metadata as Record<string, unknown>).center_lat as string);
 
689
  }
690
  }
691
 
 
692
  if (capJson.generated) {
693
  try {
694
  const parsedGenerated = JSON.parse(capJson.generated as string);
 
698
  setDraft(capJson.generated as string);
699
  }
700
  } catch (e) {
 
701
  setDraft(capJson.generated as string);
702
  }
703
  }
 
880
  </>
881
  )}
882
 
 
883
  <label className="inline-block cursor-pointer">
884
  <input
885
  type="file"
 
899
  </div>
900
  )}
901
 
 
902
  {isLoading && (
903
  <div className={styles.loadingContainer}>
904
  <Spinner className="text-ifrcRed" />
 
906
  </div>
907
  )}
908
 
 
909
  {isLoadingContribution && (
910
  <div className={styles.loadingContainer}>
911
  <Spinner className="text-ifrcRed" />
 
913
  </div>
914
  )}
915
 
 
916
  {step === 1 && !isLoading && (
917
  <div className={styles.generateButtonContainer}>
918
  {imageUrl ? (
 
936
 
937
  {step === '2a' && (
938
  <div className={styles.step2Layout}>
 
939
  <div className={styles.mapColumn}>
940
  <Container heading="Uploaded Image" headingLevel={3} withHeaderBorder withInternalPadding>
941
  <div className={styles.uploadedMapContainer}>
 
959
  </Container>
960
  </div>
961
 
 
962
  <div className={styles.contentColumn}>
963
  <div className={styles.metadataSectionCard}>
964
  <Container
 
1032
  placeholder="Select one or more"
1033
  />
1034
 
 
1035
  {imageType === 'drone_image' && (
1036
  <>
1037
  <div className={styles.droneMetadataSection}>
 
1166
 
1167
  {step === '2b' && (
1168
  <div className={styles.step2Layout}>
 
1169
  <div className={styles.mapColumn}>
1170
  <Container heading="Uploaded Image" headingLevel={3} withHeaderBorder withInternalPadding>
1171
  <div className={styles.uploadedMapContainer}>
 
1245
  </Container>
1246
  </div>
1247
 
 
1248
  <div className={styles.metadataSectionCard}>
1249
  <Container
1250
  heading="Generated Text"
 
1492
  </div>
1493
  )}
1494
 
1495
+ {/* Success page */}
1496
  {step === 3 && (
1497
  <div className={styles.successContainer}>
1498
  <Heading level={2} className={styles.successHeading}>Saved!</Heading>
go-web-app-develop/.changeset/README.md DELETED
@@ -1,8 +0,0 @@
1
- # Changesets
2
-
3
- Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4
- with multi-package repos, or single-package repos to help you version and publish your code. You can
5
- find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6
-
7
- We have a quick list of common questions to get you started engaging with this project in
8
- [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
 
 
 
 
 
 
 
 
 
go-web-app-develop/.changeset/config.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
3
- "changelog": "@changesets/cli/changelog",
4
- "commit": false,
5
- "fixed": [],
6
- "linked": [],
7
- "access": "public",
8
- "baseBranch": "develop",
9
- "updateInternalDependencies": "patch",
10
- "ignore": [],
11
- "privatePackages": {
12
- "version": true,
13
- "tag": true
14
- }
15
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.changeset/lovely-kids-boil.md DELETED
@@ -1,5 +0,0 @@
1
- ---
2
- "go-web-app": patch
3
- ---
4
-
5
- Fix use of operational timeframe date in imminent final report form
 
 
 
 
 
 
go-web-app-develop/.changeset/pre.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "mode": "pre",
3
- "tag": "beta",
4
- "initialVersions": {
5
- "go-web-app": "7.20.2",
6
- "go-ui-storybook": "1.0.7",
7
- "@ifrc-go/ui": "1.5.1"
8
- },
9
- "changesets": [
10
- "lovely-kids-boil",
11
- "solid-clubs-care",
12
- "sweet-gifts-cheer",
13
- "whole-lions-guess"
14
- ]
15
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.changeset/solid-clubs-care.md DELETED
@@ -1,8 +0,0 @@
1
- ---
2
- "go-web-app": minor
3
- ---
4
-
5
- Add Crisis categorization update date
6
-
7
- - Add updated date for crisis categorization in emergency page.
8
- - Add consent checkbox over situational overview in field report form.
 
 
 
 
 
 
 
 
 
go-web-app-develop/.changeset/sweet-gifts-cheer.md DELETED
@@ -1,9 +0,0 @@
1
- ---
2
- "go-web-app": minor
3
- ---
4
-
5
- Add support for DREF imminent v2 in final report
6
-
7
- - Add a separate route for the old dref final report form
8
- - Update dref final report to accomodate imminent v2 changes
9
-
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.changeset/whole-lions-guess.md DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- "go-web-app": patch
3
- ---
4
-
5
- - Fix calculation of Operation End date in Final report form
6
- - Fix icon position issue in the implementation table of DREF PDF export
7
- - Update the label for last update date in the crisis categorization pop-up
 
 
 
 
 
 
 
 
go-web-app-develop/.dockerignore DELETED
@@ -1,148 +0,0 @@
1
- # Swap files
2
- *.swp
3
-
4
- # Byte-compiled / optimized / DLL files
5
- __pycache__
6
- *.py[cod]
7
- *$py.class
8
-
9
- # C extensions
10
- *.so
11
-
12
- # Distribution / packaging
13
- .Python
14
- env
15
- build
16
- develop-eggs
17
- dist
18
- downloads
19
- eggs
20
- .eggs
21
- lib
22
- lib64
23
- parts
24
- sdist
25
- var
26
- *.egg-info
27
- .installed.cfg
28
- *.egg
29
-
30
- # PyInstaller
31
- # Usually these files are written by a python script from a template
32
- # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
- *.manifest
34
- *.spec
35
-
36
- # Installer logs
37
- pip-log.txt
38
- pip-delete-this-directory.txt
39
-
40
- # Unit test / coverage reports
41
- htmlcov
42
- .tox
43
- .coverage
44
- .coverage.*
45
- .cache
46
- nosetests.xml
47
- coverage.xml
48
- *,cover
49
- .hypothesis
50
-
51
- # Translations
52
- *.mo
53
- *.pot
54
-
55
- # Django stuff:
56
- *.log
57
-
58
- # Sphinx documentation
59
- docs/_build
60
-
61
- # PyBuilder
62
- target
63
-
64
- #Ipython Notebook
65
- .ipynb_checkpoints
66
-
67
- # SASS cache
68
- .sass-cache
69
- media_test
70
-
71
- # Rope project settings
72
- .ropeproject
73
-
74
- # Logs
75
- logs
76
- *.log
77
- npm-debug.log*
78
- yarn-debug.log*
79
- yarn-error.log*
80
-
81
- # Runtime data
82
- pids
83
- *.pid
84
- *.seed
85
- *.pid.lock
86
-
87
- # Directory for instrumented libs generated by jscoverage/JSCover
88
- lib-cov
89
-
90
- # Coverage directory used by tools like istanbul
91
- coverage
92
-
93
- # nyc test coverage
94
- .nyc_output
95
-
96
- # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
97
- .grunt
98
-
99
- # Bower dependency directory (https://bower.io/)
100
- bower_components
101
-
102
- # node-waf configuration
103
- .lock-wscript
104
-
105
- # Compiled binary addons (http://nodejs.org/api/addons.html)
106
- build/Release
107
-
108
- # Dependency directories
109
- node_modules
110
- jspm_packages
111
-
112
- # Typescript v1 declaration files
113
- typings
114
-
115
- # Optional npm cache directory
116
- .npm
117
-
118
- # Optional eslint cache
119
- .eslintcache
120
-
121
- # Optional REPL history
122
- .node_repl_history
123
-
124
- # Output of 'npm pack'
125
- *.tgz
126
-
127
- # Yarn Integrity file
128
- .yarn-integrity
129
-
130
- # dotenv environment variables file
131
- .env
132
- .env*
133
-
134
- # Sensitive Deploy Files
135
- deploy/eb/
136
-
137
- # tox
138
- ./.tox
139
-
140
- # Helm
141
- .helm-charts/
142
-
143
- # Docker
144
- Dockerfile
145
- .dockerignore
146
-
147
- # git
148
- .gitignore
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.github/ISSUE_TEMPLATE/01_bug_report.yml DELETED
@@ -1,92 +0,0 @@
1
- name: "Bug Report"
2
- description: "Report a technical or visual issue."
3
- labels: ["type: bug"]
4
- type: "Bug"
5
- body:
6
- - type: markdown
7
- attributes:
8
- value: |
9
- **Bug Report**
10
- Please fill out the form below with as much detail as possible.
11
- If the issue is visual, screenshots or videos are greatly appreciated.
12
- **Please review [our guide on reporting bugs](https://github.com/IFRCGo/go-web-app/blob/develop/CONTRIBUTING.md#reporting-bugs) before opening a new issue.**
13
-
14
- - type: input
15
- attributes:
16
- label: "Page URL"
17
- description: "The URL of the page where you encountered the issue."
18
- placeholder: "https://go.ifrc.org/"
19
- validations:
20
- required: true
21
-
22
- - type: dropdown
23
- attributes:
24
- label: "Environment"
25
- description: "Please select the environment where the bug occurred."
26
- options:
27
- - "Alpha"
28
- - "Staging"
29
- - "Production"
30
- validations:
31
- required: true
32
-
33
- - type: input
34
- attributes:
35
- label: "Browser"
36
- description: "Which browser are you using? (e.g., Chrome, Firefox, Safari)"
37
- placeholder: "Chrome"
38
- validations:
39
- required: true
40
-
41
- - type: textarea
42
- attributes:
43
- label: "Steps to Reproduce the Issue"
44
- description: |
45
- Please describe the issue in detail, including:
46
- 1. What actions led to the issue?
47
- 2. If possible, attach screenshots or videos demonstrating the problem.
48
- placeholder: |
49
- 1. I clicked on...
50
- 2. [Attach screenshots/videos if available]
51
- validations:
52
- required: true
53
-
54
- - type: textarea
55
- attributes:
56
- label: "Expected Behavior"
57
- description: "Describe what you expected to happen."
58
- placeholder: "I expected the page to..."
59
- validations:
60
- required: true
61
-
62
- - type: textarea
63
- attributes:
64
- label: "Actual Behavior"
65
- description: "Describe what actually happened, including any error messages."
66
- placeholder: "Instead, I saw..."
67
- validations:
68
- required: true
69
-
70
- - type: dropdown
71
- attributes:
72
- label: "Priority"
73
- description: "How urgent is this issue?"
74
- options:
75
- - "Low (Minor inconvenience)"
76
- - "Medium (Affects functionality, but there is a workaround)"
77
- - "High (Major functionality is broken)"
78
- - "Critical (Site is unusable)"
79
- validations:
80
- required: false
81
-
82
- - type: textarea
83
- attributes:
84
- label: "Additional Context (Optional)"
85
- description: |
86
- Provide any extra details, such as:
87
- - Related links.
88
- - Previous occurrences of this issue.
89
- - Workarounds you have tried.
90
- placeholder: "This issue also happened on [link]."
91
- validations:
92
- required: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.github/ISSUE_TEMPLATE/02_feature_request.yml DELETED
@@ -1,39 +0,0 @@
1
- name: "Feature Request"
2
- description: "Suggest a new idea or enhancement."
3
- labels: ["type: feature-request"]
4
- type: "Feature"
5
- body:
6
- - type: markdown
7
- attributes:
8
- value: |
9
- **Feature Request**
10
- Thank you for suggesting a new feature!
11
- Please provide as much detail as possible to help us understand and evaluate your idea.
12
- **Please review [our guide on suggesting enhancements](https://github.com/IFRCGo/go-web-app/blob/develop/CONTRIBUTING.md#suggesting-enhancements).**
13
-
14
- - type: textarea
15
- attributes:
16
- label: "Feature Description"
17
- description: |
18
- Describe your feature request in detail, including:
19
- - What the feature is.
20
- - Why it is needed and how it will improve the project.
21
- - How it will benefit users (e.g., As a user, I want to [do something] so that [desired outcome].).
22
- placeholder: "As a user, I want to filter search results by date so that I can quickly find recent information."
23
- validations:
24
- required: true
25
-
26
- - type: textarea
27
- attributes:
28
- label: "Additional Context"
29
- description: |
30
- Provide any extra details or supporting information, such as:
31
- - Links to references or related resources.
32
- - Examples from other projects or systems.
33
- - Screenshots, mockups, or diagrams.
34
- *Tip: You can attach files by clicking here and dragging them in.*
35
- placeholder: |
36
- Here's a link to a similar feature in another project: [link].
37
- I've also attached a mockup of what this could look like.
38
- validations:
39
- required: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.github/ISSUE_TEMPLATE/03_epic_request.yml DELETED
@@ -1,37 +0,0 @@
1
- name: "Epic"
2
- description: "Track a larger initiative with multiple related tasks and deliverables."
3
- labels: ["type: epic"]
4
- type: "Feature"
5
- body:
6
- - type: markdown
7
- attributes:
8
- value: |
9
- **Epic**
10
- Use this to define a large, overarching initiative.
11
- **Please review [our guide on suggesting enhancements](https://github.com/IFRCGo/go-web-app/blob/develop/CONTRIBUTING.md#suggesting-enhancements).**
12
-
13
- - type: textarea
14
- attributes:
15
- label: "Epic Summary"
16
- description: |
17
- Provide a clear and concise summary of the epic.
18
- - What is this epic about?
19
- - What problem does it solve or what goal does it achieve?
20
- - How does it align with the project’s objectives?
21
- placeholder: |
22
- Example:
23
- This epic focuses on implementing a new feature.
24
- validations:
25
- required: true
26
-
27
- - type: textarea
28
- attributes:
29
- label: "Additional Context or Resources"
30
- description: "Provide any additional information, links, or resources that will help the team understand and execute this epic."
31
- placeholder: |
32
- Examples:
33
- - Link to design mockups: [link]
34
- - Technical specs document: [link]
35
- - Reference to similar features: [link]
36
- validations:
37
- required: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.github/ISSUE_TEMPLATE/config.yml DELETED
@@ -1,5 +0,0 @@
1
- blank_issues_enabled: true
2
- contact_links:
3
- - name: Documentation
4
- url: https://go-wiki.ifrc.org/en/home
5
- about: Please consult the wiki to know more about IFRC GO.
 
 
 
 
 
 
go-web-app-develop/.github/dependabot.yml DELETED
@@ -1,27 +0,0 @@
1
- version: 2
2
- updates:
3
- - package-ecosystem: npm
4
- directory: /
5
- schedule:
6
- interval: weekly
7
- groups:
8
- eslint:
9
- patterns:
10
- - "*eslint*"
11
- vite:
12
- patterns:
13
- - "*vite*"
14
- postcss:
15
- patterns:
16
- - "*postcss*"
17
- stylelint:
18
- patterns:
19
- - "*stylelint*"
20
- all-other-dependencies:
21
- patterns:
22
- - "*"
23
- exclude-patterns:
24
- - "*eslint*"
25
- - "*vite*"
26
- - "*postcss*"
27
- - "*stylelint*"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.github/pull_request_template.md DELETED
@@ -1,30 +0,0 @@
1
- ## Summary
2
-
3
- Provide a brief description of what this PR addresses and its purpose.
4
-
5
- ## Addresses
6
-
7
- * Issue(s): *List related issues or tickets.*
8
-
9
- ## Depends On
10
-
11
- * Other PRs or Dependencies: *List PRs or dependencies this PR relies on.*
12
-
13
- ## Changes
14
-
15
- * Detailed list or prose of changes
16
- * Breaking changes
17
- * Changes to configurations
18
-
19
- ## This PR Ensures:
20
-
21
- * \[ ] No typos or grammatical errors
22
- * \[ ] No conflict markers left in the code
23
- * \[ ] No unwanted comments, temporary files, or auto-generated files
24
- * \[ ] No inclusion of secret keys or sensitive data
25
- * \[ ] No `console.log` statements meant for debugging
26
- * \[ ] All CI checks have passed
27
-
28
- ## Additional Notes
29
-
30
- *Optional: Add any other relevant context, screenshots, or details here.*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.github/workflows/add-issue-to-backlog.yml DELETED
@@ -1,16 +0,0 @@
1
- name: Add issues to Backlog
2
-
3
- on:
4
- issues:
5
- types:
6
- - opened
7
-
8
- jobs:
9
- add-to-project:
10
- name: Add issue to project
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/[email protected]
14
- with:
15
- project-url: https://github.com/orgs/IFRCGo/projects/12
16
- github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.github/workflows/chromatic.yml DELETED
@@ -1,127 +0,0 @@
1
- name: 'Chromatic'
2
-
3
- on:
4
- pull_request:
5
- push:
6
- branches:
7
- - develop
8
-
9
- concurrency:
10
- group: ${{ github.workflow }}-${{ github.ref }}-chromatic
11
- cancel-in-progress: true
12
-
13
- permissions:
14
- actions: write
15
- contents: read
16
- pages: write
17
- id-token: write
18
-
19
- jobs:
20
- changed-files:
21
- name: Check for changed files
22
- runs-on: ubuntu-latest
23
- outputs:
24
- all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }}
25
- any_changed: ${{ steps.changed-files.outputs.any_changed }}
26
- steps:
27
- - uses: actions/checkout@v4
28
- with:
29
- fetch-depth: 0
30
- - name: Get changed files
31
- id: changed-files
32
- uses: tj-actions/changed-files@v44
33
- with:
34
- files: |
35
- packages/ui/**
36
- packages/go-ui-storybook/**
37
- ui:
38
- name: Build UI Library
39
- environment: 'test'
40
- runs-on: ubuntu-latest
41
- needs: [changed-files]
42
- if: ${{ needs.changed-files.outputs.any_changed == 'true' }}
43
- defaults:
44
- run:
45
- working-directory: packages/ui
46
- steps:
47
- - uses: actions/checkout@v4
48
- with:
49
- fetch-depth: 0
50
- - name: Install pnpm
51
- uses: pnpm/action-setup@v4
52
- - name: Install Node.js
53
- uses: actions/setup-node@v4
54
- with:
55
- node-version: 20
56
- cache: 'pnpm'
57
- - name: Install dependencies
58
- run: pnpm install
59
- - name: Typecheck
60
- run: pnpm typecheck
61
- - name: Lint CSS
62
- run: pnpm lint:css
63
- - name: Lint JS
64
- run: pnpm lint:js
65
- - name: build UI library
66
- run: pnpm build
67
- - uses: actions/upload-artifact@v4
68
- with:
69
- name: ui-build
70
- path: packages/ui/dist
71
- chromatic:
72
- name: Chromatic Deploy
73
- runs-on: ubuntu-latest
74
- needs: [ui]
75
- steps:
76
- - uses: actions/checkout@v4
77
- with:
78
- fetch-depth: 0
79
- - name: Install pnpm
80
- uses: pnpm/action-setup@v4
81
- - name: Install Node.js
82
- uses: actions/setup-node@v4
83
- with:
84
- node-version: 20
85
- cache: 'pnpm'
86
- - name: Install dependencies
87
- run: pnpm install
88
- - uses: actions/download-artifact@v4
89
- with:
90
- name: ui-build
91
- path: packages/ui/dist
92
- - name: Run Chromatic
93
- uses: chromaui/action@v1
94
- with:
95
- exitZeroOnChanges: true
96
- exitOnceUploaded: true
97
- onlyChanged: true
98
- skip: "@(renovate/**|dependabot/**)"
99
- projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
100
- token: ${{ secrets.GITHUB_TOKEN }}
101
- autoAcceptChanges: "develop"
102
- workingDir: packages/go-ui-storybook
103
- github-pages:
104
- name: Deploy to Github Pages
105
- runs-on: ubuntu-latest
106
- needs: [ui]
107
- steps:
108
- - uses: actions/checkout@v4
109
- with:
110
- fetch-depth: 0
111
- - name: Install pnpm
112
- uses: pnpm/action-setup@v4
113
- - name: Install Node.js
114
- uses: actions/setup-node@v4
115
- with:
116
- node-version: 20
117
- cache: 'pnpm'
118
- - uses: actions/download-artifact@v4
119
- with:
120
- name: ui-build
121
- path: packages/ui/dist
122
- - uses: bitovi/[email protected]
123
- with:
124
- install_command: pnpm install
125
- build_command: pnpm build-storybook
126
- path: packages/go-ui-storybook/storybook-static
127
- checkout: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.github/workflows/ci.yml DELETED
@@ -1,304 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- pull_request:
5
- push:
6
- branches:
7
- - 'develop'
8
-
9
- env:
10
- APP_ADMIN_URL: ${{ vars.APP_ADMIN_URL }}
11
- APP_API_ENDPOINT: ${{ vars.APP_API_ENDPOINT }}
12
- APP_ENVIRONMENT: ${{ vars.APP_ENVIRONMENT }}
13
- APP_MAPBOX_ACCESS_TOKEN: ${{ vars.APP_MAPBOX_ACCESS_TOKEN }}
14
- APP_RISK_ADMIN_URL: ${{ vars.APP_RISK_ADMIN_URL }}
15
- APP_RISK_API_ENDPOINT: ${{ vars.APP_RISK_API_ENDPOINT }}
16
- APP_SENTRY_DSN: ${{ vars.APP_SENTRY_DSN }}
17
- APP_SENTRY_NORMALIZE_DEPTH: ${{ vars.APP_SENTRY_NORMALIZE_DEPTH }}
18
- APP_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.APP_SENTRY_TRACES_SAMPLE_RATE }}
19
- APP_SHOW_ENV_BANNER: ${{ vars.APP_SHOW_ENV_BANNER }}
20
- APP_TINY_API_KEY: ${{ vars.APP_TINY_API_KEY }}
21
- APP_TITLE: ${{ vars.APP_TITLE }}
22
- GITHUB_WORKFLOW: true
23
-
24
- concurrency:
25
- group: ${{ github.workflow }}-${{ github.ref }}
26
- cancel-in-progress: true
27
-
28
- jobs:
29
- ui:
30
- name: Build UI Library
31
- environment: 'test'
32
- runs-on: ubuntu-latest
33
- defaults:
34
- run:
35
- working-directory: packages/ui
36
- steps:
37
- - uses: actions/checkout@v4
38
- - name: Install pnpm
39
- uses: pnpm/action-setup@v4
40
- - name: Install Node.js
41
- uses: actions/setup-node@v4
42
- with:
43
- node-version: 20
44
- cache: 'pnpm'
45
- - name: Install dependencies
46
- run: pnpm install
47
-
48
- - name: Typecheck
49
- run: pnpm typecheck
50
-
51
- - name: Lint CSS
52
- run: pnpm lint:css
53
-
54
- - name: Lint JS
55
- run: pnpm lint:js
56
-
57
- - name: Build
58
- run: pnpm build
59
-
60
- - uses: actions/upload-artifact@v4
61
- with:
62
- name: ui-build
63
- path: packages/ui/dist
64
-
65
- test:
66
- name: Run tests
67
- environment: 'test'
68
- runs-on: ubuntu-latest
69
- defaults:
70
- run:
71
- working-directory: app
72
- needs: [ui]
73
- steps:
74
- - uses: actions/checkout@v4
75
- - name: Install pnpm
76
- uses: pnpm/action-setup@v4
77
- - name: Install Node.js
78
- uses: actions/setup-node@v4
79
- with:
80
- node-version: 20
81
- cache: 'pnpm'
82
- - name: Install dependencies
83
- run: pnpm install
84
-
85
- - uses: actions/download-artifact@v4
86
- with:
87
- name: ui-build
88
- path: packages/ui/dist
89
-
90
- - name: Run test
91
- run: pnpm test
92
-
93
- translation:
94
- continue-on-error: true
95
- name: Identify error with translation files
96
- runs-on: ubuntu-latest
97
- defaults:
98
- run:
99
- working-directory: app
100
- needs: [ui]
101
- steps:
102
- - uses: actions/checkout@v4
103
- - name: Install pnpm
104
- uses: pnpm/action-setup@v4
105
- - name: Install Node.js
106
- uses: actions/setup-node@v4
107
- with:
108
- node-version: 20
109
- cache: 'pnpm'
110
- - name: Install dependencies
111
- run: pnpm install
112
-
113
- - uses: actions/download-artifact@v4
114
- with:
115
- name: ui-build
116
- path: packages/ui/dist
117
-
118
- - name: Identify error with translation files
119
- run: pnpm lint:translation
120
-
121
- translation-migrations:
122
- if: |
123
- (github.event_name == 'pull_request' && github.base_ref == 'develop') ||
124
- (github.event_name == 'push' && github.ref == 'refs/heads/develop')
125
- continue-on-error: true
126
- name: Identify if translation migrations need to be generated
127
- runs-on: ubuntu-latest
128
- defaults:
129
- run:
130
- working-directory: app
131
- needs: [ui]
132
- steps:
133
- - uses: actions/checkout@v4
134
- - name: Install pnpm
135
- uses: pnpm/action-setup@v4
136
- - name: Install Node.js
137
- uses: actions/setup-node@v4
138
- with:
139
- node-version: 20
140
- cache: 'pnpm'
141
- - name: Install dependencies
142
- run: pnpm install
143
-
144
- - uses: actions/download-artifact@v4
145
- with:
146
- name: ui-build
147
- path: packages/ui/dist
148
-
149
- - name: Identify if translation migrations need to be generated
150
- run: |
151
- if pnpm translatte:generate; then
152
- # The step should fail if generation is possible
153
- exit 1
154
- fi
155
-
156
- unused:
157
- name: Identify unused files
158
- runs-on: ubuntu-latest
159
- needs: [ui]
160
- steps:
161
- - uses: actions/checkout@v4
162
- - name: Install pnpm
163
- uses: pnpm/action-setup@v4
164
- - name: Install Node.js
165
- uses: actions/setup-node@v4
166
- with:
167
- node-version: 20
168
- cache: 'pnpm'
169
- - name: Install dependencies
170
- run: pnpm install
171
-
172
- - name: Initialize types
173
- run: pnpm initialize:type
174
- working-directory: app
175
-
176
- - name: Identify unused files
177
- run: pnpm lint:unused
178
-
179
- lint:
180
- name: Lint JS
181
- runs-on: ubuntu-latest
182
- defaults:
183
- run:
184
- working-directory: app
185
- needs: [ui]
186
- steps:
187
- - uses: actions/checkout@v4
188
- - name: Install pnpm
189
- uses: pnpm/action-setup@v4
190
- - name: Install Node.js
191
- uses: actions/setup-node@v4
192
- with:
193
- node-version: 20
194
- cache: 'pnpm'
195
- - name: Install dependencies
196
- run: pnpm install
197
-
198
- - uses: actions/download-artifact@v4
199
- with:
200
- name: ui-build
201
- path: packages/ui/dist
202
-
203
- - name: Lint JS
204
- run: pnpm lint:js
205
-
206
- lint-css:
207
- name: Lint CSS
208
- runs-on: ubuntu-latest
209
- defaults:
210
- run:
211
- working-directory: app
212
- needs: [ui]
213
- steps:
214
- - uses: actions/checkout@v4
215
- - name: Install pnpm
216
- uses: pnpm/action-setup@v4
217
- - name: Install Node.js
218
- uses: actions/setup-node@v4
219
- with:
220
- node-version: 20
221
- cache: 'pnpm'
222
- - name: Install dependencies
223
- run: pnpm install
224
-
225
- - uses: actions/download-artifact@v4
226
- with:
227
- name: ui-build
228
- path: packages/ui/dist
229
-
230
- - name: Lint CSS
231
- run: pnpm lint:css
232
-
233
- # FIXME: Identify a way to generate schema before we run typecheck
234
- # typecheck:
235
- # name: Typecheck
236
- # runs-on: ubuntu-latest
237
- # steps:
238
- # - uses: actions/checkout@v4
239
- # - name: Install pnpm
240
- # uses: pnpm/action-setup@v4
241
- # - name: Install Node.js
242
- # uses: actions/setup-node@v4
243
- # with:
244
- # node-version: 20
245
- # cache: 'pnpm'
246
- # - name: Install dependencies
247
- # run: pnpm install
248
- #
249
- # - name: Typecheck
250
- # run: pnpm typecheck
251
-
252
- typos:
253
- name: Spell Check with Typos
254
- runs-on: ubuntu-latest
255
- steps:
256
- - name: Checkout Actions Repository
257
- uses: actions/checkout@v4
258
-
259
- - name: Check spelling
260
- uses: crate-ci/[email protected]
261
-
262
- build:
263
- name: Build GO Web App
264
- environment: 'test'
265
- runs-on: ubuntu-latest
266
- defaults:
267
- run:
268
- working-directory: app
269
- needs: [lint, lint-css, test, ui]
270
- steps:
271
- - uses: actions/checkout@v4
272
- - name: Install pnpm
273
- uses: pnpm/action-setup@v4
274
- - name: Install Node.js
275
- uses: actions/setup-node@v4
276
- with:
277
- node-version: 20
278
- cache: 'pnpm'
279
- - name: Install dependencies
280
- run: pnpm install
281
-
282
- - uses: actions/download-artifact@v4
283
- with:
284
- name: ui-build
285
- path: packages/ui/dist
286
-
287
- - name: Build
288
- run: pnpm build
289
-
290
- validate_helm:
291
- name: Validate Helm
292
- runs-on: ubuntu-latest
293
-
294
- steps:
295
- - uses: actions/checkout@main
296
-
297
- - name: Install Helm
298
- uses: azure/setup-helm@v4
299
-
300
- - name: Helm lint
301
- run: helm lint ./nginx-serve/helm --values ./nginx-serve/helm/values-test.yaml
302
-
303
- - name: Helm template
304
- run: helm template ./nginx-serve/helm --values ./nginx-serve/helm/values-test.yaml
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.github/workflows/publish-nginx-serve.yml DELETED
@@ -1,147 +0,0 @@
1
- name: Publish Helm
2
-
3
- on:
4
- workflow_dispatch:
5
- push:
6
- branches:
7
- - develop
8
- - project/*
9
-
10
- permissions:
11
- packages: write
12
-
13
-
14
- jobs:
15
- publish_image:
16
- name: Publish Docker Image
17
- runs-on: ubuntu-latest
18
-
19
- outputs:
20
- docker_image_name: ${{ steps.prep.outputs.tagged_image_name }}
21
- docker_image_tag: ${{ steps.prep.outputs.tag }}
22
- docker_image: ${{ steps.prep.outputs.tagged_image }}
23
-
24
- steps:
25
- - uses: actions/checkout@main
26
-
27
- - name: Login to GitHub Container Registry
28
- uses: docker/login-action@v3
29
- with:
30
- registry: ghcr.io
31
- username: ${{ github.actor }}
32
- password: ${{ secrets.GITHUB_TOKEN }}
33
-
34
- - name: 🐳 Prepare Docker
35
- id: prep
36
- env:
37
- IMAGE_NAME: ghcr.io/${{ github.repository }}
38
- run: |
39
- BRANCH_NAME=$(echo $GITHUB_REF_NAME | sed 's|[/:]|-|' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g' | cut -c1-100 | sed 's/-*$//')
40
-
41
- # XXX: Check if there is a slash in the BRANCH_NAME eg: project/add-docker
42
- if [[ "$BRANCH_NAME" == *"/"* ]]; then
43
- # XXX: Change the docker image package to -alpha
44
- IMAGE_NAME="$IMAGE_NAME-alpha"
45
- TAG="$(echo "$BRANCH_NAME" | sed 's|/|-|g').$(echo $GITHUB_SHA | head -c7)"
46
- else
47
- TAG="$BRANCH_NAME.$(echo $GITHUB_SHA | head -c7)"
48
- fi
49
-
50
- IMAGE_NAME=$(echo $IMAGE_NAME | tr '[:upper:]' '[:lower:]')
51
- echo "tagged_image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT
52
- echo "tag=${TAG}" >> $GITHUB_OUTPUT
53
- echo "tagged_image=${IMAGE_NAME}:${TAG}" >> $GITHUB_OUTPUT
54
- echo "::notice::Tagged docker image: ${IMAGE_NAME}:${TAG}"
55
-
56
- - name: 🐳 Set up Docker Buildx
57
- id: buildx
58
- uses: docker/setup-buildx-action@v3
59
-
60
- - name: 🐳 Cache Docker layers
61
- uses: actions/cache@v4
62
- with:
63
- path: /tmp/.buildx-cache
64
- key: ${{ runner.os }}-buildx-${{ github.ref }}
65
- restore-keys: |
66
- ${{ runner.os }}-buildx-refs/develop
67
- ${{ runner.os }}-buildx-
68
-
69
- - name: 🐳 Docker build
70
- uses: docker/build-push-action@v6
71
- with:
72
- context: .
73
- builder: ${{ steps.buildx.outputs.name }}
74
- file: nginx-serve/Dockerfile
75
- target: nginx-serve
76
- load: true
77
- push: true
78
- tags: ${{ steps.prep.outputs.tagged_image }}
79
- cache-from: type=local,src=/tmp/.buildx-cache
80
- cache-to: type=local,dest=/tmp/.buildx-cache-new
81
- build-args: |
82
- "APP_SENTRY_TRACES_SAMPLE_RATE=0.8"
83
- "APP_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0.8"
84
- "APP_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=0.8"
85
-
86
- - name: 🐳 Move docker cache
87
- run: |
88
- rm -rf /tmp/.buildx-cache
89
- mv /tmp/.buildx-cache-new /tmp/.buildx-cache
90
-
91
- publish_helm:
92
- name: Publish Helm
93
- needs: publish_image
94
- runs-on: ubuntu-latest
95
-
96
- steps:
97
- - name: Checkout code
98
- uses: actions/checkout@v4
99
-
100
- - name: Login to GitHub Container Registry
101
- uses: docker/login-action@v3
102
- with:
103
- registry: ghcr.io
104
- username: ${{ github.actor }}
105
- password: ${{ secrets.GITHUB_TOKEN }}
106
-
107
- - name: Install Helm
108
- uses: azure/setup-helm@v3
109
-
110
- - name: Tag docker image in Helm Chart values.yaml
111
- env:
112
- IMAGE_NAME: ${{ needs.publish_image.outputs.docker_image_name }}
113
- IMAGE_TAG: ${{ needs.publish_image.outputs.docker_image_tag }}
114
- run: |
115
- # Update values.yaml with latest docker image
116
- sed -i "s|SET-BY-CICD-IMAGE|$IMAGE_NAME|" nginx-serve/helm/values.yaml
117
- sed -i "s/SET-BY-CICD-TAG/$IMAGE_TAG/" nginx-serve/helm/values.yaml
118
-
119
- - name: Package Helm Chart
120
- id: set-variables
121
- run: |
122
- # XXX: Check if there is a slash in the BRANCH_NAME eg: project/add-docker
123
- if [[ "$GITHUB_REF_NAME" == *"/"* ]]; then
124
- # XXX: Change the helm chart to <chart-name>-alpha
125
- sed -i 's/^name: \(.*\)/name: \1-alpha/' nginx-serve/helm/Chart.yaml
126
- fi
127
-
128
- SHA_SHORT=$(git rev-parse --short HEAD)
129
- sed -i "s/SET-BY-CICD/$SHA_SHORT/g" nginx-serve/helm/Chart.yaml
130
- helm package ./nginx-serve/helm -d .helm-charts
131
-
132
- - name: Push Helm Chart
133
- env:
134
- IMAGE: ${{ needs.publish_image.outputs.docker_image }}
135
- OCI_REPO: oci://ghcr.io/${{ github.repository }}
136
- run: |
137
- OCI_REPO=$(echo $OCI_REPO | tr '[:upper:]' '[:lower:]')
138
- PACKAGE_FILE=$(ls .helm-charts/*.tgz | head -n 1)
139
- echo "# Helm Chart" >> $GITHUB_STEP_SUMMARY
140
- echo "" >> $GITHUB_STEP_SUMMARY
141
- echo "Tagged Image: **$IMAGE**" >> $GITHUB_STEP_SUMMARY
142
- echo "" >> $GITHUB_STEP_SUMMARY
143
- echo "Helm push output" >> $GITHUB_STEP_SUMMARY
144
- echo "" >> $GITHUB_STEP_SUMMARY
145
- echo '```bash' >> $GITHUB_STEP_SUMMARY
146
- helm push "$PACKAGE_FILE" $OCI_REPO >> $GITHUB_STEP_SUMMARY
147
- echo '```' >> $GITHUB_STEP_SUMMARY
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.github/workflows/publish-storybook-nginx-serve.yml DELETED
@@ -1,127 +0,0 @@
1
- name: Publish Storybook Helm
2
-
3
- on:
4
- workflow_dispatch:
5
- push:
6
- branches:
7
- - develop
8
-
9
- permissions:
10
- packages: write
11
-
12
-
13
- jobs:
14
- publish_image:
15
- name: 🐳 Publish Docker Image
16
- runs-on: ubuntu-latest
17
-
18
- outputs:
19
- docker_image_name: ${{ steps.prep.outputs.tagged_image_name }}
20
- docker_image_tag: ${{ steps.prep.outputs.tag }}
21
- docker_image: ${{ steps.prep.outputs.tagged_image }}
22
-
23
- steps:
24
- - uses: actions/checkout@main
25
-
26
- - name: Login to GitHub Container Registry
27
- uses: docker/login-action@v3
28
- with:
29
- registry: ghcr.io
30
- username: ${{ github.actor }}
31
- password: ${{ secrets.GITHUB_TOKEN }}
32
-
33
- - name: 🐳 Prepare Docker
34
- id: prep
35
- env:
36
- IMAGE_NAME: ghcr.io/${{ github.repository }}/go-ui-storybook
37
- run: |
38
- BRANCH_NAME=$(echo $GITHUB_REF_NAME | sed 's|[/:]|-|' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g' | cut -c1-100 | sed 's/-*$//')
39
- TAG="$BRANCH_NAME.$(echo $GITHUB_SHA | head -c7)"
40
- IMAGE_NAME=$(echo $IMAGE_NAME | tr '[:upper:]' '[:lower:]')
41
- echo "tagged_image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT
42
- echo "tag=${TAG}" >> $GITHUB_OUTPUT
43
- echo "tagged_image=${IMAGE_NAME}:${TAG}" >> $GITHUB_OUTPUT
44
- echo "::notice::Tagged docker image: ${IMAGE_NAME}:${TAG}"
45
-
46
- - name: 🐳 Set up Docker Buildx
47
- id: buildx
48
- uses: docker/setup-buildx-action@v3
49
-
50
- - name: 🐳 Cache Docker layers
51
- uses: actions/cache@v4
52
- with:
53
- path: /tmp/.buildx-cache
54
- key: ${{ runner.os }}-buildx-${{ github.ref }}
55
- restore-keys: |
56
- ${{ runner.os }}-buildx-refs/develop
57
- ${{ runner.os }}-buildx-
58
-
59
- - name: 🐳 Docker build
60
- uses: docker/build-push-action@v6
61
- with:
62
- context: .
63
- builder: ${{ steps.buildx.outputs.name }}
64
- file: packages/go-ui-storybook/nginx-serve/Dockerfile
65
- target: nginx-serve
66
- load: true
67
- push: true
68
- tags: ${{ steps.prep.outputs.tagged_image }}
69
- cache-from: type=local,src=/tmp/.buildx-cache
70
- cache-to: type=local,dest=/tmp/.buildx-cache-new
71
-
72
- - name: 🐳 Move docker cache
73
- run: |
74
- rm -rf /tmp/.buildx-cache
75
- mv /tmp/.buildx-cache-new /tmp/.buildx-cache
76
-
77
- publish_helm:
78
- name: ⎈ Publish Helm
79
- needs: publish_image
80
- runs-on: ubuntu-latest
81
-
82
- steps:
83
- - name: Checkout code
84
- uses: actions/checkout@v4
85
-
86
- - name: Login to GitHub Container Registry
87
- uses: docker/login-action@v3
88
- with:
89
- registry: ghcr.io
90
- username: ${{ github.actor }}
91
- password: ${{ secrets.GITHUB_TOKEN }}
92
-
93
- - name: ⎈ Install Helm
94
- uses: azure/setup-helm@v3
95
-
96
- - name: ⎈ Tag docker image in Helm Chart values.yaml
97
- env:
98
- IMAGE_NAME: ${{ needs.publish_image.outputs.docker_image_name }}
99
- IMAGE_TAG: ${{ needs.publish_image.outputs.docker_image_tag }}
100
- run: |
101
- # Update values.yaml with latest docker image
102
- sed -i "s|SET-BY-CICD-IMAGE|$IMAGE_NAME|" packages/go-ui-storybook/nginx-serve/helm/values.yaml
103
- sed -i "s/SET-BY-CICD-TAG/$IMAGE_TAG/" packages/go-ui-storybook/nginx-serve/helm/values.yaml
104
-
105
- - name: ⎈ Package Helm Chart
106
- id: set-variables
107
- run: |
108
- SHA_SHORT=$(git rev-parse --short HEAD)
109
- sed -i "s/SET-BY-CICD/$SHA_SHORT/g" packages/go-ui-storybook/nginx-serve/helm/Chart.yaml
110
- helm package ./packages/go-ui-storybook/nginx-serve/helm -d .helm-charts
111
-
112
- - name: ⎈ Push Helm Chart
113
- env:
114
- IMAGE: ${{ needs.publish_image.outputs.docker_image }}
115
- OCI_REPO: oci://ghcr.io/${{ github.repository }}
116
- run: |
117
- OCI_REPO=$(echo $OCI_REPO | tr '[:upper:]' '[:lower:]')
118
- PACKAGE_FILE=$(ls .helm-charts/*.tgz | head -n 1)
119
- echo "## 🚀 IFRC GO UI Helm Chart 🚀" >> $GITHUB_STEP_SUMMARY
120
- echo "" >> $GITHUB_STEP_SUMMARY
121
- echo "🐳 Tagged Image: **$IMAGE**" >> $GITHUB_STEP_SUMMARY
122
- echo "" >> $GITHUB_STEP_SUMMARY
123
- echo "⎈ Helm push output" >> $GITHUB_STEP_SUMMARY
124
- echo "" >> $GITHUB_STEP_SUMMARY
125
- echo '```bash' >> $GITHUB_STEP_SUMMARY
126
- helm push "$PACKAGE_FILE" $OCI_REPO >> $GITHUB_STEP_SUMMARY
127
- echo '```' >> $GITHUB_STEP_SUMMARY
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.gitignore DELETED
@@ -1,43 +0,0 @@
1
- # Logs
2
- logs
3
- *.log
4
- npm-debug.log*
5
- yarn-debug.log*
6
- yarn-error.log*
7
- pnpm-debug.log*
8
- lerna-debug.log*
9
-
10
- node_modules
11
- dist
12
- dist-ssr
13
- build
14
- build-ssr
15
- *.local
16
-
17
- # Editor directories and files
18
- .vscode/*
19
- !.vscode/extensions.json
20
- .idea
21
- .DS_Store
22
- *.suo
23
- *.ntvs*
24
- *.njsproj
25
- *.sln
26
- *.sw?
27
-
28
- .env*
29
- !.env.example
30
- .eslintcache
31
- tsconfig.tsbuildinfo
32
-
33
- # Custom ignores
34
-
35
- stats.html
36
- generated/
37
- coverage/
38
-
39
- # storybook build
40
- storybook-static/
41
-
42
- # Helm
43
- .helm-charts/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/.npmrc DELETED
@@ -1 +0,0 @@
1
- enable-pre-post-scripts=true
 
 
go-web-app-develop/COLLABORATING.md DELETED
@@ -1,18 +0,0 @@
1
- # IFRC GO Collaboration Guide
2
-
3
- This document offers guidelines for collaborators on codebase maintenance, testing, building and deployment, and issue management.
4
-
5
- ## Repository
6
-
7
- * [Issues and Pull Requests](./collaborating/issues-and-pull-requests.md)
8
- * [Structure](./collaborating/repository-structure.md)
9
- * [Linting](./collaborating/linting.md)
10
- * [Technology Used](./collaborating/technology.md)
11
-
12
- ## Development
13
-
14
- * [Developing](./collaborating/developing.md)
15
- * [Translation](./collaborating/translation.md)
16
- * [Building](./collaborating/building.md)
17
- * [Testing](./collaborating/testing.md)
18
- * [Release](./collaborating/release.md)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/CONTRIBUTING.md DELETED
@@ -1,81 +0,0 @@
1
- # IFRC GO Web Application Contributing Guide
2
-
3
- First off, thanks for taking the time to contribute! ❤️
4
-
5
- All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution.
6
-
7
- ## Table of Contents
8
-
9
- * [I Have a Question](#i-have-a-question)
10
- * [I Want To Contribute](#i-want-to-contribute)
11
- * [What should I know before I get started?](#what-should-i-know-before-i-get-started)
12
- * [Reporting Bugs](#reporting-bugs)
13
- * [Suggesting Enhancements](#suggesting-enhancements)
14
- * [Becoming a Collaborator](#becoming-a-collaborator)
15
-
16
- ## I Have a Question
17
-
18
- > If you want to ask a question, we assume that you have read the available [documentation](https://go-wiki.ifrc.org/en/home).
19
-
20
- Before you ask a question, it is best to search for existing [issues](https://github.com/IFRCGo/go-web-app/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue.
21
-
22
- If you then still feel the need to ask a question and need clarification, we recommend the following:
23
-
24
- * Open a [discussion](https://github.com/IFRCGo/go-web-app/discussions).
25
- * Open an [issue](https://github.com/IFRCGo/go-web-app/issues/new/choose).
26
- * Provide as much context as you can about what you're running into.
27
-
28
- ## I Want To Contribute
29
-
30
- Any individual is welcome to contribute to IFRC GO. The repository currently has two kinds of contribution personas:
31
-
32
- * A **Contributor** is any individual who creates an issue/PR, comments on an issue/PR, or contributes in some other way.
33
- * A **Collaborator** is a contributor with write access to the repository.
34
-
35
- ### What should I know before I get started?
36
-
37
- ### IFRC GO and Packages
38
-
39
- The project is hosted at <https://go.ifrc.org/>.
40
-
41
- The project comprises several [repositories](https://github.com/orgs/IFRCGo/repositories), with notable ones including:
42
-
43
- * [go-web-app](https://github.com/IFRCGo/go-web-app/) - The frontend repository for the IFRC GO project.
44
- * [go-api](https://github.com/IFRCGo/go-api) - The backed repository for the IFRC GO project.
45
-
46
- ### Reporting Bugs
47
-
48
- #### Before Submitting a Bug Report
49
-
50
- Ensure the issue is not a user error by reviewing the documentation. Check the [existing bug reports](https://github.com/IFRCGo/go-web-app/issues?q=is%3Aissue%20state%3Aopen%20type%3ABug) to confirm if the issue has already been reported.
51
-
52
- #### Submitting the Bug Report
53
-
54
- 1. Open a new [Issue](https://github.com/IFRCGo/go-web-app/issues/new?q=is%3Aissue+state%3Aopen+type%3ABug\&template=01_bug_report.yml).
55
- 2. Provide all relevant details.
56
-
57
- #### After Submitting the Issue
58
-
59
- * The team will categorize and attempt to reproduce the issue.
60
- * If reproducible, the team will work on resolving the bug.
61
-
62
- ### Suggesting Enhancements
63
-
64
- #### Before Submitting an Enhancement
65
-
66
- * Review the [documentation](https://go-wiki.ifrc.org/en/home) to ensure the functionality isn't already covered.
67
- * Perform a [search](https://github.com/IFRCGo/go-web-app/issues) to check if the enhancement has been suggested. If so, comment on the existing issue.
68
- * Confirm that your suggestion aligns with the project’s scope and objectives.
69
-
70
- #### How to Submit an Enhancement Suggestion
71
-
72
- Enhancements are tracked as [GitHub issues](https://github.com/IFRCGo/go-web-app/issues).
73
-
74
- * Open a new [feature request](https://github.com/IFRCGo/go-web-app/issues/new?q=is%3Aissue+state%3Aopen+type%3ABug\&template=02_feature_request.yml) or [Epic ticket](https://github.com/IFRCGo/go-web-app/issues/new?q=is%3Aissue+state%3Aopen+type%3ABug\&template=03_epic_request.yml) depending on the scale of the enhancement.
75
- * Provide a clear description and submit the ticket.
76
-
77
- ## Becoming a Collaborator
78
-
79
- Collaborators are key members of the IFRC GO Web Application Team, responsible for its development. Members should have expertise in modern web technologies and standards.
80
-
81
- For detailed guidelines, refer to the [Collaboration Guide](./COLLABORATING.md).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2023 GO
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/README.md DELETED
@@ -1,117 +0,0 @@
1
- <p align="center">
2
- <br />
3
- <a href="https://go.ifrc.org/">
4
- <picture>
5
- <img src="https://github.com/IFRCGo/go-web-app/blob/develop/app/src/assets/icons/go-logo-2020.svg" width="200px" alt="IFRC GO Logo">
6
- </picture>
7
- </a>
8
- </p>
9
-
10
- # IFRC GO
11
-
12
- [IFRC GO](https://go.ifrc.org/) is the platform of the International Federation of Red Cross and Red Crescent, aimed at connecting crucial information on emergency needs with the appropriate response. This repository houses the frontend source code for the application, developed using [React](https://react.dev/), [Vite](https://vitejs.dev/), and associated technologies.
13
-
14
- ## Built With
15
-
16
- [![React][react-shields]][react-url] [![Vite][vite-shields]][vite-url] [![TypeScript][typescript-shields]][typescript-url] [![pnpm][pnpm-shields]][pnpm-url]
17
-
18
- ## Getting Started
19
-
20
- Below are the steps to guide you through preparing your local environment for IFRC GO Web application development. The repository is set up as a [monorepo](https://monorepo.tools/). The [app](https://github.com/IFRCGo/go-web-app/tree/develop/app) directory houses the application code, while the [packages](https://github.com/IFRCGo/go-web-app/tree/develop/packages) directory contains related packages, including the [IFRC GO UI](https://www.npmjs.com/package/@ifrc-go/ui) components library.
21
-
22
- ### Prerequisites
23
-
24
- To begin, ensure you have network access. Then, you'll need the following:
25
-
26
- 1. [Git](https://git-scm.com/)
27
- 2. [Node.js](https://nodejs.org/en/) as specified under `engines` section in `package.json` file
28
- 3. [pnpm](https://pnpm.io/) as specified under `engines` section in `package.json` file
29
- 4. Alternatively, you can use [Docker](https://www.docker.com/) to build the application.
30
-
31
- > \[!NOTE]\
32
- > Make sure the correct versions of pnpm and Node.js are installed. They are specified under `engines` section in `package.json` file.
33
-
34
- ### Local Development
35
-
36
- 1. Clone the repository using HTTPS, SSH, or GitHub CLI:
37
-
38
- ```bash
39
- git clone https://github.com/IFRCGo/go-web-app.git # HTTPS
40
- git clone [email protected]:IFRCGo/go-web-app.git # SSH
41
- gh repo clone IFRCGo/go-web-app # GitHub CLI
42
- ```
43
-
44
- 2. Install the dependencies:
45
-
46
- ```bash
47
- pnpm install
48
- ```
49
-
50
- 3. Create a `.env` file in the `app` directory and add variables from [env.ts](https://github.com/IFRCGo/go-web-app/blob/develop/app/env.ts). Any variables marked with `.optional()` are not mandatory for setup and can be skipped.
51
-
52
- ```bash
53
- cd app
54
- touch .env
55
- ```
56
-
57
- Example `.env` file
58
- ```
59
- APP_TITLE=IFRC GO
60
- APP_ENVIRONMENT=testing
61
- ...
62
- ```
63
-
64
- 4. Start the development server:
65
-
66
- ```bash
67
- pnpm start:app
68
- ```
69
-
70
- ## Contributing
71
-
72
- * Check out existing [Issues](https://github.com/IFRCGo/go-web-app/issues) and [Pull Requests](https://github.com/IFRCGo/go-web-app/pulls) to contribute.
73
- * To request a feature or report a bug, [create a GitHub Issue](https://github.com/IFRCGo/go-web-app/issues/new/choose).
74
- * [Contribution Guide →](./CONTRIBUTING.md)
75
- * [Collaboration Guide →](./COLLABORATING.md)
76
-
77
- ## Additional Packages
78
-
79
- The repository hosts multiple packages under the `packages` directory.
80
-
81
- 1. [IFRC GO UI](https://github.com/IFRCGo/go-web-app/tree/develop/packages/ui) is a React UI components library tailored to meet the specific requirements of the IFRC GO community and its associated projects.
82
- 2. [IFRC GO UI Storybook](https://github.com/IFRCGo/go-web-app/tree/develop/packages/go-ui-storybook) serves as the comprehensive showcase for the IFRC GO UI components library. It is hosted on [Chromatic](https://66557be6b68dacbf0a96db23-zctxglhsnk.chromatic.com/).
83
-
84
- ## IFRC GO Backend
85
-
86
- The backend that serves the frontend application is maintained in a separate [repository](https://github.com/IFRCGo/go-api/).
87
-
88
- ## Previous Repository
89
-
90
- [Go Frontend](https://github.com/IFRCGo/go-frontend) is the previous version of the project which contains the original codebase and project history.
91
-
92
- ## Community & Support
93
-
94
- * Visit the [IFRC GO Wiki](https://go-wiki.ifrc.org/) for documentation of the IFRC GO platform.
95
- * Stay informed about the latest project updates on [Medium](https://ifrcgoproject.medium.com/).
96
-
97
- ## License
98
-
99
- [MIT](https://github.com/IFRCGo/go-web-app/blob/develop/LICENSE)
100
-
101
- <!-- MARKDOWN LINKS & IMAGES -->
102
-
103
- [react-shields]: https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB
104
-
105
- [react-url]: https://reactjs.org/
106
-
107
- [vite-shields]: https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white
108
-
109
- [vite-url]: https://vitejs.dev/
110
-
111
- [typescript-shields]: https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white
112
-
113
- [typescript-url]: https://www.typescriptlang.org/
114
-
115
- [pnpm-shields]: https://img.shields.io/badge/pnpm-F69220?style=for-the-badge&logo=pnpm&logoColor=fff
116
-
117
- [pnpm-url]: https://pnpm.io/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/CHANGELOG.md DELETED
@@ -1,729 +0,0 @@
1
- # go-web-app
2
-
3
- ## 7.21.0-beta.2
4
-
5
- ### Patch Changes
6
-
7
- - b949fcd: Fix use of operational timeframe date in imminent final report form
8
-
9
- ## 7.21.0-beta.1
10
-
11
- ### Patch Changes
12
-
13
- - 84b4802: - Fix calculation of Operation End date in Final report form
14
- - Fix icon position issue in the implementation table of DREF PDF export
15
- - Update the label for last update date in the crisis categorization pop-up
16
-
17
- ## 7.21.0-beta.0
18
-
19
- ### Minor Changes
20
-
21
- - 039c488: Add Crisis categorization update date
22
-
23
- - Add updated date for crisis categorization in emergency page.
24
- - Add consent checkbox over situational overview in field report form.
25
-
26
- - 3ee9979: Add support for DREF imminent v2 in final report
27
-
28
- - Add a separate route for the old dref final report form
29
- - Update dref final report to accomodate imminent v2 changes
30
-
31
- ## 7.20.2
32
-
33
- ### Patch Changes
34
-
35
- - 8090b9a: Fix other action section visibility condition in DREF export
36
-
37
- ## 7.20.1
38
-
39
- ### Patch Changes
40
-
41
- - 4418171: Fix DREF form to properly save major coordination mechanism [#1928](https://github.com/IFRCGo/go-web-app/issues/1928)
42
-
43
- ## 7.20.1-beta.0
44
-
45
- ### Patch Changes
46
-
47
- - 4418171: Fix DREF form to properly save major coordination mechanism [#1928](https://github.com/IFRCGo/go-web-app/issues/1928)
48
-
49
- ## 7.20.0
50
-
51
- ### Minor Changes
52
-
53
- - 5771a6b: Update DREF application form and export
54
-
55
- - add new field hazard date and location
56
- - update hazard date as forcasted day of event
57
- - update the section in dref application export
58
- - remove Current National Society Actions from the export
59
-
60
- ## 7.20.0-beta.0
61
-
62
- ### Minor Changes
63
-
64
- - 5771a6b: Update DREF application form and export
65
-
66
- - add new field hazard date and location
67
- - update hazard date as forcasted day of event
68
- - update the section in dref application export
69
- - remove Current National Society Actions from the export
70
-
71
- ## 7.19.0
72
-
73
- ### Minor Changes
74
-
75
- - 456a145: Fix versioning
76
-
77
- ### Patch Changes
78
-
79
- - 47786f8: Fix the undefined society name issue in surge page [#1899](https://github.com/IFRCGo/go-web-app/issues/1899)
80
-
81
- ## 7.18.2
82
-
83
- ### Patch Changes
84
-
85
- - e51a80f: Update the action for the DREF Ops update form for imminent.
86
- - Remove change to response modal in the ops update form for type imminent.
87
- - Fix the order of the field in operational timeframe tab.
88
- - Add description text under upload assessment report button in DREF operation update form
89
- - Fix the error while viewing PER process [#1838](https://github.com/IFRCGo/go-web-app/issues/1838).
90
-
91
- ## 7.18.1
92
-
93
- ### Patch Changes
94
-
95
- - 75bf525: Fix logic to disable ops update for old imminents
96
-
97
- ## 7.18.0
98
-
99
- ### Minor Changes
100
-
101
- - bfcaecf: Address [Dref imminent Application](https://github.com/IFRCGo/go-web-app/issues/1455)
102
- - Update logic for creation of dref final report for imminent
103
- - Update allocatioon form for dref imminent
104
- - Add Activity input in proposed action for dref type imminent
105
- - Add proposed actions icons
106
- - Show proposed actions for existing imminent dref applications
107
- - Hide unused sections for dref imminent export and preserve proposed actions order
108
- - Prevent selection of past dates for the `hazard_date` in dref imminent
109
- - Add auto total population calculation in dref
110
- - Add a confirmation popup before creating ops. update from imminent dref
111
-
112
- ### Patch Changes
113
-
114
- - ee1bd60: Add proper redirect for Non-sovereign country in the country ongoing emergencies page
115
- - 771d085: Community Based Surveillance updates (Surge CoS Health)
116
- - Changed page: https://go.ifrc.org/surge/catalogue/health/community-based-surveillance
117
- - The changes affect team size and some standard components (e.g. kit content)
118
- - Updated dependencies [bfcaecf]
119
- - @ifrc-go/[email protected]
120
-
121
- ## 7.17.4
122
-
123
- ### Patch Changes
124
-
125
- - 14a7f2c: Update People assisted field label in the export of Dref final report.
126
-
127
- ## 7.17.3
128
-
129
- ### Patch Changes
130
-
131
- - fc8b427: Update field label in DrefFinalReport form and export
132
-
133
- ## 7.17.2
134
-
135
- ### Patch Changes
136
-
137
- - 54df6ff: Update DREF final report form
138
-
139
- - The DREF final report form and export now include a new "Assisted Population" field, replacing the "Targeted Population" field.
140
-
141
- ## 7.17.1
142
-
143
- ### Patch Changes
144
-
145
- - 215030a: Update DREF forms
146
-
147
- - Move Response strategy description from placeholder to below the input
148
- - Add DREF allocation field in event details for the Loan type Ops. update form
149
-
150
- ## 7.17.0
151
-
152
- ### Minor Changes
153
-
154
- - 0b351d1: Address [DREF Superticket 2 bugs](https://github.com/IFRCGo/go-web-app/issues/1784)
155
-
156
- - Update no of images in for "Description of event" from 2 to 4
157
- - Update descriptions of few fields
158
- - Replace \* with bullet in description of planned interventions in DREF import
159
- - Add some of the missing fields to exports
160
- - Remove warnings for previously removed fields
161
-
162
- ## 7.16.2
163
-
164
- ### Patch Changes
165
-
166
- - c086629: Update Learn > Resources > Montandon page
167
- - Update styling of 'API Access' buttons
168
- - Reword 'Access API' link to 'Access Montandon API'
169
- - Reword 'Explore Radiant Earth API' to 'Explore data in STAC browser'
170
- - 2ee6a1e: Remove a broken image from Catalogue of Surge Services > Health > ERU Hospital page
171
-
172
- ## 7.16.1
173
-
174
- ### Patch Changes
175
-
176
- - d561dc4: - Update Montandon landing page - Fix typo in Justin's name and email - Update description
177
- - Fix position and deploying organisation in ongoing RR deployment table
178
-
179
- ## 7.16.0
180
-
181
- ### Minor Changes
182
-
183
- - 9dcdd38: Add Montandon landing page
184
-
185
- - Add a basic landing page for Montandon with links and information
186
- - Add link to Montandon landing page to Learn > Resources menu
187
-
188
- ## 7.15.0
189
-
190
- ### Minor Changes
191
-
192
- - c26bda4: Implement [ERU Readiness](https://github.com/IFRCGo/go-web-app/issues/1710)
193
-
194
- - Restucture surge page to acommodate ERU
195
- - Move surge deployment related sections to a new dedicated tab **Active Surge Deployments**
196
- - Update active deployments to improve scaling of points in the map
197
- - Add **Active Surge Support per Emergency** section
198
- - Revamp **Surge Overview** tab
199
- - Add **Rapid Response Personnel** sub-tab
200
- - Update existings charts and add new related tables/charts
201
- - Add **Emergency Response Unit** sub-tab
202
- - Add section to visualize ERU capacity and readiness
203
- - Add section to view ongoing ERU deployments
204
- - Add a form to update ERU Readiness
205
- - Add option to export ERU Readiness data
206
- - Update **Respond > Surge/Deployments** menu to include **Active Surge Deployments**
207
-
208
- - 9ed8181: Address feedbacks in [DREF superticket feedbacks](https://github.com/IFRCGo/go-web-app/issues/1816)
209
-
210
- - Make end date of operation readonly field in all DREF forms
211
- - Fix font and spacing issues in the DREF exports (caused by link text overflow)
212
- - Update styling of Risk and Security Considerations section to match that of Previous Operations
213
- - Update visibility condition of National Society Actions in Final Report export
214
-
215
- ### Patch Changes
216
-
217
- - Updated dependencies [c26bda4]
218
- - @ifrc-go/[email protected]
219
-
220
- ## 7.14.0
221
-
222
- ### Minor Changes
223
-
224
- - 18ccc85:
225
- - Update styling of vertical NavigationTab
226
- - Hide register URL in the T&C page for logged in user
227
- - Update styling of T&C page
228
- - Make the page responsive
229
- - Make sidebar sticky
230
- - Update url for [monty docs](https://github.com/IFRCGo/go-web-app/issues/1418#issuecomment-2422371363)
231
- - 8d3a7bd: Initiate shutdown for 3W
232
- - Remove "Submit 3W Projects" from the menu Prepare > Global 3W projects
233
- - Rename "Global 3W Projects" to "Programmatic Partnerships" in Prepare menu
234
- - Update global 3W page
235
- - Update title and description for Programmatic Partnerships
236
- - Remove all the contents related to 3W
237
- - Replace contents in various places with project shutdown message
238
- - Regional 3W tab
239
- - 3W Projects section in Accounts > My Form > 3W
240
- - Projects tab in Country > Ongoing Activities
241
- - All Projects page
242
- - New, edit 3W project form
243
- - View 3W project page
244
- - Remove NS Activities section in Country > NS overview > NS Activities page
245
- - Remove Projects section from search results page
246
-
247
- ### Patch Changes
248
-
249
- - Updated dependencies [18ccc85]
250
- - @ifrc-go/[email protected]
251
-
252
- ## 7.13.0
253
-
254
- ### Minor Changes
255
-
256
- - 69fd74f: - Update page title for Emergency to include the name
257
- - Update page title of Flash update to include the name
258
- - Fix the user registration link in the Terms & Condition page
259
- - 680c673: Implement [DREF Superticket 2.0](https://github.com/IFRCGo/go-web-app/issues/1695)
260
-
261
- ### Patch Changes
262
-
263
- - fe4b727: - Upgrade pnpm to v10.6.1
264
- - Cleanup Dockerfile
265
- - Configure depandabot to track other dependencies updates
266
- - Upgrade eslint
267
- - Use workspace protocol to reference workspace packages
268
- - 9f20016: Enable user to edit their position field in [#1647](https://github.com/IFRCGo/go-web-app/issues/1647)
269
- - ef15af1: Add secondary ordering in tables for rows with same date
270
- - Updated dependencies [fe4b727]
271
- - @ifrc-go/[email protected]
272
-
273
- ## 7.12.1
274
-
275
- ### Patch Changes
276
-
277
- - Fix nullable type of assessment for NS capacity
278
-
279
- ## 7.12.0
280
-
281
- ### Minor Changes
282
-
283
- - f766bc7: Add link to IFRC Survey Designer in the tools section under learn menu
284
-
285
- ### Patch Changes
286
-
287
- - 7f51854: - Surge CoS: Health fix
288
- - 3a1cac8: Hide focal point details based on user permissions
289
- - 43d3bf1: - Add Surge CoS Administration section
290
- - Add Surge CoS Faecal Sludge Management (FSM) section
291
- - Update Surge CoS IT&T section
292
- - Update Surge CoS Basecamp section (as OSH)
293
-
294
- ## 7.11.1
295
-
296
- ### Patch Changes
297
-
298
- - ff426cd: Use current language for field report title generation
299
-
300
- ## 7.11.0
301
-
302
- ### Minor Changes
303
-
304
- - Field report number generation: Change only when the country or event changes
305
-
306
- ## 7.10.1
307
-
308
- ### Patch Changes
309
-
310
- - 14567f1: Improved tables by adding default and second-level ordering in [#1633](https://github.com/IFRCGo/go-web-app/issues/1633)
311
-
312
- - Appeal Documents table, `emergencies/{xxx}/reports` page
313
- - Recent Emergencies in Regions – All Appeals table
314
- - All Deployed Personnel – Default sorting (filters to be added)
315
- - Deployed ERUs – Changed filter title
316
- - Key Documents tables in Countries
317
- - Response documents
318
- - Main page – Active Operations table
319
- - The same `AppealsTable` is used in:
320
- - Active Operations in Regions
321
- - Previous Operations in Countries
322
-
323
- - 78d25b2:
324
-
325
- - Update on the ERU MHPSS Module in the Catalogue of Services in [#1648](https://github.com/IFRCGo/go-web-app/issues/1648)
326
- - Update on a PER role profile in [#1648](https://github.com/IFRCGo/go-web-app/issues/1648)
327
- - Update link to the IM Technical Competency Framework in [#1483](https://github.com/IFRCGo/go-web-app/issues/1483)
328
-
329
- - 44623a7: Undo DREF Imminent changes
330
- - b57c453: Show the number of people assisted in the DREF Final Report export in [#1665](https://github.com/IFRCGo/go-web-app/issues/1665)
331
-
332
- ## 7.10.0
333
-
334
- ### Minor Changes
335
-
336
- - 4f89133: Fix DREF PGA export styling
337
-
338
- ## 7.9.0
339
-
340
- ### Minor Changes
341
-
342
- - 7927522: Update Imminent DREF Application in [#1455](https://github.com/IFRCGo/go-web-app/issues/1455)
343
-
344
- - Hide sections/fields
345
- - Rename sections/fields
346
- - Remove sections/fields
347
- - Reflect changes in the PDF export
348
-
349
- ### Patch Changes
350
-
351
- - Updated dependencies [4032688]
352
- - @ifrc-go/[email protected]
353
-
354
- ## 7.8.1
355
-
356
- ### Patch Changes
357
-
358
- - 9c51dee: Remove `summary` field from field report form
359
- - Update @ifrc-go/ui version
360
-
361
- ## 7.8.0
362
-
363
- ### Minor Changes
364
-
365
- - 4843cb0: Added Operational Learning 2.0
366
-
367
- - Key Figures Overview in Operational Learning
368
- - Map View for Operational Learning
369
- - Learning by Sector Bar Chart
370
- - Learning by Region Bar Chart
371
- - Sources Over Time Line Chart
372
- - Methodology changes for the prioritization step
373
- - Added an option to regenerate cached summaries
374
- - Summary post-processing and cleanup
375
- - Enabled MDR code search in admin
376
-
377
- ### Patch Changes
378
-
379
- - f96e177: Move field report/emergency title generation logic from client to server
380
- - e85fc32: Integrate `crate-ci/typos` for code spell checking
381
- - 4cdea2b: Add redirection logic for `preparedness#operational-learning`
382
- - 9a50443: Add appeal doc type for appeal documents
383
- - 817d56d: Display properly formatted appeal type in search results
384
- - 1159fa4: Redirect obsolete URLs to recent ones
385
- - redirect `/reports/` to `/field-reports/`
386
- - redirect `/deployments/` -> `/surge/overview`
387
- - Updated dependencies [4843cb0]
388
- - @ifrc-go/[email protected]
389
-
390
- ## 7.7.0
391
-
392
- ### Minor Changes
393
-
394
- - 3258b96: Add local unit validation workflow
395
-
396
- ### Patch Changes
397
-
398
- - Updated dependencies [c5a446f]
399
- - @ifrc-go/[email protected]
400
-
401
- ## 7.6.6
402
-
403
- ### Patch Changes
404
-
405
- - 8cdc946: Hide Local unit contact details on the list view for logged in users in [#1485](https://github.com/ifRCGo/go-web-app/issues/1485)
406
- Update `tinymce-react` plugin to the latest version and enabled additional plugins, including support for lists in [#1481](https://github.com/ifRCGo/go-web-app/issues/1481)
407
- - ecca810: Replace the from-communication-copied text of CoS Health header
408
- - 7cf2514: Prioritize GDACS as the Primary Source for Imminent Risk Watch in [#1547](https://github.com/IFRCGo/go-web-app/issues/1547)
409
- - 8485076: Add Organization type and Learning type filter in Operational learning in [#1469](https://github.com/IFRCGo/go-web-app/issues/1469)
410
- - 766d98d: Auto append https:// for incomplete URLs in [#1505](https://github.com/IFRCGo/go-web-app/issues/1505)
411
-
412
- ## 7.6.5
413
-
414
- ### Patch Changes
415
-
416
- - 478e73b: Update labels for severity control in Imminent Risk Map
417
- Update navigation for the events in Imminent Risk Map
418
- Fix issue displayed when opening a DREF import template
419
- Fix submission issue when importing a DREF import file
420
- - f82f846: Update Health Section in Catalogue of Surge Services
421
- - ade84aa: Display ICRC Presence
422
- - Display ICRC presence across partner countries
423
- - Highlight key operational countries
424
-
425
- ## 7.6.4
426
-
427
- ### Patch Changes
428
-
429
- - d85f64d: Update Imminent Events
430
-
431
- - Hide WFP ADAM temporarily from list sources
432
- - Show exposure control for cyclones from GDACS only
433
-
434
- ## 7.6.3
435
-
436
- ### Patch Changes
437
-
438
- - 7bbf3d2: Update key insights disclaimer text in Ops. Learning
439
- - 0e40681: Update FDRS data in Country / Context and Structure / NS indicators
440
-
441
- - Add separate icon for each field for data year
442
- - Use separate icon for disaggregation
443
- - Update descriptions on dref import template (more details on _Missing / to be implemented_ section in https://github.com/IFRCGo/go-web-app/pull/1434#issuecomment-2459034932)
444
-
445
- - Updated dependencies [801ec3c]
446
- - @ifrc-go/[email protected]
447
-
448
- ## 7.6.2
449
-
450
- ### Patch Changes
451
-
452
- - 4fa6a36: Updated PER terminology and add PER logo in PER PDF export
453
- - 813e93f: Add link to GO UI storybook in resources page
454
- - 20dfeb3: Update DREF import template
455
- - Update guidance
456
- - Improve template stylings
457
- - Update message in error popup when import fails
458
- - 8a18ad8: Add beta tag, URL redirect, and link to old dashboard on Ops Learning
459
-
460
- ## 7.6.1
461
-
462
- ### Patch Changes
463
-
464
- - 7afaf34: Fix null event in appeal for operational learning
465
-
466
- ## 7.6.0
467
-
468
- ### Minor Changes
469
-
470
- - Add new Operational Learning Page
471
-
472
- - Add link to Operational Learning page under `Learn` navigation menu
473
- - Integrate LLM summaries for Operational Learning
474
-
475
- ## 7.5.3
476
-
477
- ### Patch Changes
478
-
479
- - d7f5f53: Revamp risk imminent events for cyclone
480
- - Visualize storm position, forecast uncertainty, track line and exposed area differently
481
- - Add option to toggle visibility of these different layers
482
- - Add severity legend for exposure
483
- - Update styling for items in event list
484
- - Update styling for event details page
485
- - 36a64fa: Integrate multi-select functionality in operational learning filters to allow selection of multiple filter items.
486
- - 894d00c: Add a new 404 page
487
- - 7757e54: Add an option to download excel import template for DREF (Response) which user can fill up and import.
488
- - a8d021d: Update resources page
489
- - Add a new video for LocalUnits
490
- - Update ordering of videos
491
- - aea512d: Prevent users from pasting images into rich text field
492
- - fd54657: Add Terms and Conditions page
493
- - bf55ccc: Add Cookie Policy page
494
- - df80c4f: Fix contact details in Field Report being always required when filled once
495
- - 81dc3bd: Added color mapping based on PER Area and Rating across all PER charts
496
- - Updated dependencies [dd92691]
497
- - Updated dependencies [d7f5f53]
498
- - Updated dependencies [fe6a455]
499
- - Updated dependencies [81dc3bd]
500
- - @ifrc-go/[email protected]
501
-
502
- ## 7.5.2
503
-
504
- ### Patch Changes
505
-
506
- - 37bba31: Add collaboration guide
507
-
508
- ## 7.5.1
509
-
510
- ### Patch Changes
511
-
512
- - 2a5e4a1: Add Core Competency Framework link to Resources page in [#1331](https://github.com/IFRCGo/go-web-app/issues/1331)
513
- - 31eaa97: Add Health Mapping Report to Resources page in [#1331](https://github.com/IFRCGo/go-web-app/issues/1331)
514
- - 4192da1: - Local Units popup, view/edit mode improvements in [#1178](https://github.com/IFRCGo/go-web-app/issues/1178)
515
- - Remove ellipsize heading option in local units map popup
516
- - Local units title on popup are now clickable that opens up a modal to show details
517
- - Added an Edit button to the View Mode for users with edit permissions
518
- - Users will now see a **disabled grey button** when the content is already validated
519
- - 5c7ab88: Display the public visibility field report to public users in [#1743](https://github.com/IFRCGo/go-web-app/issues/1343)
520
-
521
- ## 7.5.0
522
-
523
- ### Minor Changes
524
-
525
- - 5845699: Clean up Resources page
526
-
527
- ## 7.4.2
528
-
529
- ### Patch Changes
530
-
531
- - d734e04: - Fix duplication volunteer label in the Field Report details
532
- - Fix rating visibility in the Country > NS Overview > Strategic priorities page
533
-
534
- ## 7.4.1
535
-
536
- ### Patch Changes
537
-
538
- - a4f77ab: Fetch and use latest available WorldBank data in [#571](https://github.com/IFRCGo/go-api/issues/2224)
539
- - ebf033a: Update Technical Competencies Link on the Cash page of the Catalogue of Surge Services in [#1290](https://github.com/IFRCGo/go-web-app/issues/1290)
540
- - 18d0dc9: Use `molnix status` to filter surge alerts in [#2208](https://github.com/IFRCGo/go-api/issues/2208)
541
- - b070c66: Check guest user permission for local units
542
- - 72df1f2: Add new drone icon for UAV team in [#1280](https://github.com/IFRCGo/go-web-app/issues/1280)
543
- - 2ff7940: Link version number to release notes on GitHub in [#1004](https://github.com/IFRCGo/go-web-app/issues/1004)
544
- Updated @ifrc-go/icons to v2.0.1
545
- - Updated dependencies [72df1f2]
546
- - @ifrc-go/[email protected]
547
-
548
- ## 7.4.0
549
-
550
- ### Minor Changes
551
-
552
- - b6bd6aa: Implement Guest User Permission in [#1237](https://github.com/IFRCGo/go-web-app/issues/1237)
553
-
554
- ## 7.3.13
555
-
556
- ### Patch Changes
557
-
558
- - 453a397: - Update Local Unit map, table and form to match the updated design in [#1178](https://github.com/IFRCGo/go-web-app/issues/1178)
559
- - Add delete button in Local units table and form
560
- - Use filter prop in container and remove manual stylings
561
- - Update size of WikiLink to match height of other action items
562
- - Add error boundary to BaseMap component
563
- - Updated dependencies [453a397]
564
- - @ifrc-go/[email protected]
565
-
566
- ## 7.3.12
567
-
568
- ### Patch Changes
569
-
570
- - ba6734e: Show admin labels in maps in different languages, potentially fixing [#1036](https://github.com/IFRCGo/go-web-app/issues/1036)
571
-
572
- ## 7.3.11
573
-
574
- ### Patch Changes
575
-
576
- - d9491a2: Fix appeals statistics calculation
577
-
578
- ## 7.3.10
579
-
580
- ### Patch Changes
581
-
582
- - 3508c83: Add missing validations in DREF forms
583
- - 3508c83: Fix region filter in All Appeals table
584
- - 073fa1e: Remove personal detail for focal point in local units table
585
- - b508475: Add June 2024 Catalogue of Surge Services Updates
586
- - 3508c83: Handle countries with no bounding box
587
- - d9491a2: Fix appeals based statistics calculation
588
- - Updated dependencies [073fa1e]
589
- - @ifrc-go/[email protected]
590
-
591
- ## 7.3.9
592
-
593
- ### Patch Changes
594
-
595
- - 49f5410: - Reorder CoS list
596
- - Update texts in CoS strategic partnerships resource mobilisation
597
-
598
- ## 7.3.8
599
-
600
- ### Patch Changes
601
-
602
- - 478ab69: Hide contact information from IFRC Presence
603
- - 3fbe60f: Hide add/edit local units on production environment
604
- - 90678ed: Show Organization Type properly in Account Details page
605
-
606
- ## 7.3.7
607
-
608
- ### Patch Changes
609
-
610
- - 909a5e2: Fix Appeals table for Africa Region
611
- - 5a1ae43: Add presentation mode in local units map
612
- - 96120aa: Fix DREF exports margins and use consistent date format
613
- - 8a4f26d: Avoid crash on country pages for countries without bbox
614
-
615
- ## 7.3.6
616
-
617
- ### Patch Changes
618
-
619
- - 1b4b6df: Add local unit form
620
- - 2631a9f: Add office type and location information for IFRC delegation office
621
- - 2d7a6a5: - Enable ability to start PER in IFRC supported languages
622
- - Make PER forms `readOnly` in case of language mismatch
623
- - e4bf098: Fix incorrect statistics for past appeals of a country
624
- - Updated dependencies [0ab207d]
625
- - Updated dependencies [66151a7]
626
- - @ifrc-go/[email protected]
627
-
628
- ## 7.3.5
629
-
630
- ### Patch Changes
631
-
632
- - 894a762: Fix seasonal risk score in regional and global risk watch
633
-
634
- ## 7.3.4
635
-
636
- ### Patch Changes
637
-
638
- - d368ada: Fix GNI per capita in country profile overview
639
-
640
- ## 7.3.3
641
-
642
- ### Patch Changes
643
-
644
- - 73e1966: Update CoS pages as mentioned in #913
645
- - 179a073: Show all head of delegation under IFRC Presence
646
- - 98d6b62: Fix region operation map to apply filter for Africa
647
-
648
- ## 7.3.2
649
-
650
- ### Patch Changes
651
-
652
- - f83c12b: Show Local name when available and use English name as fallback for local units data
653
-
654
- ## 7.3.1
655
-
656
- ### Patch Changes
657
-
658
- - 7f0212b: Integrate mapbox street view for local units map
659
- - Updated dependencies [7f0212b]
660
- - @ifrc-go/[email protected]
661
-
662
- ## 7.3.0
663
-
664
- ### Minor Changes
665
-
666
- - 0dffd52: Add table view in NS local units
667
-
668
- ## 7.2.5
669
-
670
- ### Patch Changes
671
-
672
- - 556766e: - Refetch token list after new token is created
673
- - Update link for terms and conditions for Montandon
674
-
675
- ## 7.2.4
676
-
677
- ### Patch Changes
678
-
679
- - 30eac3c: Add option to generate API token for Montandon in the user profile
680
-
681
- ## 7.2.3
682
-
683
- ### Patch Changes
684
-
685
- - Fix crash due to undefined ICRC presence in country page
686
-
687
- ## 7.2.2
688
-
689
- ### Patch Changes
690
-
691
- - - Update country risk page sources
692
- - Update CoS pages
693
- - Updated dependencies [a1c0554]
694
- - Updated dependencies [e9552b4]
695
- - @ifrc-go/[email protected]
696
-
697
- ## 7.2.1
698
-
699
- ### Patch Changes
700
-
701
- - Remove personal identifiable information for local units
702
-
703
- ## 7.2.0
704
-
705
- ### Minor Changes
706
-
707
- - 9657d4b: Update country pages with appropriate source links
708
- - 66fa7cf: Show FDRS data retrieval year in NS indicators
709
- - b69e8e5: Update IFRC legal status link
710
- - 300250a: Show latest strategic plan of National Society under Strategic Priorities
711
- - 9657d4b: Add GO Wiki links for country page sections
712
- - b38d9d9: Improve overall styling of country pages
713
- - Make loading animation consistent across all pages
714
- - Make empty message consistent
715
- - Use ChartContainer and update usage of charting hooks
716
- - Update BaseMap to extend defaultMapOptions (instead of replacing it)
717
- - Add an option to provide popupClassName in MapPopup
718
- - 80be711: Rename `Supporting Partners` to `Partners`.
719
- - Update IFRC legal status link.
720
- - Update the name of the strategic priorities link to indicate that they were created by the National Society.
721
- - 176e01b: Simplify usage of PER question group in PER assessment form
722
- - Add min widths in account table columns
723
-
724
- ## 7.1.5
725
-
726
- ### Patch Changes
727
-
728
- - Updated dependencies
729
- - @ifrc-go/[email protected]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/env.ts DELETED
@@ -1,29 +0,0 @@
1
- import { defineConfig, Schema } from '@julr/vite-plugin-validate-env';
2
-
3
- export default defineConfig({
4
- APP_TITLE: Schema.string(),
5
- APP_ENVIRONMENT: (key, value) => {
6
- // NOTE: APP_ENVIRONMENT_PLACEHOLDER is meant to be used with image builds
7
- // The value will be later replaced with the actual value
8
- const regex = /^production|staging|testing|alpha-\d+|development|APP_ENVIRONMENT_PLACEHOLDER$/;
9
- const valid = !!value && (value.match(regex) !== null);
10
- if (!valid) {
11
- throw new Error(`Value for environment variable "${key}" must match regex "${regex}", instead received "${value}"`);
12
- }
13
- if (value === 'APP_ENVIRONMENT_PLACEHOLDER') {
14
- console.warn(`Using ${value} for app environment. Make sure to not use this for builds without helm chart`)
15
- }
16
- return value as ('production' | 'staging' | 'testing' | `alpha-${number}` | 'development' | 'APP_ENVIRONMENT_PLACEHOLDER');
17
- },
18
- APP_API_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }),
19
- APP_ADMIN_URL: Schema.string.optional({ format: 'url', protocol: true, tld: false }),
20
- APP_MAPBOX_ACCESS_TOKEN: Schema.string(),
21
- APP_TINY_API_KEY: Schema.string(),
22
- APP_RISK_API_ENDPOINT: Schema.string({ format: 'url', protocol: true }),
23
- APP_SDT_URL: Schema.string.optional({ format: 'url', protocol: true, tld: false }),
24
- APP_SENTRY_DSN: Schema.string.optional(),
25
- APP_SENTRY_TRACES_SAMPLE_RATE: Schema.number.optional(),
26
- APP_SENTRY_REPLAYS_SESSION_SAMPLE_RATE: Schema.number.optional(),
27
- APP_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: Schema.number.optional(),
28
- APP_GOOGLE_ANALYTICS_ID: Schema.string.optional(),
29
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/eslint.config.js DELETED
@@ -1,165 +0,0 @@
1
- import { FlatCompat } from '@eslint/eslintrc';
2
- import js from '@eslint/js';
3
- import json from "@eslint/json";
4
- import tseslint from "typescript-eslint";
5
- import process from 'process';
6
-
7
- const dirname = process.cwd();
8
-
9
- const compat = new FlatCompat({
10
- baseDirectory: dirname,
11
- resolvePluginsRelativeTo: dirname,
12
- });
13
-
14
- const appConfigs = compat.config({
15
- env: {
16
- node: true,
17
- browser: true,
18
- es2020: true,
19
- },
20
- root: true,
21
- extends: [
22
- 'airbnb',
23
- 'airbnb/hooks',
24
- 'plugin:@typescript-eslint/recommended',
25
- 'plugin:react-hooks/recommended',
26
- ],
27
- parser: '@typescript-eslint/parser',
28
- parserOptions: {
29
- ecmaVersion: 'latest',
30
- sourceType: 'module',
31
- },
32
- plugins: [
33
- '@typescript-eslint',
34
- 'react-refresh',
35
- 'simple-import-sort',
36
- 'import-newlines'
37
- ],
38
- settings: {
39
- 'import/parsers': {
40
- '@typescript-eslint/parser': ['.ts', '.tsx']
41
- },
42
- 'import/resolver': {
43
- typescript: {
44
- project: [
45
- './tsconfig.json',
46
- ],
47
- },
48
- },
49
- },
50
- rules: {
51
- 'react-refresh/only-export-components': 'warn',
52
-
53
- 'no-unused-vars': 0,
54
- '@typescript-eslint/no-unused-vars': 1,
55
-
56
- 'no-use-before-define': 0,
57
- '@typescript-eslint/no-use-before-define': 1,
58
-
59
- 'no-shadow': 0,
60
- '@typescript-eslint/no-shadow': ['error'],
61
-
62
- '@typescript-eslint/consistent-type-imports': [
63
- 'warn',
64
- {
65
- disallowTypeAnnotations: false,
66
- fixStyle: 'inline-type-imports',
67
- prefer: 'type-imports',
68
- },
69
- ],
70
-
71
- 'import/no-extraneous-dependencies': [
72
- 'error',
73
- {
74
- devDependencies: [
75
- '**/*.test.{ts,tsx}',
76
- 'eslint.config.js',
77
- 'postcss.config.cjs',
78
- 'stylelint.config.cjs',
79
- 'vite.config.ts',
80
- ],
81
- optionalDependencies: false,
82
- },
83
- ],
84
-
85
- indent: ['error', 4, { SwitchCase: 1 }],
86
-
87
- 'import/no-cycle': ['error', { allowUnsafeDynamicCyclicDependency: true }],
88
-
89
- 'react/react-in-jsx-scope': 'off',
90
- 'camelcase': 'off',
91
-
92
- 'react/jsx-indent': ['error', 4],
93
- 'react/jsx-indent-props': ['error', 4],
94
- 'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx', '.ts', '.tsx'] }],
95
-
96
- 'import/extensions': ['off', 'never'],
97
-
98
- 'react-hooks/rules-of-hooks': 'error',
99
- 'react-hooks/exhaustive-deps': 'warn',
100
-
101
- 'react/require-default-props': ['warn', { ignoreFunctionalComponents: true }],
102
- 'simple-import-sort/imports': 'warn',
103
- 'simple-import-sort/exports': 'warn',
104
- 'import-newlines/enforce': ['warn', 1]
105
- },
106
- overrides: [
107
- {
108
- files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
109
- rules: {
110
- 'simple-import-sort/imports': [
111
- 'error',
112
- {
113
- 'groups': [
114
- // side effect imports
115
- ['^\\u0000'],
116
- // packages `react` related packages come first
117
- ['^react', '^@?\\w'],
118
- // internal packages
119
- ['^#.+$'],
120
- // parent imports. Put `..` last
121
- // other relative imports. Put same-folder imports and `.` last
122
- ['^\\.\\.(?!/?$)', '^\\.\\./?$', '^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
123
- // style imports
124
- ['^.+\\.json$', '^.+\\.module.css$'],
125
- ]
126
- }
127
- ]
128
- }
129
- }
130
- ]
131
- }).map((conf) => ({
132
- ...conf,
133
- files: ['src/**/*.tsx', 'src/**/*.jsx', 'src/**/*.ts', 'src/**/*.js'],
134
- ignores: [
135
- "node_modules/",
136
- "build/",
137
- "coverage/",
138
- 'src/generated/types.ts'
139
- ],
140
- }));
141
-
142
- const otherConfig = {
143
- files: ['*.js', '*.ts', '*.cjs'],
144
- ...js.configs.recommended,
145
- ...tseslint.configs.recommended,
146
- };
147
-
148
- const jsonConfig = {
149
- files: ['**/*.json'],
150
- language: 'json/json',
151
- rules: {
152
- 'json/no-duplicate-keys': 'error',
153
- },
154
- };
155
-
156
- export default [
157
- {
158
- plugins: {
159
- json,
160
- },
161
- },
162
- ...appConfigs,
163
- otherConfig,
164
- jsonConfig,
165
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/index.html DELETED
@@ -1,69 +0,0 @@
1
- <!DOCTYPE html>
2
- <html
3
- lang="en"
4
- translate="no"
5
- >
6
- <head>
7
- <meta charset="UTF-8" />
8
- <link
9
- rel="icon"
10
- type="image/svg+xml"
11
- href="/go-icon.svg"
12
- />
13
- <meta
14
- name="viewport"
15
- content="width=device-width, initial-scale=1.0"
16
- />
17
- <meta
18
- name="description"
19
- content=""
20
- />
21
- <title>
22
- %APP_TITLE%
23
- </title>
24
- <link rel="preconnect" href="https://fonts.googleapis.com">
25
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
26
- <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
27
- <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
28
- <link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
29
-
30
- <style>
31
- html, body {
32
- margin: 0;
33
- padding: 0;
34
- }
35
-
36
- body {
37
- font-family: Poppins, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
38
- }
39
-
40
- @media screen {
41
- body {
42
- background-color: #f7f7f7;
43
- }
44
- }
45
-
46
- #webapp-preload {
47
- width: 100vw;
48
- height: 100vh;
49
- display: flex;
50
- align-items: center;
51
- justify-content: center;
52
- }
53
- </style>
54
- </head>
55
- <body>
56
- <noscript>
57
- %APP_TITLE% needs JS.
58
- </noscript>
59
- <div id="webapp-root">
60
- <div id="webapp-preload">
61
- %APP_TITLE% loading...
62
- </div>
63
- </div>
64
- <script
65
- type="module"
66
- src="/src/index.tsx"
67
- ></script>
68
- </body>
69
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/package.json DELETED
@@ -1,119 +0,0 @@
1
- {
2
- "name": "go-web-app",
3
- "version": "7.21.0-beta.2",
4
- "type": "module",
5
- "private": true,
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/IFRCGo/go-web-app.git",
10
- "directory": "app"
11
- },
12
- "scripts": {
13
- "translatte": "tsx scripts/translatte/main.ts",
14
- "translatte:generate": "pnpm translatte generate-migration ../translationMigrations ./src/**/i18n.json ../packages/ui/src/**/i18n.json",
15
- "translatte:lint": "pnpm translatte lint ./src/**/i18n.json ../packages/ui/src/**/i18n.json",
16
- "initialize:type": "mkdir -p generated/ && pnpm initialize:type:go-api && pnpm initialize:type:risk-api",
17
- "initialize:type:go-api": "test -f ./generated/types.ts && true || cp types.stub.ts ./generated/types.ts",
18
- "initialize:type:risk-api": "test -f ./generated/riskTypes.ts && true || cp types.stub.ts ./generated/riskTypes.ts",
19
- "generate:type": "pnpm generate:type:go-api && pnpm generate:type:risk-api",
20
- "generate:type:go-api": "dotenv -- cross-var openapi-typescript \"%APP_API_ENDPOINT%api-docs/\" -o ./generated/types.ts --alphabetize",
21
- "generate:type:risk-api": "dotenv -- cross-var openapi-typescript \"%APP_RISK_API_ENDPOINT%api-docs/\" -o ./generated/riskTypes.ts --alphabetize",
22
- "prestart": "pnpm initialize:type",
23
- "start": "pnpm -F @ifrc-go/ui build && vite",
24
- "prebuild": "pnpm initialize:type",
25
- "build": "pnpm -F @ifrc-go/ui build && vite build",
26
- "preview": "vite preview",
27
- "pretypecheck": "pnpm initialize:type",
28
- "typecheck": "tsc",
29
- "prelint:js": "pnpm initialize:type",
30
- "lint:js": "eslint src",
31
- "lint:css": "stylelint \"./src/**/*.css\"",
32
- "lint:translation": "pnpm translatte:lint",
33
- "lint": "pnpm lint:js && pnpm lint:css && pnpm lint:translation",
34
- "lint:fix": "pnpm lint:js --fix && pnpm lint:css --fix",
35
- "test": "vitest",
36
- "test:coverage": "vitest run --coverage",
37
- "surge:deploy": "branch=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD); branch=$(echo $branch | tr ./ -); cp ../build/index.html ../build/200.html; surge -p ../build/ -d https://ifrc-go-$branch.surge.sh",
38
- "surge:teardown": "branch=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD); branch=$(echo $branch | tr ./ -); surge teardown https://ifrc-go-$branch.surge.sh"
39
- },
40
- "dependencies": {
41
- "@ifrc-go/icons": "^2.0.1",
42
- "@ifrc-go/ui": "workspace:^",
43
- "@sentry/react": "^7.81.1",
44
- "@tinymce/tinymce-react": "^5.1.1",
45
- "@togglecorp/fujs": "^2.1.1",
46
- "@togglecorp/re-map": "^0.3.0",
47
- "@togglecorp/toggle-form": "^2.0.4",
48
- "@togglecorp/toggle-request": "^1.0.0-beta.3",
49
- "@turf/bbox": "^6.5.0",
50
- "@turf/buffer": "^6.5.0",
51
- "exceljs": "^4.3.0",
52
- "file-saver": "^2.0.5",
53
- "html-to-image": "^1.11.11",
54
- "mapbox-gl": "^1.13.0",
55
- "papaparse": "^5.4.1",
56
- "react": "^18.2.0",
57
- "react-dom": "^18.2.0",
58
- "react-router-dom": "^6.18.0",
59
- "sanitize-html": "^2.10.0"
60
- },
61
- "devDependencies": {
62
- "@eslint/eslintrc": "^3.1.0",
63
- "@eslint/js": "^9.20.0",
64
- "@eslint/json": "^0.5.0",
65
- "@julr/vite-plugin-validate-env": "^1.0.1",
66
- "@types/file-saver": "^2.0.5",
67
- "@types/mapbox-gl": "^1.13.0",
68
- "@types/node": "^20.11.6",
69
- "@types/papaparse": "^5.3.8",
70
- "@types/react": "^18.0.28",
71
- "@types/react-dom": "^18.0.11",
72
- "@types/sanitize-html": "^2.9.0",
73
- "@types/yargs": "^17.0.32",
74
- "@typescript-eslint/eslint-plugin": "^8.11.0",
75
- "@typescript-eslint/parser": "^8.11.0",
76
- "@vitejs/plugin-react-swc": "^3.5.0",
77
- "@vitest/coverage-v8": "^1.2.2",
78
- "autoprefixer": "^10.4.14",
79
- "cross-var": "^1.1.0",
80
- "dotenv-cli": "^7.4.2",
81
- "eslint": "^9.20.1",
82
- "eslint-config-airbnb": "^19.0.4",
83
- "eslint-import-resolver-typescript": "^3.6.3",
84
- "eslint-plugin-import": "^2.31.0",
85
- "eslint-plugin-import-exports-imports-resolver": "^1.0.1",
86
- "eslint-plugin-import-newlines": "^1.3.4",
87
- "eslint-plugin-jsx-a11y": "^6.10.1",
88
- "eslint-plugin-react": "^7.37.4",
89
- "eslint-plugin-react-hooks": "^5.0.0",
90
- "eslint-plugin-react-refresh": "^0.4.13",
91
- "eslint-plugin-simple-import-sort": "^12.1.1",
92
- "fast-glob": "^3.3.2",
93
- "happy-dom": "^9.18.3",
94
- "openapi-typescript": "6.5.5",
95
- "postcss": "^8.5.3",
96
- "postcss-nested": "^7.0.2",
97
- "postcss-normalize": "^13.0.1",
98
- "postcss-preset-env": "^10.1.5",
99
- "rollup-plugin-visualizer": "^5.9.0",
100
- "stylelint": "^16.17.0",
101
- "stylelint-config-concentric": "^2.0.2",
102
- "stylelint-config-recommended": "^15.0.0",
103
- "stylelint-value-no-unknown-custom-properties": "^6.0.1",
104
- "surge": "^0.23.1",
105
- "ts-md5": "^1.3.1",
106
- "tsx": "^4.7.2",
107
- "typescript": "^5.5.2",
108
- "typescript-eslint": "^8.26.0",
109
- "vite": "^5.0.10",
110
- "vite-plugin-checker": "^0.7.0",
111
- "vite-plugin-compression2": "^0.11.0",
112
- "vite-plugin-radar": "^0.9.2",
113
- "vite-plugin-svgr": "^4.2.0",
114
- "vite-plugin-webfont-dl": "^3.9.4",
115
- "vite-tsconfig-paths": "^4.2.2",
116
- "vitest": "^1.2.2",
117
- "yargs": "^17.7.2"
118
- }
119
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/postcss.config.cjs DELETED
@@ -1,8 +0,0 @@
1
- module.exports = {
2
- plugins: [
3
- require('postcss-preset-env'),
4
- require('postcss-nested'),
5
- require('postcss-normalize'),
6
- require('autoprefixer'),
7
- ],
8
- };
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/public/go-icon.svg DELETED
go-web-app-develop/app/scripts/translatte/README.md DELETED
@@ -1,59 +0,0 @@
1
- # translatte
2
-
3
- A simple script to synchronize translations in source code to translations in
4
- server
5
-
6
- ## Usecase
7
-
8
- ### Generating migrations
9
-
10
- When adding a new feature or updating existing feature or removing an
11
- existing feature on the codebase, we may need to update the strings used
12
- in the application.
13
-
14
- Developers can change the translations using their preferred choice of editor.
15
-
16
- Once all of the changes have been made, we can generate a migration file for the translations using:
17
-
18
- ```bash
19
- pnpm translatte generate-migration ./src/translationMigrations ./src/**/i18n.json
20
- ```
21
-
22
- Once the migration file has been created, the migration file can be committed to the VCS.
23
-
24
- ### Applying migrations
25
-
26
- When we are deploying the changes to the server, we will need to update
27
- the strings in the server.
28
-
29
- We can generate the new set of strings for the server using:
30
-
31
- ```bash
32
- pnpm translatte apply-migrations ./src/translationMigrations --last-migration "name_of_last_migration" --source "strings_json_from_server.json" --destination "new_strings_json_for_server.json"
33
- ```
34
-
35
- ### Merge migrations
36
-
37
- Once the migrations are applied to the strings in the server, we can merge the migrations into a single file.
38
-
39
- To merge migrations, we can run the following command:
40
-
41
- ```bash
42
- pnpm translatte merge-migrations ./src/translationMigrations --from 'initial_migration.json' --to 'final_migration.json'
43
- ```
44
-
45
- ### Checking migrations
46
-
47
- We can use the following command to check for valid migrations:
48
-
49
- ```bash
50
- pnpm translatte lint ./src/**/i18n.json
51
- ```
52
-
53
- ### Listing migrations
54
-
55
- We can use the following command to list all migrations:
56
-
57
- ```bash
58
- pnpm translatte list-migrations ./src/translationMigrations
59
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/scripts/translatte/commands/applyMigrations.test.ts DELETED
@@ -1,104 +0,0 @@
1
- import { expect } from 'vitest';
2
- import { mkdirSync } from 'fs';
3
- import { join } from 'path';
4
-
5
- import { testWithTmpDir } from '../testHelpers';
6
- import {
7
- writeFilePromisify,
8
- readJsonFilesContents,
9
- } from '../utils';
10
- import {
11
- migrationContent1,
12
- migrationContent2,
13
- migrationContent3,
14
- migrationContent4,
15
- migrationContent5,
16
- migrationContent6,
17
-
18
- strings1,
19
- strings2,
20
- } from '../mockData';
21
- import applyMigrations from './applyMigrations';
22
- import { SourceFileContent } from '../types';
23
-
24
- testWithTmpDir('test applyMigrations with no data in server', async ({ tmpdir }) => {
25
- mkdirSync(join(tmpdir, 'migrations'));
26
- const migrations = [
27
- { name: '000001-1000000000000.json', content: migrationContent1 },
28
- { name: '000002-1000000000000.json', content: migrationContent2 },
29
- { name: '000003-1000000000000.json', content: migrationContent3 },
30
- { name: '000004-1000000000000.json', content: migrationContent4 },
31
- { name: '000005-1000000000000.json', content: migrationContent5 },
32
- ].map(({ name, content }) => writeFilePromisify(
33
- join(tmpdir, 'migrations', name),
34
- JSON.stringify(content, null, 4),
35
- 'utf8',
36
- ));
37
- await Promise.all(migrations);
38
-
39
- mkdirSync(join(tmpdir, 'strings'));
40
-
41
- const emptySourceFile: SourceFileContent = {
42
- last_migration: undefined,
43
- strings: [],
44
- };
45
- await writeFilePromisify(
46
- join(tmpdir, 'strings', 'before.json'),
47
- JSON.stringify(emptySourceFile),
48
- 'utf8',
49
- );
50
-
51
- await applyMigrations(
52
- tmpdir,
53
- join(tmpdir, 'strings', 'before.json'),
54
- join(tmpdir, 'strings', 'after.json'),
55
- 'migrations',
56
- ['np'],
57
- undefined,
58
- false,
59
- );
60
-
61
- const newSourceFiles = await readJsonFilesContents([
62
- join(tmpdir, 'strings', 'after.json'),
63
- ]);
64
- const newSourceFileContent = newSourceFiles[0].content;
65
-
66
- expect(newSourceFileContent).toEqual(strings1)
67
- });
68
-
69
- testWithTmpDir('test applyMigrations with data in server', async ({ tmpdir }) => {
70
- mkdirSync(join(tmpdir, 'migrations'));
71
- const migrations = [
72
- { name: '000006-1000000000000.json', content: migrationContent6 },
73
- ].map(({ name, content }) => writeFilePromisify(
74
- join(tmpdir, 'migrations', name),
75
- JSON.stringify(content, null, 4),
76
- 'utf8',
77
- ));
78
- await Promise.all(migrations);
79
-
80
- mkdirSync(join(tmpdir, 'strings'));
81
-
82
- await writeFilePromisify(
83
- join(tmpdir, 'strings', 'before.json'),
84
- JSON.stringify(strings1),
85
- 'utf8',
86
- );
87
-
88
- await applyMigrations(
89
- tmpdir,
90
- join(tmpdir, 'strings', 'before.json'),
91
- join(tmpdir, 'strings', 'after.json'),
92
- 'migrations',
93
- ['np'],
94
- undefined,
95
- false,
96
- );
97
-
98
- const newSourceFiles = await readJsonFilesContents([
99
- join(tmpdir, 'strings', 'after.json'),
100
- ]);
101
- const newSourceFileContent = newSourceFiles[0].content;
102
-
103
- expect(newSourceFileContent).toEqual(strings2)
104
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/scripts/translatte/commands/applyMigrations.ts DELETED
@@ -1,177 +0,0 @@
1
- import { Md5 } from 'ts-md5';
2
- import { listToMap, isDefined, unique } from '@togglecorp/fujs';
3
- import { isAbsolute, join, basename } from 'path';
4
- import {
5
- readSource,
6
- getMigrationFilesAttrs,
7
- readMigrations,
8
- writeFilePromisify,
9
- } from '../utils';
10
- import { merge } from './mergeMigrations';
11
- import {
12
- SourceFileContent,
13
- MigrationFileContent,
14
- SourceStringItem,
15
- } from '../types';
16
-
17
- function apply(
18
- strings: SourceStringItem[],
19
- migrationActions: MigrationFileContent['actions'],
20
- languages: string[],
21
- ): SourceStringItem[] {
22
- const stringsMapping = listToMap(
23
- strings,
24
- (item) => `${item.page_name}:${item.key}:${item.language}` as string,
25
- (item) => item,
26
- );
27
-
28
- const newMapping: {
29
- [key: string]: SourceStringItem | null;
30
- } = { };
31
-
32
- unique(['en', ...languages]).forEach((language) => {
33
- migrationActions.forEach((action) => {
34
- const isSourceLanguage = language === 'en';
35
- const key = `${action.namespace}:${action.key}:${language}`;
36
- if (action.action === 'add') {
37
- const hash = Md5.hashStr(action.value);
38
-
39
- const prevValue = stringsMapping[key];
40
- // NOTE: we are comparing hash instead of value so that this works for source language as well as other languages
41
- if (prevValue && prevValue.hash !== hash) {
42
- throw `Add: We already have string with different value for namespace '${action.namespace}' and key '${action.key}'`;
43
- }
44
-
45
- if (newMapping[key]) {
46
- throw `Add: We already have string for namespace '${action.namespace}' and key '${action.key}' in migration`;
47
- }
48
-
49
- newMapping[key] = {
50
- hash,
51
- key: action.key,
52
- page_name: action.namespace,
53
- language,
54
- value: isSourceLanguage
55
- ? action.value
56
- : '',
57
- };
58
- } else if (action.action === 'remove') {
59
- // NOTE: We can add or move string so we might have value in newMapping
60
- if (!newMapping[key]) {
61
- newMapping[key] = null;
62
- }
63
- } else {
64
- const prevValue = stringsMapping[key];
65
- if (!prevValue) {
66
- throw `Update: We do not have string with namespace '${action.namespace}' and key '${action.key}'`;
67
- }
68
-
69
- const newKey = action.newKey ?? prevValue.key;
70
- const newNamespace = action.newNamespace ?? prevValue.page_name;
71
- const newValue = isSourceLanguage
72
- ? action.newValue ?? prevValue.value
73
- : prevValue.value;
74
- const newHash = isSourceLanguage
75
- ? Md5.hashStr(newValue)
76
- : prevValue.hash;
77
-
78
- const newCanonicalKey = `${newNamespace}:${newKey}:${language}`;
79
-
80
-
81
- // NOTE: remove the old key and add new key
82
- if (!newMapping[key]) {
83
- newMapping[key] = null;
84
- }
85
-
86
- const newItem = {
87
- hash: newHash,
88
- key: newKey,
89
- page_name: newNamespace,
90
- language,
91
- value: newValue,
92
- }
93
-
94
- if (newMapping[newCanonicalKey]) {
95
- throw `Update: We already have string for namespace '${action.namespace}' and key '${action.key}' in migration`;
96
- }
97
- newMapping[newCanonicalKey] = newItem;
98
- }
99
- });
100
- });
101
-
102
- const finalMapping: typeof newMapping = {
103
- ...stringsMapping,
104
- ...newMapping,
105
- };
106
-
107
- return Object.values(finalMapping)
108
- .filter(isDefined)
109
- .sort((foo, bar) => (
110
- foo.page_name.localeCompare(bar.page_name)
111
- || foo.key.localeCompare(bar.key)
112
- || foo.language.localeCompare(bar.language)
113
- ))
114
- }
115
-
116
- async function applyMigrations(
117
- projectPath: string,
118
- sourceFileName: string,
119
- destinationFileName: string,
120
- migrationFilePath: string,
121
- languages: string[],
122
- from: string | undefined,
123
- dryRun: boolean | undefined,
124
- ) {
125
- const sourcePath = isAbsolute(sourceFileName)
126
- ? sourceFileName
127
- : join(projectPath, sourceFileName)
128
- const sourceFile = await readSource(sourcePath)
129
-
130
- const migrationFilesAttrs = await getMigrationFilesAttrs(projectPath, migrationFilePath);
131
- const selectedMigrationFilesAttrs = from
132
- ? migrationFilesAttrs.filter((item) => (item.migrationName > from))
133
- : migrationFilesAttrs;
134
-
135
- console.info(`Found ${selectedMigrationFilesAttrs.length} migration files`);
136
-
137
- if (selectedMigrationFilesAttrs.length < 1) {
138
- throw 'There should be at least 1 migration file';
139
- }
140
-
141
- const selectedMigrations = await readMigrations(
142
- selectedMigrationFilesAttrs.map((migration) => migration.fileName),
143
- );
144
-
145
- const lastMigration = selectedMigrations[selectedMigrations.length - 1];
146
-
147
- const mergedMigrationActions = merge(
148
- selectedMigrations.map((migration) => migration.content),
149
- );
150
-
151
- const outputSourceFileContent: SourceFileContent = {
152
- ...sourceFile.content,
153
- last_migration: basename(lastMigration.file),
154
- strings: apply(
155
- sourceFile.content.strings,
156
- mergedMigrationActions,
157
- languages,
158
- ),
159
- };
160
-
161
- const destinationPath = isAbsolute(destinationFileName)
162
- ? destinationFileName
163
- : join(projectPath, destinationFileName)
164
-
165
- if (dryRun) {
166
- console.info(`Creating file '${destinationPath}'`);
167
- console.info(outputSourceFileContent);
168
- } else {
169
- await writeFilePromisify(
170
- destinationPath,
171
- JSON.stringify(outputSourceFileContent, null, 4),
172
- 'utf8',
173
- );
174
- }
175
- }
176
-
177
- export default applyMigrations;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/scripts/translatte/commands/exportMigration.ts DELETED
@@ -1,62 +0,0 @@
1
- import xlsx from 'exceljs';
2
-
3
- import { readMigrations } from '../utils';
4
- import { isNotDefined } from '@togglecorp/fujs';
5
-
6
- async function exportMigration(
7
- migrationFilePath: string,
8
- exportFileName: string,
9
- ) {
10
- const migrations = await readMigrations(
11
- [migrationFilePath]
12
- );
13
-
14
- const actions = migrations[0].content.actions;
15
- const workbook = new xlsx.Workbook();
16
- const now = new Date();
17
- workbook.created = now;
18
-
19
- const yyyy = now.getFullYear();
20
- const mm = (now.getMonth() + 1).toString().padStart(2, '0');
21
- const dd = now.getDate().toString().padStart(2, '0');
22
- const worksheet = workbook.addWorksheet(
23
- `${yyyy}-${mm}-${dd}`
24
- );
25
-
26
- worksheet.columns = [
27
- { header: 'Namespace', key: 'namespace' },
28
- { header: 'Key', key: 'key' },
29
- { header: 'EN', key: 'en' },
30
- { header: 'FR', key: 'fr' },
31
- { header: 'ES', key: 'es' },
32
- { header: 'AR', key: 'ar' },
33
- ]
34
-
35
- actions.forEach((actionItem) => {
36
- if (actionItem.action === 'remove') {
37
- return;
38
- }
39
-
40
- if (actionItem.action === 'update' && isNotDefined(actionItem.newValue)) {
41
- return;
42
- }
43
-
44
- const value = actionItem.action === 'update'
45
- ? actionItem.newValue
46
- : actionItem.value;
47
-
48
- worksheet.addRow({
49
- namespace: actionItem.namespace,
50
- key: actionItem.key,
51
- en: value,
52
- });
53
- });
54
-
55
- const fileName = isNotDefined(exportFileName)
56
- ? `go-strings-${yyyy}-${mm}-${dd}`
57
- : exportFileName;
58
-
59
- await workbook.xlsx.writeFile(`${fileName}.xlsx`);
60
- }
61
-
62
- export default exportMigration;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/scripts/translatte/commands/generateMigration.test.ts DELETED
@@ -1,102 +0,0 @@
1
- import { expect } from 'vitest';
2
- import { mkdirSync } from 'fs';
3
- import { join } from 'path';
4
-
5
- import generateMigration from './generateMigration';
6
- import { testWithTmpDir } from '../testHelpers';
7
- import { writeFilePromisify, readMigrations } from '../utils';
8
- import {
9
- migrationContent1,
10
- migrationContent2,
11
- migrationContent3,
12
- migrationContent4,
13
- migrationContent5,
14
- loginContent,
15
- registerContent,
16
- updatedLoginContent,
17
- updatedRegisterContent,
18
- migrationContent6,
19
- } from '../mockData';
20
-
21
-
22
- testWithTmpDir('test generateMigration with no change', async ({ tmpdir }) => {
23
- mkdirSync(join(tmpdir, 'migrations'));
24
- const migrations = [
25
- { name: '000001-1000000000000.json', content: migrationContent1 },
26
- { name: '000002-1000000000000.json', content: migrationContent2 },
27
- { name: '000003-1000000000000.json', content: migrationContent3 },
28
- { name: '000004-1000000000000.json', content: migrationContent4 },
29
- { name: '000005-1000000000000.json', content: migrationContent5 },
30
- ].map(({ name, content }) => writeFilePromisify(
31
- join(tmpdir, 'migrations', name),
32
- JSON.stringify(content, null, 4),
33
- 'utf8',
34
- ));
35
- await Promise.all(migrations);
36
-
37
- mkdirSync(join(tmpdir, 'src'));
38
- const translations = [
39
- { name: 'home.i18n.json', content: loginContent },
40
- { name: 'register.i18n.json', content: registerContent },
41
- ].map(({ name, content }) => writeFilePromisify(
42
- join(tmpdir, 'src', name),
43
- JSON.stringify(content, null, 4),
44
- 'utf8',
45
- ));
46
- await Promise.all(translations);
47
-
48
- await expect(
49
- () => generateMigration(
50
- tmpdir,
51
- 'migrations',
52
- 'src/**/*.i18n.json',
53
- new Date().getTime(),
54
- false,
55
- ),
56
- ).rejects.toThrow('Nothing to do');
57
- });
58
-
59
- testWithTmpDir('test generateMigration with change', async ({ tmpdir }) => {
60
- mkdirSync(join(tmpdir, 'migrations'));
61
- const migrations = [
62
- { name: '000001-1000000000000.json', content: migrationContent1 },
63
- { name: '000002-1000000000000.json', content: migrationContent2 },
64
- { name: '000003-1000000000000.json', content: migrationContent3 },
65
- { name: '000004-1000000000000.json', content: migrationContent4 },
66
- { name: '000005-1000000000000.json', content: migrationContent5 },
67
- ].map(({ name, content }) => writeFilePromisify(
68
- join(tmpdir, 'migrations', name),
69
- JSON.stringify(content, null, 4),
70
- 'utf8',
71
- ));
72
- await Promise.all(migrations);
73
-
74
- mkdirSync(join(tmpdir, 'src'));
75
-
76
- const translations = [
77
- { name: 'home.i18n.json', content: updatedLoginContent },
78
- { name: 'register.i18n.json', content: updatedRegisterContent },
79
- ].map(({ name, content }) => writeFilePromisify(
80
- join(tmpdir, 'src', name),
81
- JSON.stringify(content, null, 4),
82
- 'utf8',
83
- ));
84
- await Promise.all(translations);
85
-
86
- const timestamp = new Date().getTime();
87
-
88
- await generateMigration(
89
- tmpdir,
90
- 'migrations',
91
- 'src/**/*.i18n.json',
92
- timestamp,
93
- false,
94
- );
95
-
96
- const generatedMigrations = await readMigrations([
97
- join(tmpdir, 'migrations', `000006-${timestamp}.json`)
98
- ]);
99
- const generatedMigrationContent = generatedMigrations[0].content;
100
-
101
- expect(generatedMigrationContent).toEqual(migrationContent6)
102
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/scripts/translatte/commands/generateMigration.ts DELETED
@@ -1,195 +0,0 @@
1
- import { Md5 } from 'ts-md5';
2
- import { join, isAbsolute } from 'path';
3
-
4
- import {
5
- writeFilePromisify,
6
- oneOneMapping,
7
- readTranslations,
8
- getTranslationFileNames,
9
- getMigrationFilesAttrs,
10
- readMigrations,
11
- oneOneMappingNonUnique,
12
- } from '../utils';
13
- import { MigrationActionItem, MigrationFileContent } from '../types';
14
- import { merge } from './mergeMigrations';
15
-
16
- function getCombinedKey(key: string, namespace: string) {
17
- return `${namespace}:${key}`;
18
- }
19
-
20
- type StateItem = {
21
- filename?: string;
22
- namespace: string;
23
- key: string;
24
- value: string;
25
- }
26
-
27
- // FIXME: The output should be stable
28
- function generateMigration(
29
- prevState: StateItem[],
30
- currentState: StateItem[],
31
- ): MigrationActionItem[] {
32
- /*
33
- console.info('prevState length', prevState.length);
34
- console.info('currentState length', currentState.length);
35
- console.info('Total change', Math.abs(prevState.length - currentState.length));
36
- */
37
-
38
- const {
39
- // Same, key, namespace and same value
40
- validCommonItems: identicalStateItems,
41
-
42
- // Same, key, namespace but different value
43
- invalidCommonItems: valueUpdatedStateItems,
44
-
45
- // items with different key or namespace or both
46
- prevStateRemainder: potentiallyRemovedStateItems,
47
-
48
- // items with different key or namespace or both
49
- currentStateRemainder: potentiallyAddedStateItems,
50
- } = oneOneMapping(
51
- prevState,
52
- currentState,
53
- ({ key, namespace }) => getCombinedKey(key, namespace),
54
- (prev, current) => prev.value === current.value,
55
- );
56
-
57
- console.info(`Unchanged strings: ${identicalStateItems.length}`)
58
- console.info(`Value updated strings: ${valueUpdatedStateItems.length}`)
59
-
60
- console.info(`Potentially removed: ${potentiallyRemovedStateItems.length}`)
61
- console.info(`Potentially added: ${potentiallyAddedStateItems.length}`)
62
-
63
- const {
64
- commonItems: namespaceUpdatedStateItems,
65
- prevStateRemainder: potentiallyRemovedStateItemsAfterNamespaceChange,
66
- currentStateRemainder: potentiallyAddedStateItemsAfterNamespaceChange,
67
- } = oneOneMappingNonUnique(
68
- potentiallyRemovedStateItems,
69
- potentiallyAddedStateItems,
70
- (item) => getCombinedKey(item.key, Md5.hashStr(item.value)),
71
- );
72
-
73
- const {
74
- commonItems: keyUpdatedStateItems,
75
- prevStateRemainder: removedStateItems,
76
- currentStateRemainder: addedStateItems,
77
- } = oneOneMappingNonUnique(
78
- potentiallyRemovedStateItemsAfterNamespaceChange,
79
- potentiallyAddedStateItemsAfterNamespaceChange,
80
- (item) => getCombinedKey(item.namespace, Md5.hashStr(item.value)),
81
- );
82
-
83
- console.info(`Namespace updated strings: ${namespaceUpdatedStateItems.length}`)
84
- console.info(`Added strings: ${addedStateItems.length}`)
85
- console.info(`Removed strings: ${removedStateItems.length}`)
86
-
87
- return [
88
- ...valueUpdatedStateItems.map(({ prevStateItem, currentStateItem }) => ({
89
- action: 'update' as const,
90
- key: prevStateItem.key,
91
- namespace: prevStateItem.namespace,
92
- newValue: currentStateItem.value,
93
- })),
94
- ...namespaceUpdatedStateItems.map(({ prevStateItem, currentStateItem }) => ({
95
- action: 'update' as const,
96
- key: prevStateItem.key,
97
- namespace: prevStateItem.namespace,
98
- newNamespace: currentStateItem.namespace,
99
- })),
100
- ...keyUpdatedStateItems.map(({ prevStateItem, currentStateItem }) => ({
101
- action: 'update' as const,
102
- key: prevStateItem.key,
103
- newKey: currentStateItem.key,
104
- namespace: prevStateItem.namespace,
105
- })),
106
- ...addedStateItems.map((item) => ({
107
- action: 'add' as const,
108
- key: item.key,
109
- namespace: item.namespace,
110
- value: item.value,
111
- })),
112
- ...removedStateItems.map((item) => ({
113
- action: 'remove' as const,
114
- key: item.key,
115
- namespace: item.namespace,
116
- })),
117
- ].sort((foo, bar) => (
118
- foo.namespace.localeCompare(bar.namespace)
119
- || foo.action.localeCompare(bar.action)
120
- || foo.key.localeCompare(bar.key)
121
- ));
122
- }
123
-
124
- async function generate(
125
- projectPath: string,
126
- migrationFilePath: string,
127
- translationFileName: string | string[],
128
- timestamp: number,
129
- dryRun: boolean | undefined,
130
- ) {
131
- const migrationFilesAttrs = await getMigrationFilesAttrs(projectPath, migrationFilePath);
132
- const selectedMigrationFilesAttrs = migrationFilesAttrs;
133
- console.info(`Found ${selectedMigrationFilesAttrs.length} migration files`);
134
- const selectedMigrations = await readMigrations(
135
- selectedMigrationFilesAttrs.map((migration) => migration.fileName),
136
- );
137
- const mergedMigrationActions = merge(
138
- selectedMigrations.map((migration) => migration.content),
139
- );
140
-
141
- const serverState: StateItem[] = mergedMigrationActions.map((item) => {
142
- if (item.action !== 'add') {
143
- throw `The action should be "add" but found "${item.action}"`;
144
- }
145
- return {
146
- filename: undefined,
147
- namespace: item.namespace,
148
- key: item.key,
149
- value: item.value,
150
- }
151
- });
152
- const translationFiles = await getTranslationFileNames(
153
- projectPath,
154
- Array.isArray(translationFileName) ? translationFileName : [translationFileName],
155
- );
156
- const { translations } = await readTranslations(translationFiles);
157
- const fileState = translations.map((item) => ({
158
- ...item,
159
- }));
160
-
161
- const migrationActionItems = generateMigration(
162
- serverState,
163
- fileState,
164
- );
165
-
166
- if (migrationActionItems.length <= 0) {
167
- throw 'Nothing to do';
168
- }
169
-
170
- const lastMigration = migrationFilesAttrs[migrationFilesAttrs.length - 1];
171
-
172
- const migrationContent: MigrationFileContent = {
173
- parent: lastMigration?.migrationName,
174
- actions: migrationActionItems,
175
- }
176
-
177
- const num = String(Number(lastMigration?.num ?? '000000') + 1).padStart(6, '0');
178
-
179
- const outputMigrationFile = isAbsolute(migrationFilePath)
180
- ? join(migrationFilePath, `${num}-${timestamp}.json`)
181
- : join(projectPath, migrationFilePath, `${num}-${timestamp}.json`)
182
-
183
- if (dryRun) {
184
- console.info(`Creating migration file '${outputMigrationFile}'`);
185
- console.info(migrationContent);
186
- } else {
187
- await writeFilePromisify(
188
- outputMigrationFile,
189
- JSON.stringify(migrationContent, null, 4),
190
- 'utf8',
191
- );
192
- }
193
- }
194
-
195
- export default generate;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/scripts/translatte/commands/lint.test.ts DELETED
@@ -1,75 +0,0 @@
1
- import { expect } from 'vitest';
2
- import { join } from 'path';
3
- import { mkdirSync } from 'fs';
4
-
5
- import { loginContent, registerContent } from '../mockData';
6
- import { testWithTmpDir } from '../testHelpers';
7
- import { writeFilePromisify } from '../utils';
8
- import lint from './lint';
9
-
10
- testWithTmpDir('test lint with duplicate file', async ({ tmpdir }) => {
11
- mkdirSync(join(tmpdir, 'i18n'));
12
-
13
- const writes = [
14
- { name: 'login.i18n.json', content: loginContent },
15
- { name: 'register.i18n.json', content: registerContent },
16
- { name: 'register-form.i18n.json', content: registerContent },
17
- ].map(({ name, content }) => writeFilePromisify(
18
- join(tmpdir, 'i18n', name),
19
- JSON.stringify(content, null, 4),
20
- 'utf8',
21
- ));
22
- await Promise.all(writes);
23
-
24
- await expect(
25
- () => lint(tmpdir, ['**/*.i18n.json'], false)
26
- ).rejects.toThrow('Found 12 duplicated strings.');
27
- });
28
-
29
- testWithTmpDir('test lint with duplicate string and same text', async ({ tmpdir }) => {
30
- mkdirSync(join(tmpdir, 'i18n'));
31
-
32
- const writes = [
33
- { name: 'login.i18n.json', content: loginContent },
34
- { name: 'register.i18n.json', content: registerContent },
35
- { name: 'register-form.i18n.json', content: {
36
- namespace: 'register',
37
- strings: {
38
- firstNameLabel: 'First Name',
39
- },
40
- } },
41
- ].map(({ name, content }) => writeFilePromisify(
42
- join(tmpdir, 'i18n', name),
43
- JSON.stringify(content, null, 4),
44
- 'utf8',
45
- ));
46
- await Promise.all(writes);
47
-
48
- await expect(
49
- () => lint(tmpdir, ['**/*.i18n.json'], false)
50
- ).rejects.toThrow('Found 2 duplicated strings.');
51
- });
52
-
53
- testWithTmpDir('test lint with duplicate string and different text', async ({ tmpdir }) => {
54
- mkdirSync(join(tmpdir, 'i18n'));
55
-
56
- const writes = [
57
- { name: 'login.i18n.json', content: loginContent },
58
- { name: 'register.i18n.json', content: registerContent },
59
- { name: 'register-form.i18n.json', content: {
60
- namespace: 'register',
61
- strings: {
62
- firstNameLabel: 'First Name*',
63
- },
64
- } },
65
- ].map(({ name, content }) => writeFilePromisify(
66
- join(tmpdir, 'i18n', name),
67
- JSON.stringify(content, null, 4),
68
- 'utf8',
69
- ));
70
- await Promise.all(writes);
71
-
72
- await expect(
73
- () => lint(tmpdir, ['**/*.i18n.json'], false)
74
- ).rejects.toThrow('Found 2 duplicated strings.');
75
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/scripts/translatte/commands/lint.ts DELETED
@@ -1,106 +0,0 @@
1
- import { listToMap } from '@togglecorp/fujs';
2
-
3
- import {
4
- getDuplicateItems,
5
- getTranslationFileNames,
6
- readTranslations,
7
- writeFilePromisify,
8
- } from '../utils';
9
-
10
- function lowercaseFirstChar(chars: string) {
11
- return chars.charAt(0).toLowerCase() + chars.slice(1);
12
- }
13
-
14
- async function lint(
15
- projectPath: string,
16
- translationFileName: string[],
17
- fix: boolean | undefined,
18
- ) {
19
- const fileNames = await getTranslationFileNames(projectPath, translationFileName);
20
- const { translations, filesContents } = await readTranslations(fileNames);
21
-
22
- const namespaces = new Set(translations.map((item) => item.namespace));
23
-
24
- console.info(`Found ${namespaces.size} namespaces.`);
25
- console.info(`Found ${translations.length} strings.`);
26
-
27
- const duplicates = getDuplicateItems(
28
- translations,
29
- (string) => `${string.namespace}:${string.key}`,
30
- );
31
-
32
- if (duplicates.length > 0) {
33
- console.info(JSON.stringify(duplicates, null, 2));
34
- throw `Found ${duplicates.length} duplicated strings.`;
35
- }
36
-
37
- // FIXME: We should get these custom rules from config file later
38
- const customRules: {
39
- location: string,
40
- namespace: ((match: RegExpMatchArray) => string) | string;
41
- }[] = [
42
- { location: '.*/app/src/views/(\\w+)/(?:.*/)?i18n.json$', namespace: (match) => lowercaseFirstChar(match[1]) },
43
- { location: '.*/app/src/components/domain/(\\w+)/(?:.*/)?i18n.json$', namespace: (match) => lowercaseFirstChar(match[1]) },
44
- { location: '.*/app/src/.*/i18n.json$', namespace: 'common' },
45
- { location: '.*/packages/ui/src/.*/i18n.json$', namespace: 'common' },
46
- ];
47
-
48
- const namespaceErrors: {
49
- fileName: string,
50
- expectedNamespace: string,
51
- receivedNamespace: string,
52
- }[] = [];
53
- for (const item of filesContents) {
54
- const { file: fileName, content: { namespace } } = item;
55
- for (const rule of customRules) {
56
- const match = fileName.match(new RegExp(rule.location));
57
- if (match) {
58
- const correctNamespace = typeof rule.namespace === 'string'
59
- ? rule.namespace
60
- : rule.namespace(match);
61
- if (correctNamespace !== namespace) {
62
- namespaceErrors.push({
63
- fileName,
64
- expectedNamespace: correctNamespace,
65
- receivedNamespace: namespace,
66
- })
67
- }
68
- break;
69
- }
70
- };
71
- };
72
-
73
- if (namespaceErrors.length > 0) {
74
- if (fix) {
75
- const metadataMapping = listToMap(
76
- filesContents,
77
- (fileContents) => fileContents.file,
78
- (fileContents) => fileContents.content,
79
- );
80
- const updates = namespaceErrors.map((namespaceError) => {
81
- const content = metadataMapping[namespaceError.fileName];
82
- const updatedContent = {
83
- ...content,
84
- namespace: namespaceError.expectedNamespace,
85
- }
86
- return writeFilePromisify(
87
- namespaceError.fileName,
88
- JSON.stringify(updatedContent, null, 4),
89
- 'utf8',
90
- );
91
- });
92
- await Promise.all(updates);
93
- console.info(`Fixed namespace in ${namespaceErrors.length} files`);
94
- } else {
95
- console.info(JSON.stringify(namespaceErrors, null, 2));
96
- throw `Found ${namespaceErrors.length} issues with namespaces.`;
97
- }
98
- }
99
-
100
- // TODO: Throw error
101
- // - if the naming of migration files is not correct
102
- // - if the parent field is not correct
103
- // - if we have duplicates
104
- }
105
-
106
- export default lint;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/scripts/translatte/commands/listMigrations.test.ts DELETED
@@ -1,48 +0,0 @@
1
- import { expect } from 'vitest';
2
- import { join } from 'path';
3
- import { mkdirSync } from 'fs';
4
-
5
- import { testWithTmpDir } from '../testHelpers';
6
- import { writeFilePromisify, getMigrationFilesAttrs } from '../utils';
7
- import {
8
- migrationContent1,
9
- migrationContent2,
10
- migrationContent3,
11
- migrationContent4,
12
- migrationContent5,
13
- } from '../mockData';
14
-
15
- testWithTmpDir('test listMigrations', async ({ tmpdir }) => {
16
- mkdirSync(join(tmpdir, 'migrations'));
17
-
18
- const writes = [
19
- { name: '001-1000000000000.json', content: migrationContent1 },
20
- { name: '002-1000000000000.json', content: migrationContent2 },
21
- { name: '003-1000000000000.json', content: migrationContent3 },
22
- { name: '004-1000000000000.json', content: migrationContent4 },
23
- { name: '005-1000000000000.json', content: migrationContent5 },
24
-
25
- { name: 'xyz-1000000000000.json', content: migrationContent5 },
26
- { name: '006-abcdefghijklm.json', content: migrationContent5 },
27
- { name: '005-1000000000000', content: migrationContent5 },
28
- { name: 'migration-6.json', content: migrationContent5 },
29
- ].map(({ name, content }) => writeFilePromisify(
30
- join(tmpdir, 'migrations', name),
31
- JSON.stringify(content, null, 4),
32
- 'utf8',
33
- ));
34
- await Promise.all(writes);
35
-
36
- expect(
37
- (await getMigrationFilesAttrs(
38
- tmpdir,
39
- 'migrations',
40
- )).map((item) => ({ ...item, fileName: undefined })),
41
- ).toEqual([
42
- { migrationName: '001-1000000000000.json', num: '001', timestamp: '1000000000000' },
43
- { migrationName: '002-1000000000000.json', num: '002', timestamp: '1000000000000' },
44
- { migrationName: '003-1000000000000.json', num: '003', timestamp: '1000000000000' },
45
- { migrationName: '004-1000000000000.json', num: '004', timestamp: '1000000000000' },
46
- { migrationName: '005-1000000000000.json', num: '005', timestamp: '1000000000000' },
47
- ]);
48
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/scripts/translatte/commands/listMigrations.ts DELETED
@@ -1,11 +0,0 @@
1
- import { getMigrationFilesAttrs } from '../utils';
2
-
3
- async function listMigrations(projectPath: string, path: string) {
4
- const migrationFileAttrs = await getMigrationFilesAttrs(projectPath, path);
5
- console.info(`Found ${migrationFileAttrs.length} migration files.`);
6
- if (migrationFileAttrs.length > 0) {
7
- console.info(migrationFileAttrs);
8
- }
9
- }
10
-
11
- export default listMigrations;
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/scripts/translatte/commands/mergeMigrations.test.ts DELETED
@@ -1,371 +0,0 @@
1
- import { expect, test } from 'vitest';
2
- import { existsSync } from 'fs';
3
- import { join } from 'path';
4
-
5
- import mergeMigrations, { merge } from './mergeMigrations';
6
- import { testWithTmpDir } from '../testHelpers';
7
- import { writeFilePromisify, readMigrations } from '../utils';
8
- import {
9
- migrationContent1,
10
- migrationContent2,
11
- migrationContent3,
12
- migrationContent4,
13
- migrationContent5,
14
- } from '../mockData';
15
-
16
- test('Test merge migrations 1-5', () => {
17
- expect(merge([
18
- migrationContent1,
19
- migrationContent2,
20
- migrationContent3,
21
- migrationContent4,
22
- migrationContent5,
23
- ])).toEqual([
24
- {
25
- "action": "add",
26
- "namespace": "login",
27
- "key": "emailLabel",
28
- "value": "Email/Username"
29
- },
30
- {
31
- "action": "add",
32
- "namespace": "login",
33
- "key": "emailPlaceholder",
34
- "value": "Email/Username*"
35
- },
36
- {
37
- "action": "add",
38
- "namespace": "login",
39
- "key": "passwordLabel",
40
- "value": "Password"
41
- },
42
- {
43
- "action": "add",
44
- "namespace": "login",
45
- "key": "loginButton",
46
- "value": "Login"
47
- },
48
- {
49
- "action": "add",
50
- "namespace": "register",
51
- "key": "firstNameLabel",
52
- "value": "First Name"
53
- },
54
- {
55
- "action": "add",
56
- "namespace": "register",
57
- "key": "lastNameLabel",
58
- "value": "Last Name"
59
- },
60
- {
61
- "action": "add",
62
- "namespace": "register",
63
- "key": "emailLabel",
64
- "value": "Email"
65
- },
66
- {
67
- "action": "add",
68
- "namespace": "register",
69
- "key": "passwordLabel",
70
- "value": "Password"
71
- },
72
- {
73
- "action": "add",
74
- "namespace": "register",
75
- "key": "confirmPasswordLabel",
76
- "value": "Confirm Password"
77
- },
78
- {
79
- "action": "add",
80
- "namespace": "login",
81
- "key": "header",
82
- "value": "If you are staff, member or volunteer of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) login with you email and password."
83
- },
84
- {
85
- "action": "add",
86
- "namespace": "register",
87
- "key": "registerButton",
88
- "value": "Register"
89
- },
90
- ]);
91
- });
92
-
93
- test('Test merge migrations 2-5', () => {
94
- expect(merge([
95
- migrationContent2,
96
- migrationContent3,
97
- migrationContent4,
98
- migrationContent5,
99
- ])).toEqual([
100
- {
101
- "action": "update",
102
- "namespace": "login",
103
- "key": "emailLabel",
104
- "newValue": "Email/Username"
105
- },
106
- {
107
- "action": "update",
108
- "namespace": "login",
109
- "key": "passwordLabel",
110
- "newValue": "Password"
111
- },
112
- {
113
- "action": "update",
114
- "namespace": "register",
115
- "key": "firstNameLabel",
116
- "newValue": "First Name"
117
- },
118
- {
119
- "action": "update",
120
- "namespace": "register",
121
- "key": "lastNameLabel",
122
- "newValue": "Last Name"
123
- },
124
- {
125
- "action": "update",
126
- "namespace": "register",
127
- "key": "emailLabel",
128
- "newValue": "Email"
129
- },
130
- {
131
- "action": "update",
132
- "namespace": "register",
133
- "key": "passwordLabel",
134
- "newValue": "Password"
135
- },
136
- {
137
- "action": "update",
138
- "namespace": "register",
139
- "key": "confirmPasswordLabel",
140
- "newValue": "Confirm Password"
141
- },
142
- {
143
- "action": "update",
144
- "key": "signUpButton",
145
- "namespace": "register",
146
- "newKey": "registerButton",
147
- "newNamespace": undefined,
148
- "newValue": "Register",
149
- },
150
- {
151
- "action": "add",
152
- "namespace": "login",
153
- "key": "header",
154
- "value": "If you are staff, member or volunteer of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) login with you email and password."
155
- },
156
- {
157
- "action": "remove",
158
- "namespace": "home",
159
- "key": "header"
160
- },
161
- {
162
- "action": "remove",
163
- "namespace": "home",
164
- "key": "subHeader"
165
- },
166
- ]);
167
- });
168
-
169
- test('Test merge migrations 3-5', () => {
170
- expect(merge([
171
- migrationContent3,
172
- migrationContent4,
173
- migrationContent5,
174
- ])).toEqual([
175
- {
176
- "action": "add",
177
- "namespace": "login",
178
- "key": "header",
179
- "value": "If you are staff, member or volunteer of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) login with you email and password."
180
- },
181
- {
182
- "action": "update",
183
- "namespace": "register",
184
- "key": "signUpButton",
185
- "newKey": "registerButton"
186
- },
187
- {
188
- "action": "remove",
189
- "namespace": "home",
190
- "key": "header"
191
- },
192
- {
193
- "action": "remove",
194
- "namespace": "home",
195
- "key": "subHeader"
196
- },
197
- ]);
198
- });
199
-
200
- test('Test merge migrations 4-5', () => {
201
- expect(merge([
202
- migrationContent4,
203
- migrationContent5,
204
- ])).toEqual([
205
- {
206
- "action": "remove",
207
- "namespace": "login",
208
- "key": "header"
209
- },
210
- {
211
- "action": "update",
212
- "namespace": "register",
213
- "key": "header",
214
- "newNamespace": "login"
215
- },
216
- {
217
- "action": "update",
218
- "namespace": "register",
219
- "key": "signUpButton",
220
- "newKey": "registerButton"
221
- },
222
- {
223
- "action": "remove",
224
- "namespace": "home",
225
- "key": "header"
226
- },
227
- {
228
- "action": "remove",
229
- "namespace": "home",
230
- "key": "subHeader"
231
- },
232
- ]);
233
- });
234
-
235
- test('Test merge migrations 5-5', () => {
236
- expect(merge([
237
- migrationContent5,
238
- ])).toEqual(migrationContent5.actions)
239
- })
240
-
241
- testWithTmpDir('test mergeMigrations 1-5', async ({ tmpdir }) => {
242
- const writes = [
243
- { name: '000001-1000000000000.json', content: migrationContent1 },
244
- { name: '000002-1000000000000.json', content: migrationContent2 },
245
- { name: '000003-1000000000000.json', content: migrationContent3 },
246
- { name: '000004-1000000000000.json', content: migrationContent4 },
247
- { name: '000005-1000000000000.json', content: migrationContent5 },
248
- ].map(({ name, content }) => writeFilePromisify(
249
- join(tmpdir, name),
250
- JSON.stringify(content, null, 4),
251
- 'utf8',
252
- ));
253
- await Promise.all(writes);
254
-
255
- await mergeMigrations(
256
- tmpdir,
257
- '.',
258
- '000001-1000000000000.json',
259
- '000005-1000000000000.json',
260
- false,
261
- );
262
-
263
- expect(existsSync(join(tmpdir, '000001-1000000000000.json'))).toBeFalsy();
264
- expect(existsSync(join(tmpdir, '000002-1000000000000.json'))).toBeFalsy();
265
- expect(existsSync(join(tmpdir, '000003-1000000000000.json'))).toBeFalsy();
266
- expect(existsSync(join(tmpdir, '000004-1000000000000.json'))).toBeFalsy();
267
- expect(existsSync(join(tmpdir, '000005-1000000000000.json'))).toBeTruthy();
268
-
269
- const generatedFiles = await readMigrations([join(tmpdir, '000005-1000000000000.json')]);
270
- const generatedFile = generatedFiles[0];
271
- expect(generatedFile.content.parent).toBe(undefined);
272
- });
273
-
274
- testWithTmpDir('test mergeMigrations 2-5', async ({ tmpdir }) => {
275
- const writes = [
276
- { name: '000001-1000000000000.json', content: migrationContent1 },
277
- { name: '000002-1000000000000.json', content: migrationContent2 },
278
- { name: '000003-1000000000000.json', content: migrationContent3 },
279
- { name: '000004-1000000000000.json', content: migrationContent4 },
280
- { name: '000005-1000000000000.json', content: migrationContent5 },
281
- ].map(({ name, content }) => writeFilePromisify(
282
- join(tmpdir, name),
283
- JSON.stringify(content, null, 4),
284
- 'utf8',
285
- ));
286
- await Promise.all(writes);
287
-
288
- await mergeMigrations(
289
- tmpdir,
290
- '.',
291
- '000002-1000000000000.json',
292
- '000005-1000000000000.json',
293
- false,
294
- );
295
-
296
- expect(existsSync(join(tmpdir, '000001-1000000000000.json'))).toBeTruthy();
297
- expect(existsSync(join(tmpdir, '000002-1000000000000.json'))).toBeFalsy();
298
- expect(existsSync(join(tmpdir, '000003-1000000000000.json'))).toBeFalsy();
299
- expect(existsSync(join(tmpdir, '000004-1000000000000.json'))).toBeFalsy();
300
- expect(existsSync(join(tmpdir, '000005-1000000000000.json'))).toBeTruthy();
301
-
302
- const generatedFiles = await readMigrations([join(tmpdir, '000005-1000000000000.json')]);
303
- const generatedFile = generatedFiles[0];
304
- expect(generatedFile.content.parent).toBe('000001-1000000000000');
305
- });
306
-
307
- testWithTmpDir('test mergeMigrations 3-5', async ({ tmpdir }) => {
308
- const writes = [
309
- { name: '000001-1000000000000.json', content: migrationContent1 },
310
- { name: '000002-1000000000000.json', content: migrationContent2 },
311
- { name: '000003-1000000000000.json', content: migrationContent3 },
312
- { name: '000004-1000000000000.json', content: migrationContent4 },
313
- { name: '000005-1000000000000.json', content: migrationContent5 },
314
- ].map(({ name, content }) => writeFilePromisify(
315
- join(tmpdir, name),
316
- JSON.stringify(content, null, 4),
317
- 'utf8',
318
- ));
319
- await Promise.all(writes);
320
-
321
- await mergeMigrations(
322
- tmpdir,
323
- '.',
324
- '000003-1000000000000.json',
325
- '000005-1000000000000.json',
326
- false,
327
- );
328
-
329
- expect(existsSync(join(tmpdir, '000001-1000000000000.json'))).toBeTruthy();
330
- expect(existsSync(join(tmpdir, '000002-1000000000000.json'))).toBeTruthy();
331
- expect(existsSync(join(tmpdir, '000003-1000000000000.json'))).toBeFalsy();
332
- expect(existsSync(join(tmpdir, '000004-1000000000000.json'))).toBeFalsy();
333
- expect(existsSync(join(tmpdir, '000005-1000000000000.json'))).toBeTruthy();
334
-
335
- const generatedFiles = await readMigrations([join(tmpdir, '000005-1000000000000.json')]);
336
- const generatedFile = generatedFiles[0];
337
- expect(generatedFile.content.parent).toBe('000002-1000000000000');
338
- });
339
-
340
- testWithTmpDir('test mergeMigrations 4-5', async ({ tmpdir }) => {
341
- const writes = [
342
- { name: '000001-1000000000000.json', content: migrationContent1 },
343
- { name: '000002-1000000000000.json', content: migrationContent2 },
344
- { name: '000003-1000000000000.json', content: migrationContent3 },
345
- { name: '000004-1000000000000.json', content: migrationContent4 },
346
- { name: '000005-1000000000000.json', content: migrationContent5 },
347
- ].map(({ name, content }) => writeFilePromisify(
348
- join(tmpdir, name),
349
- JSON.stringify(content, null, 4),
350
- 'utf8',
351
- ));
352
- await Promise.all(writes);
353
-
354
- await mergeMigrations(
355
- tmpdir,
356
- '.',
357
- '000004-1000000000000.json',
358
- '000005-1000000000000.json',
359
- false,
360
- );
361
-
362
- expect(existsSync(join(tmpdir, '000001-1000000000000.json'))).toBeTruthy();
363
- expect(existsSync(join(tmpdir, '000002-1000000000000.json'))).toBeTruthy();
364
- expect(existsSync(join(tmpdir, '000003-1000000000000.json'))).toBeTruthy();
365
- expect(existsSync(join(tmpdir, '000004-1000000000000.json'))).toBeFalsy();
366
- expect(existsSync(join(tmpdir, '000005-1000000000000.json'))).toBeTruthy();
367
-
368
- const generatedFiles = await readMigrations([join(tmpdir, '000005-1000000000000.json')]);
369
- const generatedFile = generatedFiles[0];
370
- expect(generatedFile.content.parent).toBe('000003-1000000000000');
371
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
go-web-app-develop/app/scripts/translatte/commands/mergeMigrations.ts DELETED
@@ -1,220 +0,0 @@
1
- import { listToMap, isDefined } from '@togglecorp/fujs';
2
-
3
- import { MigrationActionItem, MigrationFileContent } from '../types';
4
- import {
5
- concat,
6
- removeUndefinedKeys,
7
- getMigrationFilesAttrs,
8
- readMigrations,
9
- removeFiles,
10
- writeFilePromisify
11
- } from '../utils';
12
-
13
- function getCanonicalKey(
14
- item: MigrationActionItem,
15
- opts: { useNewKey: boolean },
16
- ) {
17
- if (opts.useNewKey && item.action === 'update') {
18
- return concat(
19
- item.newNamespace ?? item.namespace,
20
- item.newKey ?? item.key,
21
- );
22
- }
23
- return concat(
24
- item.namespace,
25
- item.key,
26
- );
27
- }
28
-
29
- function mergeMigrationActionItems(
30
- prevMigrationActionItems: MigrationActionItem[],
31
- nextMigrationActionItems: MigrationActionItem[],
32
- ) {
33
- interface PrevMappings {
34
- [key: string]: MigrationActionItem,
35
- }
36
-
37
- const prevCanonicalKeyMappings: PrevMappings = listToMap(
38
- prevMigrationActionItems,
39
- (item) => getCanonicalKey(item, { useNewKey: true }),
40
- (item) => item,
41
- );
42
-
43
- interface NextMappings {
44
- [key: string]: MigrationActionItem | null,
45
- }
46
-
47
- const nextMappings = nextMigrationActionItems.reduce<NextMappings>(
48
- (acc, nextMigrationActionItem) => {
49
- const canonicalKey = getCanonicalKey(nextMigrationActionItem, { useNewKey: false })
50
-
51
- const prevItemWithCanonicalKey = prevCanonicalKeyMappings[canonicalKey];
52
- // const prevItemWithKey = prevKeyMappings[nextMigrationActionItem.key];
53
-
54
- if (!prevItemWithCanonicalKey) {
55
- return {
56
- ...acc,
57
- [canonicalKey]: nextMigrationActionItem,
58
- };
59
- }
60
-
61
- if (prevItemWithCanonicalKey.action === 'add' && nextMigrationActionItem.action === 'add') {
62
- throw `Action 'add' already exists for '${canonicalKey}'`;
63
- }
64
- if (prevItemWithCanonicalKey.action === 'add' && nextMigrationActionItem.action === 'remove') {
65
- return {
66
- ...acc,
67
- [canonicalKey]: null,
68
- };
69
- }
70
- if (prevItemWithCanonicalKey.action === 'add' && nextMigrationActionItem.action === 'update') {
71
- const newKey = nextMigrationActionItem.newKey
72
- ?? prevItemWithCanonicalKey.key;
73
- const newNamespace = nextMigrationActionItem.newNamespace
74
- ?? prevItemWithCanonicalKey.namespace;
75
-
76
- const newMigrationItem = removeUndefinedKeys<MigrationActionItem>({
77
- action: 'add',
78
- namespace: newNamespace,
79
- key: newKey,
80
- value: nextMigrationActionItem.newValue
81
- ?? prevItemWithCanonicalKey.value,
82
- });
83
-
84
- const newCanonicalKey = getCanonicalKey(newMigrationItem, { useNewKey: true });
85
- if (acc[newCanonicalKey] !== undefined && acc[newCanonicalKey] !== null) {
86
- throw `Action 'update' cannot be applied to '${newCanonicalKey}' as the key already exists`;
87
- }
88
-
89
- return {
90
- ...acc,
91
- // Setting null so that we remove them on the mappings.
92
- // No need to set null, if we have already overridden with other value
93
- [canonicalKey]: acc[canonicalKey] === undefined || acc[canonicalKey] === null
94
- ? null
95
- : acc[canonicalKey],
96
- [newCanonicalKey]: newMigrationItem,
97
- }
98
- }
99
- if (prevItemWithCanonicalKey.action === 'remove' && nextMigrationActionItem.action === 'add') {
100
- return {
101
- ...acc,
102
- [canonicalKey]: removeUndefinedKeys<MigrationActionItem>({
103
- action: 'update',
104
- namespace: prevItemWithCanonicalKey.namespace,
105
- key: prevItemWithCanonicalKey.key,
106
- newValue: nextMigrationActionItem.value,
107
- })
108
- };
109
- }
110
- if (prevItemWithCanonicalKey.action === 'remove' && nextMigrationActionItem.action === 'remove') {
111
- // pass
112
- return acc;
113
- }
114
- if (prevItemWithCanonicalKey.action === 'remove' && nextMigrationActionItem.action === 'update') {
115
- throw `Action 'update' cannot be applied to '${canonicalKey}' after action 'remove'`;
116
- }
117
- if (prevItemWithCanonicalKey.action === 'update' && nextMigrationActionItem.action === 'add') {
118
- throw `Action 'add' cannot be applied to '${canonicalKey}' after action 'update'`;
119
- }
120
- if (prevItemWithCanonicalKey.action === 'update' && nextMigrationActionItem.action === 'update') {
121
- return {
122
- ...acc,
123
- [canonicalKey]: removeUndefinedKeys<MigrationActionItem>({
124
- action: 'update',
125
- namespace: prevItemWithCanonicalKey.namespace,
126
- key: prevItemWithCanonicalKey.key,
127
- newNamespace: nextMigrationActionItem.newNamespace ?? prevItemWithCanonicalKey.newNamespace,
128
- newKey: nextMigrationActionItem.newKey ?? prevItemWithCanonicalKey.newKey,
129
- newValue: nextMigrationActionItem.newValue ?? prevItemWithCanonicalKey.newValue,
130
- }),
131
- };
132
- }
133
- if (prevItemWithCanonicalKey.action === 'update' && nextMigrationActionItem.action === 'remove') {
134
- return {
135
- ...acc,
136
- [canonicalKey]: removeUndefinedKeys<MigrationActionItem>({
137
- action: 'remove',
138
- namespace: prevItemWithCanonicalKey.namespace,
139
- key: prevItemWithCanonicalKey.key,
140
- }),
141
- };
142
- }
143
- return acc;
144
- },
145
- {},
146
- );
147
-
148
- const finalMappings = {
149
- ...prevCanonicalKeyMappings,
150
- ...nextMappings,
151
- };
152
-
153
- return Object.values(finalMappings).filter(isDefined);
154
- }
155
-
156
- export function merge(migrationFileContents: MigrationFileContent[]) {
157
- const migrationActionItems = migrationFileContents.reduce<MigrationActionItem[]>(
158
- (acc, migrationActionItem) => {
159
- const newMigrationItems = mergeMigrationActionItems(acc, migrationActionItem.actions)
160
- return newMigrationItems;
161
- },
162
- [],
163
- );
164
-
165
- return migrationActionItems;
166
- }
167
-
168
- async function mergeMigrations(
169
- projectPath: string,
170
- path: string,
171
- from: string,
172
- to: string,
173
- dryRun: boolean | undefined,
174
- ) {
175
- const migrationFilesAttrs = await getMigrationFilesAttrs(projectPath, path);
176
- const selectedMigrationFilesAttrs = migrationFilesAttrs.filter(
177
- (item) => (item.migrationName >= from && item.migrationName <= to)
178
- );
179
- console.info(`Found ${selectedMigrationFilesAttrs.length} migration files`);
180
-
181
- if (selectedMigrationFilesAttrs.length <= 1) {
182
- throw 'There should be atleast 2 migration files';
183
- }
184
- const selectedMigrations = await readMigrations(
185
- selectedMigrationFilesAttrs.map((migration) => migration.fileName),
186
- );
187
-
188
- const firstMigration= selectedMigrations[0];
189
- const lastMigration = selectedMigrations[selectedMigrations.length - 1];
190
-
191
- const selectedMigrationsFileNames = selectedMigrationFilesAttrs.map((migration) => migration.fileName);
192
-
193
- const mergedMigrationContent = {
194
- actions: merge(selectedMigrations.map((migration) => migration.content)),
195
- parent: firstMigration.content.parent,
196
- };
197
-
198
- if (dryRun) {
199
- console.info('Deleting the following migration files');
200
- console.info(selectedMigrationsFileNames);
201
- } else {
202
- await removeFiles(
203
- selectedMigrationsFileNames,
204
- );
205
- }
206
-
207
- const newFileName = lastMigration.file;
208
- if (dryRun) {
209
- console.info(`Creating migration file '${newFileName}'`);
210
- console.info(mergedMigrationContent);
211
- } else {
212
- await writeFilePromisify(
213
- newFileName,
214
- JSON.stringify(mergedMigrationContent, null, 4),
215
- 'utf8',
216
- );
217
- }
218
- }
219
-
220
- export default mergeMigrations;