SCGR commited on
Commit
d7291ef
·
1 Parent(s): d9e6017

Integrate VLMs

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +24 -5
  2. frontend/src/App.tsx +83 -1
  3. frontend/src/components/Card.tsx +34 -0
  4. frontend/src/components/HeaderNav.tsx +47 -36
  5. frontend/src/index.css +41 -0
  6. frontend/src/main.tsx +1 -0
  7. frontend/src/pages/AnalyticsPage.tsx +569 -3
  8. frontend/src/pages/DemoPage.tsx +1079 -0
  9. frontend/src/pages/DevPage.tsx +245 -0
  10. frontend/src/pages/ExplorePage.tsx +192 -112
  11. frontend/src/pages/MapDetailPage.tsx +20 -22
  12. frontend/src/pages/UploadPage.tsx +302 -228
  13. go-web-app-develop/.changeset/README.md +8 -0
  14. go-web-app-develop/.changeset/config.json +15 -0
  15. go-web-app-develop/.changeset/lovely-kids-boil.md +5 -0
  16. go-web-app-develop/.changeset/pre.json +15 -0
  17. go-web-app-develop/.changeset/solid-clubs-care.md +8 -0
  18. go-web-app-develop/.changeset/sweet-gifts-cheer.md +9 -0
  19. go-web-app-develop/.changeset/whole-lions-guess.md +7 -0
  20. go-web-app-develop/.dockerignore +148 -0
  21. go-web-app-develop/.github/ISSUE_TEMPLATE/01_bug_report.yml +92 -0
  22. go-web-app-develop/.github/ISSUE_TEMPLATE/02_feature_request.yml +39 -0
  23. go-web-app-develop/.github/ISSUE_TEMPLATE/03_epic_request.yml +37 -0
  24. go-web-app-develop/.github/ISSUE_TEMPLATE/config.yml +5 -0
  25. go-web-app-develop/.github/dependabot.yml +27 -0
  26. go-web-app-develop/.github/pull_request_template.md +30 -0
  27. go-web-app-develop/.github/workflows/add-issue-to-backlog.yml +16 -0
  28. go-web-app-develop/.github/workflows/chromatic.yml +127 -0
  29. go-web-app-develop/.github/workflows/ci.yml +304 -0
  30. go-web-app-develop/.github/workflows/publish-nginx-serve.yml +147 -0
  31. go-web-app-develop/.github/workflows/publish-storybook-nginx-serve.yml +127 -0
  32. go-web-app-develop/.gitignore +43 -0
  33. go-web-app-develop/.npmrc +1 -0
  34. go-web-app-develop/COLLABORATING.md +18 -0
  35. go-web-app-develop/CONTRIBUTING.md +81 -0
  36. go-web-app-develop/LICENSE +21 -0
  37. go-web-app-develop/README.md +117 -0
  38. go-web-app-develop/app/CHANGELOG.md +729 -0
  39. go-web-app-develop/app/env.ts +29 -0
  40. go-web-app-develop/app/eslint.config.js +165 -0
  41. go-web-app-develop/app/index.html +69 -0
  42. go-web-app-develop/app/package.json +119 -0
  43. go-web-app-develop/app/postcss.config.cjs +8 -0
  44. go-web-app-develop/app/public/go-icon.svg +4 -0
  45. go-web-app-develop/app/scripts/translatte/README.md +59 -0
  46. go-web-app-develop/app/scripts/translatte/commands/applyMigrations.test.ts +104 -0
  47. go-web-app-develop/app/scripts/translatte/commands/applyMigrations.ts +177 -0
  48. go-web-app-develop/app/scripts/translatte/commands/exportMigration.ts +62 -0
  49. go-web-app-develop/app/scripts/translatte/commands/generateMigration.test.ts +102 -0
  50. go-web-app-develop/app/scripts/translatte/commands/generateMigration.ts +195 -0
.gitignore CHANGED
@@ -5,13 +5,31 @@ build/
5
  .cache/
6
  .vite/
7
 
8
- # Local env & secrets
9
  .env*
10
  !.env.example
11
 
12
- # ─── Go backend (we’ll add later) ───────────────
13
- /backend/bin/
14
- /backend/coverage.out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  # ─── Docker ─────────────────────────────────────
17
  *.log
@@ -20,8 +38,9 @@ docker-compose.override.yml
20
  # ─── OS / editor cruft ─────────────────────────
21
  .DS_Store
22
  Thumbs.db
 
23
  .idea/
24
  .vscode/
25
- *.swp
26
 
 
27
  /generated/prisma
 
5
  .cache/
6
  .vite/
7
 
8
+ # ─── Local env & secrets ────────────────────────
9
  .env*
10
  !.env.example
11
 
12
+ # ─── Python / FastAPI backend ───────────────────
13
+ # Byte-compiled / optimized / DLL files
14
+ __pycache__/
15
+ *.py[cod]
16
+ *$py.class
17
+
18
+ # Virtual environments
19
+ .venv/
20
+ venv/
21
+
22
+ # Distribution / packaging
23
+ *.egg-info/
24
+ dist/
25
+ build/
26
+ *.whl
27
+
28
+ # Testing / coverage
29
+ .coverage
30
+ .pytest_cache/
31
+ htmlcov/
32
+ .mypy_cache/
33
 
34
  # ─── Docker ─────────────────────────────────────
35
  *.log
 
38
  # ─── OS / editor cruft ─────────────────────────
39
  .DS_Store
40
  Thumbs.db
41
+ *.swp
42
  .idea/
43
  .vscode/
 
44
 
45
+ # ─── Prisma (if you’re using it) ────────────────
46
  /generated/prisma
frontend/src/App.tsx CHANGED
@@ -1,10 +1,15 @@
1
  import { createBrowserRouter, RouterProvider } from 'react-router-dom';
 
 
 
2
  import RootLayout from './layouts/RootLayout';
3
  import UploadPage from './pages/UploadPage';
4
  import AnalyticsPage from './pages/AnalyticsPage';
5
  import ExplorePage from './pages/ExplorePage';
6
  import HelpPage from './pages/HelpPage';
7
  import MapDetailPage from './pages/MapDetailPage';
 
 
8
 
9
  const router = createBrowserRouter([
10
  {
@@ -15,11 +20,88 @@ const router = createBrowserRouter([
15
  { path: '/analytics', element: <AnalyticsPage /> },
16
  { path: '/explore', element: <ExplorePage /> },
17
  { path: '/help', element: <HelpPage /> },
 
 
18
  { path: '/map/:mapId', element: <MapDetailPage /> },
19
  ],
20
  },
21
  ]);
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  export default function App() {
24
- return <RouterProvider router={router} />;
25
  }
 
1
  import { createBrowserRouter, RouterProvider } from 'react-router-dom';
2
+ import { AlertContext, type AlertContextProps, type AlertParams, LanguageContext, type LanguageContextProps } from '@ifrc-go/ui/contexts';
3
+ import { useCallback, useMemo, useState } from 'react';
4
+ import { unique } from '@togglecorp/fujs';
5
  import RootLayout from './layouts/RootLayout';
6
  import UploadPage from './pages/UploadPage';
7
  import AnalyticsPage from './pages/AnalyticsPage';
8
  import ExplorePage from './pages/ExplorePage';
9
  import HelpPage from './pages/HelpPage';
10
  import MapDetailPage from './pages/MapDetailPage';
11
+ import DemoPage from './pages/DemoPage';
12
+ import DevPage from './pages/DevPage';
13
 
14
  const router = createBrowserRouter([
15
  {
 
20
  { path: '/analytics', element: <AnalyticsPage /> },
21
  { path: '/explore', element: <ExplorePage /> },
22
  { path: '/help', element: <HelpPage /> },
23
+ { path: '/demo', element: <DemoPage /> },
24
+ { path: '/dev', element: <DevPage /> },
25
  { path: '/map/:mapId', element: <MapDetailPage /> },
26
  ],
27
  },
28
  ]);
29
 
30
+ function Application() {
31
+ // ALERTS
32
+ const [alerts, setAlerts] = useState<AlertParams[]>([]);
33
+
34
+ const addAlert = useCallback((alert: AlertParams) => {
35
+ setAlerts((prevAlerts) => unique(
36
+ [...prevAlerts, alert],
37
+ (a) => a.name,
38
+ ) ?? prevAlerts);
39
+ }, [setAlerts]);
40
+
41
+ const removeAlert = useCallback((name: AlertParams['name']) => {
42
+ setAlerts((prevAlerts) => {
43
+ const i = prevAlerts.findIndex((a) => a.name === name);
44
+ if (i === -1) {
45
+ return prevAlerts;
46
+ }
47
+
48
+ const newAlerts = [...prevAlerts];
49
+ newAlerts.splice(i, 1);
50
+
51
+ return newAlerts;
52
+ });
53
+ }, [setAlerts]);
54
+
55
+ const updateAlert = useCallback((name: AlertParams['name'], paramsWithoutName: Omit<AlertParams, 'name'>) => {
56
+ setAlerts((prevAlerts) => {
57
+ const i = prevAlerts.findIndex((a) => a.name === name);
58
+ if (i === -1) {
59
+ return prevAlerts;
60
+ }
61
+
62
+ const newAlerts = [...prevAlerts];
63
+ newAlerts[i] = {
64
+ ...newAlerts[i],
65
+ ...paramsWithoutName,
66
+ };
67
+
68
+ return newAlerts;
69
+ });
70
+ }, [setAlerts]);
71
+
72
+ const alertContextValue = useMemo<AlertContextProps>(
73
+ () => ({
74
+ alerts,
75
+ addAlert,
76
+ removeAlert,
77
+ updateAlert,
78
+ }),
79
+ [alerts, addAlert, removeAlert, updateAlert],
80
+ );
81
+
82
+ // LANGUAGE
83
+ const languageContextValue = useMemo<LanguageContextProps>(
84
+ () => ({
85
+ languageNamespaceStatus: {},
86
+ setLanguageNamespaceStatus: () => {},
87
+ currentLanguage: 'en',
88
+ setCurrentLanguage: () => {},
89
+ strings: {},
90
+ setStrings: () => {},
91
+ registerNamespace: () => {},
92
+ }),
93
+ [],
94
+ );
95
+
96
+ return (
97
+ <AlertContext.Provider value={alertContextValue}>
98
+ <LanguageContext.Provider value={languageContextValue}>
99
+ <RouterProvider router={router} />
100
+ </LanguageContext.Provider>
101
+ </AlertContext.Provider>
102
+ );
103
+ }
104
+
105
  export default function App() {
106
+ return <Application />;
107
  }
frontend/src/components/Card.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/components/Card.tsx
2
+ import React from 'react'
3
+
4
+ export interface CardProps {
5
+ /** extra Tailwind classes to apply to the wrapper */
6
+ className?: string
7
+ /** contents of the card */
8
+ children: React.ReactNode
9
+ }
10
+
11
+ /**
12
+ * A simple white card with rounded corners, padding and soft shadow.
13
+ *
14
+ * Usage:
15
+ * import Card from '../components/Card'
16
+ *
17
+ * <Card className="max-w-md mx-auto">
18
+ * <h3>Title</h3>
19
+ * <p>Body content</p>
20
+ * </Card>
21
+ */
22
+ export default function Card({ children, className = '' }: CardProps) {
23
+ return (
24
+ <div
25
+ className={
26
+ `bg-white rounded-lg shadow p-6 ` +
27
+ className
28
+ }
29
+ >
30
+ {children}
31
+ </div>
32
+ )
33
+ }
34
+
frontend/src/components/HeaderNav.tsx CHANGED
@@ -1,63 +1,74 @@
1
- import { NavLink, useLocation } from "react-router-dom";
 
2
  import {
3
  UploadCloudLineIcon,
4
  AnalysisIcon,
5
  SearchLineIcon,
6
  QuestionLineIcon,
7
  GoMainIcon,
 
8
  } from "@ifrc-go/icons";
9
 
10
- /* Style helper for active vs. inactive nav links */
11
- const navLink = ({ isActive }: { isActive: boolean }) =>
12
- `flex items-center gap-1 px-4 sm:px-6 py-2 text-xs sm:text-sm transition-colors whitespace-nowrap mx-4 sm:mx-6
13
- ${isActive ? "text-ifrcRed font-semibold" : "text-gray-600 hover:text-ifrcRed"}`;
14
-
15
- /* Put page info in one list so it’s easy to extend */
16
  const navItems = [
17
  { to: "/upload", label: "Upload", Icon: UploadCloudLineIcon },
18
- { to: "/analytics", label: "Analytics", Icon: AnalysisIcon },
19
  { to: "/explore", label: "Explore", Icon: SearchLineIcon },
 
 
20
  ];
21
 
22
  export default function HeaderNav() {
23
  const location = useLocation();
24
-
25
- const handleNavigation = (e: React.MouseEvent, to: string) => {
26
- if (location.pathname === "/upload") {
27
- const uploadPage = document.querySelector('[data-step="2"]');
28
- if (uploadPage) {
29
- e.preventDefault();
30
- if (confirm("Changes will not be saved")) {
31
- window.location.href = to;
32
- }
33
- }
34
- }
35
- };
36
 
37
  return (
38
- <header className="bg-white border-b border-ifrcRed/40">
39
- <div className="flex items-center justify-between px-2 sm:px-4 py-3 max-w-full overflow-hidden">
40
-
 
 
41
  {/* ── Logo + title ─────────────────────────── */}
42
- <NavLink to="/" className="flex items-center gap-2 min-w-0" onClick={(e) => handleNavigation(e, "/")}>
43
- <GoMainIcon className="h-6 w-6 flex-shrink-0 text-ifrcRed" />
44
- <span className="font-semibold text-sm sm:text-base truncate">PromptAid Vision</span>
45
- </NavLink>
46
 
47
  {/* ── Centre nav links ─────────────────────── */}
48
- <nav className="flex flex-wrap justify-center gap-6">
49
- {navItems.map(({ to, label, Icon }) => (
50
- <NavLink key={to} to={to} className={navLink} onClick={(e) => handleNavigation(e, to)}>
51
- <Icon className="w-4 h-4" /> <span className="inline">{label}</span>
52
- </NavLink>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  ))}
54
  </nav>
55
 
56
  {/* ── Right-side utility buttons ───────────── */}
57
- <NavLink to="/help" className={navLink}>
 
 
 
 
 
58
  <QuestionLineIcon className="w-4 h-4" />
59
- </NavLink>
60
- </div>
61
- </header>
 
62
  );
63
  }
 
1
+ import { useLocation, useNavigate } from "react-router-dom";
2
+ import { Button, PageContainer } from "@ifrc-go/ui";
3
  import {
4
  UploadCloudLineIcon,
5
  AnalysisIcon,
6
  SearchLineIcon,
7
  QuestionLineIcon,
8
  GoMainIcon,
9
+ SettingsIcon,
10
  } from "@ifrc-go/icons";
11
 
12
+ /* Put page info in one list so it's easy to extend */
 
 
 
 
 
13
  const navItems = [
14
  { to: "/upload", label: "Upload", Icon: UploadCloudLineIcon },
 
15
  { to: "/explore", label: "Explore", Icon: SearchLineIcon },
16
+ { to: "/analytics", label: "Analytics", Icon: AnalysisIcon },
17
+ { to: "/dev", label: "Dev", Icon: SettingsIcon },
18
  ];
19
 
20
  export default function HeaderNav() {
21
  const location = useLocation();
22
+ const navigate = useNavigate();
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  return (
25
+ <nav className="border-b border-gray-200 bg-white">
26
+ <PageContainer
27
+ className="border-b border-ifrcRed"
28
+ contentClassName="flex items-center justify-between py-4"
29
+ >
30
  {/* ── Logo + title ─────────────────────────── */}
31
+ <div className="flex items-center gap-3 min-w-0 cursor-pointer" onClick={() => navigate('/')}>
32
+ <GoMainIcon className="h-8 w-8 flex-shrink-0 text-ifrcRed" />
33
+ <span className="font-semibold text-lg truncate text-gray-900">PromptAid Vision</span>
34
+ </div>
35
 
36
  {/* ── Centre nav links ─────────────────────── */}
37
+ <nav className="flex items-center">
38
+ {navItems.map(({ to, label, Icon }, index) => (
39
+ <div key={to} className={index < navItems.length - 1 ? "mr-8" : ""}>
40
+ <Button
41
+ name={label.toLowerCase()}
42
+ variant={location.pathname === to ? "primary" : "tertiary"}
43
+ size={1}
44
+ onClick={() => {
45
+ if (location.pathname === "/upload") {
46
+ const uploadPage = document.querySelector('[data-step="2"]');
47
+ if (uploadPage && !confirm("Changes will not be saved")) {
48
+ return;
49
+ }
50
+ }
51
+ navigate(to);
52
+ }}
53
+ >
54
+ <Icon className="w-4 h-4" />
55
+ <span className="inline ml-2 font-medium">{label}</span>
56
+ </Button>
57
+ </div>
58
  ))}
59
  </nav>
60
 
61
  {/* ── Right-side utility buttons ───────────── */}
62
+ <Button
63
+ name="help"
64
+ variant="tertiary"
65
+ size={1}
66
+ onClick={() => navigate('/help')}
67
+ >
68
  <QuestionLineIcon className="w-4 h-4" />
69
+ <span className="inline ml-2 font-medium">Help & Support</span>
70
+ </Button>
71
+ </PageContainer>
72
+ </nav>
73
  );
74
  }
frontend/src/index.css CHANGED
@@ -2,3 +2,44 @@
2
  @tailwind base;
3
  @tailwind components;
4
  @tailwind utilities;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  @tailwind base;
3
  @tailwind components;
4
  @tailwind utilities;
5
+
6
+ * {
7
+ box-sizing: border-box;
8
+ }
9
+
10
+ html {
11
+ @media screen {
12
+ margin: 0;
13
+ padding: 0;
14
+ scrollbar-gutter: stable;
15
+ }
16
+ }
17
+
18
+ body {
19
+ line-height: var(--go-ui-line-height-md);
20
+ color: var(--go-ui-color-text);
21
+ font-family: var(--go-ui-font-family-sans-serif);
22
+ font-size: var(--go-ui-font-size-md);
23
+ font-weight: var(--go-ui-font-weight-normal);
24
+
25
+ @media screen {
26
+ margin: 0;
27
+ background-color: var(--go-ui-color-background);
28
+ padding: 0;
29
+ }
30
+ }
31
+
32
+ ul, ol, p {
33
+ margin: 0;
34
+ }
35
+
36
+ @media print {
37
+ @page {
38
+ size: portrait A4;
39
+ margin: 10mm 10mm 16mm 10mm;
40
+ }
41
+
42
+ body {
43
+ font-family: 'Open Sans', sans-serif;
44
+ }
45
+ }
frontend/src/main.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import { StrictMode } from 'react'
2
  import { createRoot } from 'react-dom/client'
3
  import './index.css'
 
1
+ import '@ifrc-go/ui/index.css';
2
  import { StrictMode } from 'react'
3
  import { createRoot } from 'react-dom/client'
4
  import './index.css'
frontend/src/pages/AnalyticsPage.tsx CHANGED
@@ -1,9 +1,575 @@
1
- import { PageContainer, Heading } from '@ifrc-go/ui';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  export default function AnalyticsPage() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  return (
5
- <PageContainer className="py-10 text-center">
6
- <Heading level={2}>Analytics</Heading>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  </PageContainer>
8
  );
9
  }
 
1
+ // src/pages/AnalyticsPage.tsx
2
+
3
+ import {
4
+ PageContainer,
5
+ PieChart,
6
+ KeyFigure,
7
+ Spinner,
8
+ Container,
9
+ ProgressBar,
10
+ SegmentInput,
11
+ Table,
12
+ } from '@ifrc-go/ui';
13
+ import {
14
+ createStringColumn,
15
+ createNumberColumn,
16
+ numericIdSelector
17
+ } from '@ifrc-go/ui/utils';
18
+ import { useState, useEffect, useMemo } from 'react';
19
+ // icons not used on this page
20
+
21
+ interface AnalyticsData {
22
+ totalCaptions: number;
23
+ sources: { [key: string]: number };
24
+ types: { [key: string]: number };
25
+ regions: { [key: string]: number };
26
+ models: {
27
+ [key: string]: {
28
+ count: number;
29
+ avgAccuracy: number;
30
+ avgContext: number;
31
+ avgUsability: number;
32
+ totalScore: number;
33
+ };
34
+ };
35
+ }
36
+
37
+ interface LookupData {
38
+ s_code?: string;
39
+ t_code?: string;
40
+ r_code?: string;
41
+ label: string;
42
+ }
43
+
44
+ interface RegionData {
45
+ id: number;
46
+ name: string;
47
+ count: number;
48
+ percentage: number;
49
+ }
50
+
51
+ interface TypeData {
52
+ id: number;
53
+ name: string;
54
+ count: number;
55
+ percentage: number;
56
+ }
57
+
58
+ interface SourceData {
59
+ id: number;
60
+ name: string;
61
+ count: number;
62
+ percentage: number;
63
+ }
64
+
65
+ interface ModelData {
66
+ id: number;
67
+ name: string;
68
+ count: number;
69
+ accuracy: number;
70
+ context: number;
71
+ usability: number;
72
+ totalScore: number;
73
+ }
74
 
75
  export default function AnalyticsPage() {
76
+ const [data, setData] = useState<AnalyticsData | null>(null);
77
+ const [loading, setLoading] = useState(true);
78
+ const [view, setView] = useState<'general' | 'vlm'>('general');
79
+ const [sourcesLookup, setSourcesLookup] = useState<LookupData[]>([]);
80
+ const [typesLookup, setTypesLookup] = useState<LookupData[]>([]);
81
+ const [regionsLookup, setRegionsLookup] = useState<LookupData[]>([]);
82
+
83
+ // SegmentInput options for analytics view
84
+ const viewOptions = [
85
+ { key: 'general' as const, label: 'General Analytics' },
86
+ { key: 'vlm' as const, label: 'VLM Analytics' }
87
+ ];
88
+
89
+ useEffect(() => {
90
+ fetchAnalytics();
91
+ fetchLookupData();
92
+ }, []);
93
+
94
+ async function fetchAnalytics() {
95
+ setLoading(true);
96
+ try {
97
+ const res = await fetch('/api/images/');
98
+ const maps = await res.json();
99
+
100
+ const analytics: AnalyticsData = {
101
+ totalCaptions: maps.length,
102
+ sources: {},
103
+ types: {},
104
+ regions: {},
105
+ models: {},
106
+ };
107
+
108
+ maps.forEach((map: any) => {
109
+ if (map.source) analytics.sources[map.source] = (analytics.sources[map.source] || 0) + 1;
110
+ if (map.type) analytics.types[map.type] = (analytics.types[map.type] || 0) + 1;
111
+ if (map.countries) {
112
+ map.countries.forEach((c: any) => {
113
+ if (c.r_code) analytics.regions[c.r_code] = (analytics.regions[c.r_code] || 0) + 1;
114
+ });
115
+ }
116
+ if (map.caption?.model) {
117
+ const m = map.caption.model;
118
+ const ctr = analytics.models[m] ||= { count: 0, avgAccuracy: 0, avgContext: 0, avgUsability: 0, totalScore: 0 };
119
+ ctr.count++;
120
+ if (map.caption.accuracy != null) ctr.avgAccuracy += map.caption.accuracy;
121
+ if (map.caption.context != null) ctr.avgContext += map.caption.context;
122
+ if (map.caption.usability != null) ctr.avgUsability += map.caption.usability;
123
+ }
124
+ });
125
+
126
+ // Add all sources and types with 0 values for missing data
127
+ sourcesLookup.forEach(source => {
128
+ if (source.s_code && !analytics.sources[source.s_code]) {
129
+ analytics.sources[source.s_code] = 0;
130
+ }
131
+ });
132
+
133
+ typesLookup.forEach(type => {
134
+ if (type.t_code && !analytics.types[type.t_code]) {
135
+ analytics.types[type.t_code] = 0;
136
+ }
137
+ });
138
+
139
+ // Add all regions with 0 values for missing data
140
+ regionsLookup.forEach(region => {
141
+ if (region.r_code && !analytics.regions[region.r_code]) {
142
+ analytics.regions[region.r_code] = 0;
143
+ }
144
+ });
145
+
146
+ // Add all models with 0 values for missing data
147
+ const allModels = ['GPT-4', 'Claude', 'Gemini', 'Llama', 'Other'];
148
+ allModels.forEach(model => {
149
+ if (!analytics.models[model]) {
150
+ analytics.models[model] = { count: 0, avgAccuracy: 0, avgContext: 0, avgUsability: 0, totalScore: 0 };
151
+ }
152
+ });
153
+
154
+ Object.values(analytics.models).forEach(m => {
155
+ if (m.count > 0) {
156
+ m.avgAccuracy = Math.round(m.avgAccuracy / m.count);
157
+ m.avgContext = Math.round(m.avgContext / m.count);
158
+ m.avgUsability = Math.round(m.avgUsability / m.count);
159
+ m.totalScore = Math.round((m.avgAccuracy + m.avgContext + m.avgUsability) / 3);
160
+ }
161
+ });
162
+
163
+ setData(analytics);
164
+ } catch (e) {
165
+ console.error(e);
166
+ setData(null);
167
+ } finally {
168
+ setLoading(false);
169
+ }
170
+ }
171
+
172
+ async function fetchLookupData() {
173
+ try {
174
+ const [sourcesRes, typesRes, regionsRes] = await Promise.all([
175
+ fetch('/api/sources'),
176
+ fetch('/api/types'),
177
+ fetch('/api/regions')
178
+ ]);
179
+ const sources = await sourcesRes.json();
180
+ const types = await typesRes.json();
181
+ const regions = await regionsRes.json();
182
+ setSourcesLookup(sources);
183
+ setTypesLookup(types);
184
+ setRegionsLookup(regions);
185
+ } catch (e) {
186
+ console.error('Failed to fetch lookup data:', e);
187
+ }
188
+ }
189
+
190
+ const getSourceLabel = (code: string) => {
191
+ const source = sourcesLookup.find(s => s.s_code === code);
192
+ return source ? source.label : code;
193
+ };
194
+
195
+ const getTypeLabel = (code: string) => {
196
+ const type = typesLookup.find(t => t.t_code === code);
197
+ return type ? type.label : code;
198
+ };
199
+
200
+ // const getRegionLabel = (code: string) => {
201
+ // const region = regionsLookup.find(r => r.r_code === code);
202
+ // return region ? region.label : code;
203
+ // };
204
+
205
+ // Transform regions data for IFRC Table - show all regions including 0 data
206
+ const regionsTableData = useMemo(() => {
207
+ if (!data || !regionsLookup.length) return [];
208
+
209
+ // Create a map of all regions with their counts (0 if no data)
210
+ const allRegions = regionsLookup.reduce((acc, region) => {
211
+ if (region.r_code) {
212
+ acc[region.r_code] = {
213
+ name: region.label,
214
+ count: data.regions[region.r_code] || 0
215
+ };
216
+ }
217
+ return acc;
218
+ }, {} as Record<string, { name: string; count: number }>);
219
+
220
+ // Convert to array and sort by count descending
221
+ return Object.entries(allRegions)
222
+ .sort(([,a], [,b]) => b.count - a.count)
223
+ .map(([_, { name, count }], index) => ({
224
+ id: index + 1,
225
+ name,
226
+ count,
227
+ percentage: data.totalCaptions > 0 ? Math.round((count / data.totalCaptions) * 100) : 0
228
+ }));
229
+ }, [data, regionsLookup]);
230
+
231
+ // Transform types data for IFRC Table
232
+ const typesTableData = useMemo(() => {
233
+ if (!data) return [];
234
+
235
+ return Object.entries(data.types)
236
+ .sort(([,a], [,b]) => b - a)
237
+ .map(([typeKey, count], index) => ({
238
+ id: index + 1,
239
+ name: getTypeLabel(typeKey),
240
+ count,
241
+ percentage: Math.round((count / data.totalCaptions) * 100)
242
+ }));
243
+ }, [data, typesLookup]);
244
+
245
+ // Transform sources data for IFRC Table
246
+ const sourcesTableData = useMemo(() => {
247
+ if (!data) return [];
248
+
249
+ return Object.entries(data.sources)
250
+ .sort(([,a], [,b]) => b - a)
251
+ .map(([sourceKey, count], index) => ({
252
+ id: index + 1,
253
+ name: getSourceLabel(sourceKey),
254
+ count,
255
+ percentage: Math.round((count / data.totalCaptions) * 100)
256
+ }));
257
+ }, [data, sourcesLookup]);
258
+
259
+ // Transform models data for IFRC Table
260
+ const modelsTableData = useMemo(() => {
261
+ if (!data) return [];
262
+
263
+ return Object.entries(data.models)
264
+ .sort(([,a], [,b]) => b.totalScore - a.totalScore)
265
+ .map(([model, stats], index) => ({
266
+ id: index + 1,
267
+ name: model,
268
+ count: stats.count,
269
+ accuracy: stats.avgAccuracy,
270
+ context: stats.avgContext,
271
+ usability: stats.avgUsability,
272
+ totalScore: stats.totalScore
273
+ }));
274
+ }, [data]);
275
+
276
+ // Create columns for regions table
277
+ const regionsColumns = useMemo(() => [
278
+ createStringColumn<RegionData, number>(
279
+ 'name',
280
+ 'Region',
281
+ (item) => item.name,
282
+ ),
283
+ createNumberColumn<RegionData, number>(
284
+ 'count',
285
+ 'Count',
286
+ (item) => item.count,
287
+ ),
288
+ createNumberColumn<RegionData, number>(
289
+ 'percentage',
290
+ '% of Total',
291
+ (item) => item.percentage,
292
+ {
293
+ suffix: '%',
294
+ maximumFractionDigits: 0,
295
+ },
296
+ ),
297
+ ], []);
298
+
299
+ // Create columns for types table
300
+ const typesColumns = useMemo(() => [
301
+ createStringColumn<TypeData, number>(
302
+ 'name',
303
+ 'Type',
304
+ (item) => item.name,
305
+ ),
306
+ createNumberColumn<TypeData, number>(
307
+ 'count',
308
+ 'Count',
309
+ (item) => item.count,
310
+ ),
311
+ createNumberColumn<TypeData, number>(
312
+ 'percentage',
313
+ '% of Total',
314
+ (item) => item.percentage,
315
+ {
316
+ suffix: '%',
317
+ maximumFractionDigits: 0,
318
+ },
319
+ ),
320
+ ], []);
321
+
322
+ // Create columns for sources table
323
+ const sourcesColumns = useMemo(() => [
324
+ createStringColumn<SourceData, number>(
325
+ 'name',
326
+ 'Source',
327
+ (item) => item.name,
328
+ ),
329
+ createNumberColumn<SourceData, number>(
330
+ 'count',
331
+ 'Count',
332
+ (item) => item.count,
333
+ ),
334
+ createNumberColumn<SourceData, number>(
335
+ 'percentage',
336
+ '% of Total',
337
+ (item) => item.percentage,
338
+ {
339
+ suffix: '%',
340
+ maximumFractionDigits: 0,
341
+ },
342
+ ),
343
+ ], []);
344
+
345
+ // Create columns for models table
346
+ const modelsColumns = useMemo(() => [
347
+ createStringColumn<ModelData, number>(
348
+ 'name',
349
+ 'Model',
350
+ (item) => item.name,
351
+ ),
352
+ createNumberColumn<ModelData, number>(
353
+ 'count',
354
+ 'Count',
355
+ (item) => item.count,
356
+ ),
357
+ createNumberColumn<ModelData, number>(
358
+ 'accuracy',
359
+ 'Accuracy',
360
+ (item) => item.accuracy,
361
+ {
362
+ suffix: '%',
363
+ maximumFractionDigits: 0,
364
+ },
365
+ ),
366
+ createNumberColumn<ModelData, number>(
367
+ 'context',
368
+ 'Context',
369
+ (item) => item.context,
370
+ {
371
+ suffix: '%',
372
+ maximumFractionDigits: 0,
373
+ },
374
+ ),
375
+ createNumberColumn<ModelData, number>(
376
+ 'usability',
377
+ 'Usability',
378
+ (item) => item.usability,
379
+ {
380
+ suffix: '%',
381
+ maximumFractionDigits: 0,
382
+ },
383
+ ),
384
+ createNumberColumn<ModelData, number>(
385
+ 'totalScore',
386
+ 'Total Score',
387
+ (item) => item.totalScore,
388
+ {
389
+ suffix: '%',
390
+ maximumFractionDigits: 0,
391
+ },
392
+ ),
393
+ ], []);
394
+
395
+ if (loading) {
396
+ return (
397
+ <PageContainer>
398
+ <div className="flex items-center justify-center min-h-[400px]">
399
+ <Spinner />
400
+ </div>
401
+ </PageContainer>
402
+ );
403
+ }
404
+
405
+ if (!data) {
406
+ return (
407
+ <PageContainer>
408
+ <div className="flex items-center justify-center min-h-[400px]">
409
+ <div className="text-red-500">Failed to load analytics data. Please try again.</div>
410
+ </div>
411
+ </PageContainer>
412
+ );
413
+ }
414
+
415
+ const sourcesChartData = Object.entries(data.sources).filter(([, value]) => value > 0).map(([name, value]) => ({ name, value }));
416
+ const typesChartData = Object.entries(data.types).filter(([, value]) => value > 0).map(([name, value]) => ({ name, value }));
417
+ const regionsChartData = Object.entries(data.regions).filter(([, value]) => value > 0).map(([name, value]) => ({ name, value }));
418
+
419
+ // Official IFRC color palette for all pie charts - same order for all charts
420
+ const ifrcColors = [
421
+ '#F5333F', // IFRC Primary Red (--go-ui-color-red-90)
422
+ '#F64752', // IFRC Red 80
423
+ '#F75C65', // IFRC Red 70
424
+ '#F87079', // IFRC Red 60
425
+ '#F9858C', // IFRC Red 50
426
+ '#FA999F', // IFRC Red 40
427
+ '#FBADB2', // IFRC Red 30
428
+ '#FCC2C5' // IFRC Red 20
429
+ ];
430
+
431
+
432
  return (
433
+ <PageContainer>
434
+ <Container
435
+ heading="Analytics Dashboard"
436
+ headingLevel={2}
437
+ withHeaderBorder
438
+ withInternalPadding
439
+ className="max-w-7xl mx-auto"
440
+ >
441
+ {/* Tab selector */}
442
+ <div className="flex justify-center my-6">
443
+ <SegmentInput
444
+ name="analytics-view"
445
+ value={view}
446
+ onChange={(value) => {
447
+ if (value === 'general' || value === 'vlm') {
448
+ setView(value);
449
+ }
450
+ }}
451
+ options={viewOptions}
452
+ keySelector={(o) => o.key}
453
+ labelSelector={(o) => o.label}
454
+ />
455
+ </div>
456
+
457
+ {view === 'general' ? (
458
+ <div className="space-y-8">
459
+ {/* Summary Statistics */}
460
+ <Container heading="Summary Statistics" headingLevel={3} withHeaderBorder withInternalPadding>
461
+ <div className="grid grid-cols-2 gap-4">
462
+ <KeyFigure
463
+ value={data.totalCaptions}
464
+ label="Total Captions"
465
+ compactValue
466
+ />
467
+ <KeyFigure
468
+ value={2000}
469
+ label="Target Amount"
470
+ compactValue
471
+ />
472
+ </div>
473
+ <div className="mt-6">
474
+ <div className="flex justify-between mb-2">
475
+ <span>Progress towards target</span>
476
+ <span>{Math.round((data.totalCaptions / 2000) * 100)}%</span>
477
+ </div>
478
+ <ProgressBar value={data.totalCaptions} totalValue={2000} />
479
+ </div>
480
+ </Container>
481
+
482
+
483
+ {/* Regions Chart & Data */}
484
+ <Container heading="Regions Distribution" headingLevel={3} withHeaderBorder withInternalPadding>
485
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
486
+ <div className="flex justify-center items-center min-h-[300px]">
487
+ <PieChart
488
+ data={regionsChartData}
489
+ valueSelector={d => d.value}
490
+ labelSelector={d => d.name}
491
+ keySelector={d => d.name}
492
+ colors={ifrcColors}
493
+ showPercentageInLegend
494
+ />
495
+ </div>
496
+ <div className="w-full">
497
+ <Table
498
+ data={regionsTableData}
499
+ columns={regionsColumns}
500
+ keySelector={numericIdSelector}
501
+ filtered={false}
502
+ pending={false}
503
+ />
504
+ </div>
505
+ </div>
506
+ </Container>
507
+
508
+ {/* Sources Chart & Data */}
509
+ <Container heading="Sources Distribution" headingLevel={3} withHeaderBorder withInternalPadding>
510
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
511
+ <div className="flex justify-center items-center min-h-[300px]">
512
+ <PieChart
513
+ data={sourcesChartData}
514
+ valueSelector={d => d.value}
515
+ labelSelector={d => d.name}
516
+ keySelector={d => d.name}
517
+ colors={ifrcColors}
518
+ showPercentageInLegend
519
+ />
520
+ </div>
521
+ <div className="w-full">
522
+ <Table
523
+ data={sourcesTableData}
524
+ columns={sourcesColumns}
525
+ keySelector={numericIdSelector}
526
+ filtered={false}
527
+ pending={false}
528
+ />
529
+ </div>
530
+ </div>
531
+ </Container>
532
+
533
+ {/* Types Chart & Data */}
534
+ <Container heading="Types Distribution" headingLevel={3} withHeaderBorder withInternalPadding>
535
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
536
+ <div className="flex justify-center items-center min-h-[300px]">
537
+ <PieChart
538
+ data={typesChartData}
539
+ valueSelector={d => d.value}
540
+ labelSelector={d => d.name}
541
+ keySelector={d => d.name}
542
+ colors={ifrcColors}
543
+ showPercentageInLegend
544
+ />
545
+ </div>
546
+ <div className="w-full">
547
+ <Table
548
+ data={typesTableData}
549
+ columns={typesColumns}
550
+ keySelector={numericIdSelector}
551
+ filtered={false}
552
+ pending={false}
553
+ />
554
+ </div>
555
+ </div>
556
+ </Container>
557
+ </div>
558
+ ) : (
559
+ <div className="space-y-8">
560
+ {/* Model Performance */}
561
+ <Container heading="Model Performance" headingLevel={3} withHeaderBorder withInternalPadding>
562
+ <Table
563
+ data={modelsTableData}
564
+ columns={modelsColumns}
565
+ keySelector={numericIdSelector}
566
+ filtered={false}
567
+ pending={false}
568
+ />
569
+ </Container>
570
+ </div>
571
+ )}
572
+ </Container>
573
  </PageContainer>
574
  );
575
  }
frontend/src/pages/DemoPage.tsx ADDED
@@ -0,0 +1,1079 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ Radio,
14
+ Switch,
15
+ DateInput,
16
+ NumberInput,
17
+ PasswordInput,
18
+ RawFileInput,
19
+ Container,
20
+ Alert,
21
+ Message,
22
+ Spinner,
23
+ ProgressBar,
24
+ StackedProgressBar,
25
+ KeyFigure,
26
+ PieChart,
27
+ BarChart,
28
+ TimeSeriesChart,
29
+ Table,
30
+ HeaderCell,
31
+ TableRow,
32
+ TableData,
33
+ Tabs,
34
+ Tab,
35
+ TabList,
36
+ TabPanel,
37
+ Chip,
38
+ Tooltip,
39
+ Modal,
40
+ Popup,
41
+ DropdownMenu,
42
+ IconButton,
43
+ ConfirmButton,
44
+ Breadcrumbs,
45
+ List,
46
+ Grid,
47
+ ExpandableContainer,
48
+ BlockLoading,
49
+ InputContainer,
50
+ InputLabel,
51
+ InputHint,
52
+ InputError,
53
+ InputSection,
54
+ BooleanInput,
55
+ BooleanOutput,
56
+ DateOutput,
57
+ DateRangeOutput,
58
+ NumberOutput,
59
+ TextOutput,
60
+ HtmlOutput,
61
+ DismissableTextOutput,
62
+ DismissableListOutput,
63
+ DismissableMultiListOutput,
64
+ Legend,
65
+ LegendItem,
66
+ ChartContainer,
67
+ ChartAxes,
68
+ InfoPopup,
69
+ Footer,
70
+ NavigationTabList,
71
+ Pager,
72
+ RawButton,
73
+ RawInput,
74
+ RawTextArea,
75
+ RawList,
76
+ SegmentInput,
77
+ SelectInputContainer,
78
+ ReducedListDisplay,
79
+ Image,
80
+ TopBanner,
81
+ } from '@ifrc-go/ui';
82
+ import {
83
+ UploadCloudLineIcon,
84
+ ArrowRightLineIcon,
85
+ SearchLineIcon,
86
+ QuestionLineIcon,
87
+ GoMainIcon,
88
+ StarLineIcon,
89
+ DashboardIcon,
90
+ AnalysisIcon,
91
+ FilterLineIcon,
92
+ DropLineIcon,
93
+ CartIcon,
94
+ ChevronDownLineIcon,
95
+ ChevronUpLineIcon,
96
+ CloseLineIcon,
97
+ EditLineIcon,
98
+ DeleteBinLineIcon,
99
+ DownloadLineIcon,
100
+ ShareLineIcon,
101
+ SettingsLineIcon,
102
+ RulerLineIcon,
103
+ MagicLineIcon,
104
+ PantoneLineIcon,
105
+ MarkupLineIcon,
106
+ CalendarLineIcon,
107
+ LockLineIcon,
108
+ LocationIcon,
109
+ HeartLineIcon,
110
+ ThumbUpLineIcon,
111
+ ThumbDownLineIcon,
112
+ EyeLineIcon,
113
+ EyeOffLineIcon,
114
+ CheckLineIcon,
115
+ CropLineIcon,
116
+ AlertLineIcon,
117
+ InfoIcon,
118
+ AlarmWarningLineIcon,
119
+ SliceLineIcon,
120
+ ArrowLeftLineIcon,
121
+ ArrowDownLineIcon,
122
+ ArrowUpLineIcon,
123
+ MenuLineIcon,
124
+ MoreLineIcon,
125
+ RefreshLineIcon,
126
+ PaintLineIcon,
127
+ NotificationIcon,
128
+ HammerLineIcon,
129
+ ShapeLineIcon,
130
+ LinkLineIcon,
131
+ ExternalLinkLineIcon,
132
+ CopyLineIcon,
133
+ } from '@ifrc-go/icons';
134
+
135
+ export default function DemoPage() {
136
+ const [showModal, setShowModal] = useState(false);
137
+ const [showPopup, setShowPopup] = useState(false);
138
+ const [activeTab, setActiveTab] = useState('components');
139
+ const [loading, setLoading] = useState(false);
140
+ const [textValue, setTextValue] = useState('');
141
+ const [selectValue, setSelectValue] = useState('');
142
+ const [multiSelectValue, setMultiSelectValue] = useState<string[]>([]);
143
+ const [checkboxValue, setCheckboxValue] = useState(false);
144
+ const [radioValue, setRadioValue] = useState('option1');
145
+ const [switchValue, setSwitchValue] = useState(false);
146
+ const [dateValue, setDateValue] = useState<string>('');
147
+ const [numberValue, setNumberValue] = useState<number | undefined>();
148
+ const [passwordValue, setPasswordValue] = useState('');
149
+ const [booleanValue, setBooleanValue] = useState(false);
150
+ const [segmentValue, setSegmentValue] = useState('option1');
151
+
152
+ // Dummy data
153
+ const dummyOptions = [
154
+ { key: 'option1', label: 'Option 1' },
155
+ { key: 'option2', label: 'Option 2' },
156
+ { key: 'option3', label: 'Option 3' },
157
+ { key: 'option4', label: 'Option 4' },
158
+ ];
159
+
160
+ const dummyCountries = [
161
+ { c_code: 'US', label: 'United States', r_code: 'NAM' },
162
+ { c_code: 'CA', label: 'Canada', r_code: 'NAM' },
163
+ { c_code: 'MX', label: 'Mexico', r_code: 'NAM' },
164
+ { c_code: 'BR', label: 'Brazil', r_code: 'SAM' },
165
+ { c_code: 'AR', label: 'Argentina', r_code: 'SAM' },
166
+ { c_code: 'UK', label: 'United Kingdom', r_code: 'EUR' },
167
+ { c_code: 'DE', label: 'Germany', r_code: 'EUR' },
168
+ { c_code: 'FR', label: 'France', r_code: 'EUR' },
169
+ ];
170
+
171
+ const dummyTableData = [
172
+ { id: 1, name: 'John Doe', age: 30, country: 'United States', status: 'Active' },
173
+ { id: 2, name: 'Jane Smith', age: 25, country: 'Canada', status: 'Inactive' },
174
+ { id: 3, name: 'Bob Johnson', age: 35, country: 'Mexico', status: 'Active' },
175
+ { id: 4, name: 'Alice Brown', age: 28, country: 'Brazil', status: 'Active' },
176
+ ];
177
+
178
+ const dummyChartData = [
179
+ { name: 'Red Cross', value: 45 },
180
+ { name: 'UNICEF', value: 30 },
181
+ { name: 'WHO', value: 15 },
182
+ { name: 'WFP', value: 10 },
183
+ ];
184
+
185
+ const dummyTimeSeriesData = [
186
+ { date: '2024-01', value: 100 },
187
+ { date: '2024-02', value: 120 },
188
+ { date: '2024-03', value: 110 },
189
+ { date: '2024-04', value: 140 },
190
+ { date: '2024-05', value: 130 },
191
+ { date: '2024-06', value: 160 },
192
+ ];
193
+
194
+ const dummyBarData = [
195
+ { name: 'Q1', value: 100 },
196
+ { name: 'Q2', value: 150 },
197
+ { name: 'Q3', value: 120 },
198
+ { name: 'Q4', value: 180 },
199
+ ];
200
+
201
+ const handleLoading = () => {
202
+ setLoading(true);
203
+ setTimeout(() => setLoading(false), 2000);
204
+ };
205
+
206
+ const handleTextChange = (value: string | undefined, name: string) => {
207
+ setTextValue(value || '');
208
+ };
209
+
210
+ const handlePasswordChange = (value: string | undefined, name: string) => {
211
+ setPasswordValue(value || '');
212
+ };
213
+
214
+ const handleNumberChange = (value: number | undefined, name: string) => {
215
+ setNumberValue(value);
216
+ };
217
+
218
+ const handleDateChange = (value: string | undefined, name: string) => {
219
+ setDateValue(value || '');
220
+ };
221
+
222
+ const handleSelectChange = (value: string | undefined, name: string) => {
223
+ setSelectValue(value || '');
224
+ };
225
+
226
+ const handleMultiSelectChange = (value: string[], name: string) => {
227
+ setMultiSelectValue(value);
228
+ };
229
+
230
+ const handleCheckboxChange = (value: boolean, name: string) => {
231
+ setCheckboxValue(value);
232
+ };
233
+
234
+ const handleRadioChange = (value: string, name: string) => {
235
+ setRadioValue(value);
236
+ };
237
+
238
+ const handleSwitchChange = (value: boolean, name: string) => {
239
+ setSwitchValue(value);
240
+ };
241
+
242
+ const handleBooleanChange = (value: boolean, name: string) => {
243
+ setBooleanValue(value);
244
+ };
245
+
246
+ const handleSegmentChange = (value: string, name: string) => {
247
+ setSegmentValue(value);
248
+ };
249
+
250
+ return (
251
+ <PageContainer>
252
+ <div className="space-y-8">
253
+ {/* Header Section */}
254
+ <Container heading="Navigation & Header Components" headingLevel={2} withHeaderBorder withInternalPadding>
255
+ <div className="space-y-6">
256
+ {/* Navigation Tabs */}
257
+ <div>
258
+ <h3 className="text-lg font-semibold mb-4">Navigation Tab List</h3>
259
+ <NavigationTabList variant="primary">
260
+ <Button name="upload" variant="primary">Upload</Button>
261
+ <Button name="analytics" variant="secondary">Analytics</Button>
262
+ <Button name="explore" variant="secondary">Explore</Button>
263
+ <Button name="help" variant="secondary">Help</Button>
264
+ </NavigationTabList>
265
+ </div>
266
+
267
+ {/* Top Banner */}
268
+ <div>
269
+ <h3 className="text-lg font-semibold mb-4">Top Banner</h3>
270
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
271
+ <div className="flex justify-between items-start">
272
+ <div>
273
+ <h4 className="font-semibold text-blue-900">Important Notice</h4>
274
+ <p className="text-blue-700 mt-1">This is a top banner component for important announcements.</p>
275
+ </div>
276
+ <Button name="dismiss" variant="secondary" size={1}>
277
+ Dismiss
278
+ </Button>
279
+ </div>
280
+ </div>
281
+ </div>
282
+
283
+ {/* Breadcrumbs */}
284
+ <div>
285
+ <h3 className="text-lg font-semibold mb-4">Breadcrumbs</h3>
286
+ <nav className="flex" aria-label="Breadcrumb">
287
+ <ol className="flex items-center space-x-2">
288
+ <li>
289
+ <a href="/" className="text-gray-500 hover:text-gray-700">Home</a>
290
+ </li>
291
+ <li>
292
+ <span className="mx-2 text-gray-400">/</span>
293
+ </li>
294
+ <li>
295
+ <a href="/analytics" className="text-gray-500 hover:text-gray-700">Analytics</a>
296
+ </li>
297
+ <li>
298
+ <span className="mx-2 text-gray-400">/</span>
299
+ </li>
300
+ <li>
301
+ <span className="text-gray-900">Reports</span>
302
+ </li>
303
+ </ol>
304
+ </nav>
305
+ </div>
306
+ </div>
307
+ </Container>
308
+
309
+ {/* Basic Components */}
310
+ <Container heading="Basic Components" headingLevel={2} withHeaderBorder withInternalPadding>
311
+ <div className="space-y-6">
312
+ {/* Buttons */}
313
+ <div>
314
+ <h3 className="text-lg font-semibold mb-4">Buttons</h3>
315
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
316
+ <Button name="primary" variant="primary">Primary Button</Button>
317
+ <Button name="secondary" variant="secondary">Secondary Button</Button>
318
+ <Button name="tertiary" variant="tertiary">Tertiary Button</Button>
319
+ <Button name="disabled" disabled>Disabled Button</Button>
320
+ <Button name="loading" onClick={handleLoading} disabled={loading}>
321
+ {loading ? <Spinner /> : 'Loading Button'}
322
+ </Button>
323
+ <ConfirmButton name="confirm" onConfirm={() => alert('Confirmed!')}>
324
+ Confirm Button
325
+ </ConfirmButton>
326
+ <Button name="with-icon" variant="primary">
327
+ <UploadCloudLineIcon className="w-4 h-4 mr-2" />
328
+ Upload File
329
+ </Button>
330
+ </div>
331
+ </div>
332
+
333
+ {/* Icon Buttons */}
334
+ <div>
335
+ <h3 className="text-lg font-semibold mb-4">Icon Buttons</h3>
336
+ <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
337
+ <IconButton name="upload" variant="primary" title="Upload" ariaLabel="Upload">
338
+ <UploadCloudLineIcon />
339
+ </IconButton>
340
+ <IconButton name="search" variant="secondary" title="Search" ariaLabel="Search">
341
+ <SearchLineIcon />
342
+ </IconButton>
343
+ <IconButton name="edit" variant="tertiary" title="Edit" ariaLabel="Edit">
344
+ <EditLineIcon />
345
+ </IconButton>
346
+ <IconButton name="delete" variant="tertiary" title="Delete" ariaLabel="Delete">
347
+ <DeleteBinLineIcon />
348
+ </IconButton>
349
+ <IconButton name="download" variant="tertiary" title="Download" ariaLabel="Download">
350
+ <DownloadLineIcon />
351
+ </IconButton>
352
+ <IconButton name="share" variant="tertiary" title="Share" ariaLabel="Share">
353
+ <ShareLineIcon />
354
+ </IconButton>
355
+ </div>
356
+ </div>
357
+
358
+ {/* Chips */}
359
+ <div>
360
+ <h3 className="text-lg font-semibold mb-4">Chips</h3>
361
+ <div className="flex flex-wrap gap-2">
362
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
363
+ Primary Chip
364
+ </span>
365
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
366
+ Secondary Chip
367
+ </span>
368
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
369
+ Tertiary Chip
370
+ </span>
371
+ </div>
372
+ </div>
373
+
374
+ {/* Tooltips */}
375
+ <div>
376
+ <h3 className="text-lg font-semibold mb-4">Tooltips</h3>
377
+ <div className="flex gap-4">
378
+ <div className="relative group">
379
+ <Button name="tooltip">Hover me</Button>
380
+ <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">
381
+ This is a tooltip
382
+ </div>
383
+ </div>
384
+ <div className="relative group">
385
+ <IconButton name="tooltip-icon" variant="tertiary" title="Info" ariaLabel="Info">
386
+ <InfoIcon />
387
+ </IconButton>
388
+ <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">
389
+ Another tooltip
390
+ </div>
391
+ </div>
392
+ </div>
393
+ </div>
394
+ </div>
395
+ </Container>
396
+
397
+ {/* Form Elements */}
398
+ <Container heading="Form Elements" headingLevel={2} withHeaderBorder withInternalPadding>
399
+ <div className="space-y-6">
400
+ {/* Text Inputs */}
401
+ <div>
402
+ <h3 className="text-lg font-semibold mb-4">Text Inputs</h3>
403
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
404
+ <InputSection>
405
+ <InputLabel>Text Input</InputLabel>
406
+ <TextInput
407
+ name="text"
408
+ value={textValue}
409
+ onChange={handleTextChange}
410
+ placeholder="Enter text..."
411
+ />
412
+ <InputHint>This is a hint text</InputHint>
413
+ </InputSection>
414
+
415
+ <InputSection>
416
+ <InputLabel>Password Input</InputLabel>
417
+ <PasswordInput
418
+ name="password"
419
+ value={passwordValue}
420
+ onChange={handlePasswordChange}
421
+ placeholder="Enter password..."
422
+ />
423
+ </InputSection>
424
+
425
+ <InputSection>
426
+ <InputLabel>Number Input</InputLabel>
427
+ <NumberInput
428
+ name="number"
429
+ value={numberValue}
430
+ onChange={handleNumberChange}
431
+ placeholder="Enter number..."
432
+ />
433
+ </InputSection>
434
+
435
+ <InputSection>
436
+ <InputLabel>Date Input</InputLabel>
437
+ <DateInput
438
+ name="date"
439
+ value={dateValue}
440
+ onChange={handleDateChange}
441
+ placeholder="Select date..."
442
+ />
443
+ </InputSection>
444
+
445
+ <InputSection>
446
+ <InputLabel>Text Area</InputLabel>
447
+ <TextArea
448
+ name="textarea"
449
+ value=""
450
+ onChange={() => {}}
451
+ placeholder="Enter long text..."
452
+ rows={4}
453
+ />
454
+ </InputSection>
455
+
456
+ <InputSection>
457
+ <InputLabel>File Input</InputLabel>
458
+ <RawFileInput
459
+ name="file"
460
+ accept="image/*"
461
+ onChange={() => {}}
462
+ />
463
+ </InputSection>
464
+ </div>
465
+ </div>
466
+
467
+ {/* Select Inputs */}
468
+ <div>
469
+ <h3 className="text-lg font-semibold mb-4">Select Inputs</h3>
470
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
471
+ <InputSection>
472
+ <InputLabel>Select Input</InputLabel>
473
+ <SelectInput
474
+ name="select"
475
+ value={selectValue}
476
+ onChange={handleSelectChange}
477
+ options={dummyOptions}
478
+ keySelector={(o) => o.key}
479
+ labelSelector={(o) => o.label}
480
+ placeholder="Select an option..."
481
+ />
482
+ </InputSection>
483
+
484
+ <InputSection>
485
+ <InputLabel>Multi Select Input</InputLabel>
486
+ <MultiSelectInput
487
+ name="multiselect"
488
+ value={multiSelectValue}
489
+ onChange={handleMultiSelectChange}
490
+ options={dummyCountries}
491
+ keySelector={(o) => o.c_code}
492
+ labelSelector={(o) => o.label}
493
+ placeholder="Select countries..."
494
+ />
495
+ </InputSection>
496
+
497
+ <InputSection>
498
+ <InputLabel>Search Select Input</InputLabel>
499
+ <SearchSelectInput
500
+ name="searchselect"
501
+ value=""
502
+ onChange={() => {}}
503
+ options={dummyCountries}
504
+ keySelector={(o) => o.c_code}
505
+ labelSelector={(o) => o.label}
506
+ placeholder="Search countries..."
507
+ selectedOnTop
508
+ />
509
+ </InputSection>
510
+
511
+ <InputSection>
512
+ <InputLabel>Search Multi Select Input</InputLabel>
513
+ <SearchMultiSelectInput
514
+ name="searchmultiselect"
515
+ value={[]}
516
+ onChange={() => {}}
517
+ options={dummyCountries}
518
+ keySelector={(o) => o.c_code}
519
+ labelSelector={(o) => o.label}
520
+ placeholder="Search and select countries..."
521
+ selectedOnTop
522
+ />
523
+ </InputSection>
524
+ </div>
525
+ </div>
526
+
527
+ {/* Checkboxes & Radios */}
528
+ <div>
529
+ <h3 className="text-lg font-semibold mb-4">Checkboxes & Radios</h3>
530
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
531
+ <InputSection>
532
+ <InputLabel>Checkbox</InputLabel>
533
+ <Checkbox
534
+ name="checkbox"
535
+ value={checkboxValue}
536
+ onChange={handleCheckboxChange}
537
+ label="Accept terms and conditions"
538
+ />
539
+ </InputSection>
540
+
541
+ <InputSection>
542
+ <InputLabel>Radio Buttons</InputLabel>
543
+ <div className="space-y-2">
544
+ <label className="flex items-center">
545
+ <input
546
+ type="radio"
547
+ name="radio"
548
+ value="option1"
549
+ checked={radioValue === 'option1'}
550
+ onChange={(e) => handleRadioChange(e.target.value, 'radio')}
551
+ className="mr-2"
552
+ />
553
+ <span className="text-sm">Option 1</span>
554
+ </label>
555
+ <label className="flex items-center">
556
+ <input
557
+ type="radio"
558
+ name="radio"
559
+ value="option2"
560
+ checked={radioValue === 'option2'}
561
+ onChange={(e) => handleRadioChange(e.target.value, 'radio')}
562
+ className="mr-2"
563
+ />
564
+ <span className="text-sm">Option 2</span>
565
+ </label>
566
+ <label className="flex items-center">
567
+ <input
568
+ type="radio"
569
+ name="radio"
570
+ value="option3"
571
+ checked={radioValue === 'option3'}
572
+ onChange={(e) => handleRadioChange(e.target.value, 'radio')}
573
+ className="mr-2"
574
+ />
575
+ <span className="text-sm">Option 3</span>
576
+ </label>
577
+ </div>
578
+ </InputSection>
579
+
580
+ <InputSection>
581
+ <InputLabel>Switch</InputLabel>
582
+ <Switch
583
+ name="switch"
584
+ value={switchValue}
585
+ onChange={handleSwitchChange}
586
+ label="Enable notifications"
587
+ />
588
+ </InputSection>
589
+
590
+ <InputSection>
591
+ <InputLabel>Boolean Input</InputLabel>
592
+ <BooleanInput
593
+ name="boolean"
594
+ value={booleanValue}
595
+ onChange={handleBooleanChange}
596
+ label="Enable feature"
597
+ />
598
+ </InputSection>
599
+ </div>
600
+ </div>
601
+
602
+ {/* Segment Input */}
603
+ <div>
604
+ <h3 className="text-lg font-semibold mb-4">Segment Input</h3>
605
+ <InputSection>
606
+ <InputLabel>Segment Input</InputLabel>
607
+ <SegmentInput
608
+ name="segment"
609
+ value={segmentValue}
610
+ onChange={handleSegmentChange}
611
+ options={dummyOptions}
612
+ keySelector={(o) => o.key}
613
+ labelSelector={(o) => o.label}
614
+ />
615
+ </InputSection>
616
+ </div>
617
+ </div>
618
+ </Container>
619
+
620
+ {/* Data Display */}
621
+ <Container heading="Data Display" headingLevel={2} withHeaderBorder withInternalPadding>
622
+ <div className="space-y-6">
623
+ {/* Key Figures */}
624
+ <div>
625
+ <h3 className="text-lg font-semibold mb-4">Key Figures</h3>
626
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
627
+ <KeyFigure value={1234} label="Total Users" />
628
+ <KeyFigure value={567} label="Active Projects" />
629
+ <KeyFigure value={89} label="Countries" />
630
+ <KeyFigure value={12.5} label="Growth Rate" suffix="%" />
631
+ </div>
632
+ </div>
633
+
634
+ {/* Charts */}
635
+ <div>
636
+ <h3 className="text-lg font-semibold mb-4">Charts</h3>
637
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
638
+ <div>
639
+ <h4 className="text-md font-semibold mb-4">Pie Chart</h4>
640
+ <PieChart
641
+ data={dummyChartData}
642
+ valueSelector={(d) => d.value}
643
+ labelSelector={(d) => d.name}
644
+ keySelector={(d) => d.name}
645
+ colorSelector={(d) => '#dc2626'}
646
+ showPercentageInLegend
647
+ />
648
+ </div>
649
+ <div>
650
+ <h4 className="text-md font-semibold mb-4">Bar Chart</h4>
651
+ <BarChart
652
+ data={dummyBarData}
653
+ valueSelector={(d) => d.value}
654
+ labelSelector={(d) => d.name}
655
+ keySelector={(d) => d.name}
656
+ />
657
+ </div>
658
+ </div>
659
+ <div className="mt-8">
660
+ <h4 className="text-md font-semibold mb-4">Time Series Chart</h4>
661
+ <div className="h-64 bg-gray-50 rounded border-2 border-dashed border-gray-400 flex items-center justify-center">
662
+ <div className="text-center">
663
+ <div className="w-16 h-16 mx-auto mb-2 bg-gray-200 rounded flex items-center justify-center">
664
+ <div className="w-8 h-8 bg-gray-400 rounded"></div>
665
+ </div>
666
+ <p className="text-gray-600">Time Series Chart Component</p>
667
+ <p className="text-sm text-gray-500">This would render a time series chart with data points over time</p>
668
+ </div>
669
+ </div>
670
+ </div>
671
+ </div>
672
+
673
+ {/* Tables */}
674
+ <div>
675
+ <h3 className="text-lg font-semibold mb-4">Tables</h3>
676
+ <div className="overflow-x-auto">
677
+ <table className="min-w-full divide-y divide-gray-200">
678
+ <thead className="bg-gray-50">
679
+ <tr>
680
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
681
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Age</th>
682
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Country</th>
683
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
684
+ </tr>
685
+ </thead>
686
+ <tbody className="bg-white divide-y divide-gray-200">
687
+ {dummyTableData.map((row) => (
688
+ <tr key={row.id}>
689
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{row.name}</td>
690
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{row.age}</td>
691
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{row.country}</td>
692
+ <td className="px-6 py-4 whitespace-nowrap">
693
+ <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
694
+ row.status === 'Active'
695
+ ? 'bg-green-100 text-green-800'
696
+ : 'bg-gray-100 text-gray-800'
697
+ }`}>
698
+ {row.status}
699
+ </span>
700
+ </td>
701
+ </tr>
702
+ ))}
703
+ </tbody>
704
+ </table>
705
+ </div>
706
+ </div>
707
+
708
+ {/* Lists */}
709
+ <div>
710
+ <h3 className="text-lg font-semibold mb-4">Lists</h3>
711
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
712
+ <div>
713
+ <h4 className="text-md font-semibold mb-4">Basic List</h4>
714
+ <ul className="space-y-2">
715
+ {dummyCountries.slice(0, 5).map((country) => (
716
+ <li key={country.c_code} className="flex items-center justify-between p-2 bg-gray-50 rounded">
717
+ <span>{country.label}</span>
718
+ <span className="text-sm text-gray-500">{country.c_code}</span>
719
+ </li>
720
+ ))}
721
+ </ul>
722
+ </div>
723
+ <div>
724
+ <h4 className="text-md font-semibold mb-4">Raw List</h4>
725
+ <ul className="space-y-1">
726
+ {dummyCountries.slice(0, 5).map((country) => (
727
+ <li key={country.c_code} className="text-sm">
728
+ {country.label}
729
+ </li>
730
+ ))}
731
+ </ul>
732
+ </div>
733
+ </div>
734
+ </div>
735
+
736
+ {/* Output Components */}
737
+ <div>
738
+ <h3 className="text-lg font-semibold mb-4">Output Components</h3>
739
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
740
+ <div>
741
+ <h4 className="text-md font-semibold mb-4">Text Output</h4>
742
+ <TextOutput value="This is some text output" />
743
+ </div>
744
+ <div>
745
+ <h4 className="text-md font-semibold mb-4">Number Output</h4>
746
+ <NumberOutput value={1234.56} />
747
+ </div>
748
+ <div>
749
+ <h4 className="text-md font-semibold mb-4">Date Output</h4>
750
+ <DateOutput value={new Date()} />
751
+ </div>
752
+ <div>
753
+ <h4 className="text-md font-semibold mb-4">Boolean Output</h4>
754
+ <BooleanOutput value={true} />
755
+ </div>
756
+ </div>
757
+ </div>
758
+ </div>
759
+ </Container>
760
+
761
+ {/* Feedback */}
762
+ <Container heading="Feedback" headingLevel={2} withHeaderBorder withInternalPadding>
763
+ <div className="space-y-6">
764
+ {/* Alerts & Messages */}
765
+ <div>
766
+ <h3 className="text-lg font-semibold mb-4">Alerts & Messages</h3>
767
+ <div className="space-y-4">
768
+ <div className="bg-green-50 border border-green-200 rounded-md p-4">
769
+ <div className="flex">
770
+ <div className="flex-shrink-0">
771
+ <CheckLineIcon className="h-5 w-5 text-green-400" />
772
+ </div>
773
+ <div className="ml-3">
774
+ <p className="text-sm text-green-800">This is a success alert message.</p>
775
+ </div>
776
+ </div>
777
+ </div>
778
+ <div className="bg-yellow-50 border border-yellow-200 rounded-md p-4">
779
+ <div className="flex">
780
+ <div className="flex-shrink-0">
781
+ <AlertLineIcon className="h-5 w-5 text-yellow-400" />
782
+ </div>
783
+ <div className="ml-3">
784
+ <p className="text-sm text-yellow-800">This is a warning alert message.</p>
785
+ </div>
786
+ </div>
787
+ </div>
788
+ <div className="bg-red-50 border border-red-200 rounded-md p-4">
789
+ <div className="flex">
790
+ <div className="flex-shrink-0">
791
+ <AlertLineIcon className="h-5 w-5 text-red-400" />
792
+ </div>
793
+ <div className="ml-3">
794
+ <p className="text-sm text-red-800">This is an error alert message.</p>
795
+ </div>
796
+ </div>
797
+ </div>
798
+ <div className="bg-blue-50 border border-blue-200 rounded-md p-4">
799
+ <div className="flex">
800
+ <div className="flex-shrink-0">
801
+ <InfoIcon className="h-5 w-5 text-blue-400" />
802
+ </div>
803
+ <div className="ml-3">
804
+ <p className="text-sm text-blue-800">This is an info alert message.</p>
805
+ </div>
806
+ </div>
807
+ </div>
808
+ <div className="bg-gray-50 border border-gray-200 rounded-md p-4">
809
+ <div className="flex">
810
+ <div className="flex-shrink-0">
811
+ <InfoIcon className="h-5 w-5 text-gray-400" />
812
+ </div>
813
+ <div className="ml-3">
814
+ <h4 className="text-sm font-medium text-gray-800">Information Message</h4>
815
+ <p className="text-sm text-gray-600 mt-1">This is a message component with a title.</p>
816
+ </div>
817
+ </div>
818
+ </div>
819
+ </div>
820
+ </div>
821
+
822
+ {/* Progress Bars */}
823
+ <div>
824
+ <h3 className="text-lg font-semibold mb-4">Progress Bars</h3>
825
+ <div className="space-y-4">
826
+ <div>
827
+ <h4 className="text-sm font-medium mb-2">Basic Progress Bar</h4>
828
+ <ProgressBar value={75} totalValue={100} />
829
+ </div>
830
+ <div>
831
+ <h4 className="text-sm font-medium mb-2">Stacked Progress Bar</h4>
832
+ <StackedProgressBar
833
+ data={[
834
+ { key: 'completed', value: 60, color: '#dc2626' },
835
+ { key: 'in-progress', value: 25, color: '#f59e0b' },
836
+ { key: 'pending', value: 15, color: '#6b7280' },
837
+ ]}
838
+ valueSelector={(d) => d.value}
839
+ labelSelector={(d) => d.key}
840
+ colorSelector={(d) => d.color}
841
+ />
842
+ </div>
843
+ </div>
844
+ </div>
845
+
846
+ {/* Loading States */}
847
+ <div>
848
+ <h3 className="text-lg font-semibold mb-4">Loading States</h3>
849
+ <div className="space-y-4">
850
+ <div>
851
+ <h4 className="text-sm font-medium mb-2">Spinner</h4>
852
+ <Spinner />
853
+ </div>
854
+ <div>
855
+ <h4 className="text-sm font-medium mb-2">Block Loading</h4>
856
+ <BlockLoading />
857
+ </div>
858
+ </div>
859
+ </div>
860
+
861
+ {/* Modals & Popups */}
862
+ <div>
863
+ <h3 className="text-lg font-semibold mb-4">Modals & Popups</h3>
864
+ <div className="space-y-4">
865
+ <Button name="modal" onClick={() => setShowModal(true)}>
866
+ Open Modal
867
+ </Button>
868
+ <Button name="popup" onClick={() => setShowPopup(true)}>
869
+ Open Popup
870
+ </Button>
871
+ <Button name="info-popup" onClick={() => {}}>
872
+ Info Popup
873
+ </Button>
874
+ </div>
875
+ </div>
876
+
877
+ {/* Dropdown Menu */}
878
+ <div>
879
+ <h3 className="text-lg font-semibold mb-4">Dropdown Menu</h3>
880
+ <div className="relative inline-block text-left">
881
+ <Button name="dropdown">
882
+ Actions <ChevronDownLineIcon className="w-4 h-4 ml-2" />
883
+ </Button>
884
+ <div className="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5">
885
+ <div className="py-1">
886
+ <button className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
887
+ <EditLineIcon className="w-4 h-4 mr-2" />
888
+ Edit
889
+ </button>
890
+ <button className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
891
+ <DeleteBinLineIcon className="w-4 h-4 mr-2" />
892
+ Delete
893
+ </button>
894
+ <button className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
895
+ <DownloadLineIcon className="w-4 h-4 mr-2" />
896
+ Download
897
+ </button>
898
+ </div>
899
+ </div>
900
+ </div>
901
+ </div>
902
+ </div>
903
+ </Container>
904
+
905
+ {/* Layout */}
906
+ <Container heading="Layout" headingLevel={2} withHeaderBorder withInternalPadding>
907
+ <div className="space-y-6">
908
+ {/* Grid System */}
909
+ <div>
910
+ <h3 className="text-lg font-semibold mb-4">Grid System</h3>
911
+ <div className="grid grid-cols-3 gap-4">
912
+ <div className="bg-gray-100 p-4 rounded">Grid Item 1</div>
913
+ <div className="bg-gray-100 p-4 rounded">Grid Item 2</div>
914
+ <div className="bg-gray-100 p-4 rounded">Grid Item 3</div>
915
+ <div className="bg-gray-100 p-4 rounded">Grid Item 4</div>
916
+ <div className="bg-gray-100 p-4 rounded">Grid Item 5</div>
917
+ <div className="bg-gray-100 p-4 rounded">Grid Item 6</div>
918
+ </div>
919
+ </div>
920
+
921
+ {/* Expandable Container */}
922
+ <div>
923
+ <h3 className="text-lg font-semibold mb-4">Expandable Container</h3>
924
+ <div className="border border-gray-200 rounded-lg">
925
+ <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">
926
+ <div className="flex items-center justify-between">
927
+ <span className="font-medium">Expandable Section</span>
928
+ <ChevronDownLineIcon className="w-5 h-5 text-gray-500" />
929
+ </div>
930
+ </button>
931
+ <div className="px-4 py-3 border-t border-gray-200">
932
+ <p>This is the content inside the expandable container. It can contain any components or text.</p>
933
+ <div className="mt-4">
934
+ <Button name="inside-expandable">Button inside expandable</Button>
935
+ </div>
936
+ </div>
937
+ </div>
938
+ </div>
939
+
940
+ {/* Tabs */}
941
+ <div>
942
+ <h3 className="text-lg font-semibold mb-4">Tabs</h3>
943
+ <div className="border-b border-gray-200">
944
+ <nav className="-mb-px flex space-x-8">
945
+ <button className="border-b-2 border-blue-500 py-2 px-1 text-sm font-medium text-blue-600">
946
+ Tab 1
947
+ </button>
948
+ <button className="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 py-2 px-1 text-sm font-medium">
949
+ Tab 2
950
+ </button>
951
+ <button className="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 py-2 px-1 text-sm font-medium">
952
+ Tab 3
953
+ </button>
954
+ </nav>
955
+ <div className="mt-4">
956
+ <p>Content for tab 1</p>
957
+ </div>
958
+ </div>
959
+ </div>
960
+
961
+ {/* Pager */}
962
+ <div>
963
+ <h3 className="text-lg font-semibold mb-4">Pager</h3>
964
+ <div className="flex items-center justify-between">
965
+ <div className="flex-1 flex justify-between sm:hidden">
966
+ <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">
967
+ Previous
968
+ </button>
969
+ <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">
970
+ Next
971
+ </button>
972
+ </div>
973
+ <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
974
+ <div>
975
+ <p className="text-sm text-gray-700">
976
+ Showing <span className="font-medium">1</span> to <span className="font-medium">10</span> of <span className="font-medium">97</span> results
977
+ </p>
978
+ </div>
979
+ <div>
980
+ <nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
981
+ <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">
982
+ Previous
983
+ </button>
984
+ <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">
985
+ 1
986
+ </button>
987
+ <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">
988
+ 2
989
+ </button>
990
+ <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">
991
+ 3
992
+ </button>
993
+ <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">
994
+ Next
995
+ </button>
996
+ </nav>
997
+ </div>
998
+ </div>
999
+ </div>
1000
+ </div>
1001
+ </div>
1002
+ </Container>
1003
+
1004
+ {/* Maps Section */}
1005
+ <Container heading="Maps & Geographic Components" headingLevel={2} withHeaderBorder withInternalPadding>
1006
+ <div className="space-y-6">
1007
+ <div>
1008
+ <h3 className="text-lg font-semibold mb-4">Map Container</h3>
1009
+ <div className="h-64 bg-gray-200 rounded border-2 border-dashed border-gray-400 flex items-center justify-center">
1010
+ <div className="text-center">
1011
+ <LocationIcon className="w-12 h-12 mx-auto text-gray-400 mb-2" />
1012
+ <p className="text-gray-600">Map Container Component</p>
1013
+ <p className="text-sm text-gray-500">This would render a map with MapContainer, MapSource, MapLayer components</p>
1014
+ </div>
1015
+ </div>
1016
+ </div>
1017
+
1018
+ <div>
1019
+ <h3 className="text-lg font-semibold mb-4">Map Popup</h3>
1020
+ <div className="bg-white border rounded-lg p-4 shadow-lg max-w-sm">
1021
+ <div className="flex justify-between items-start mb-2">
1022
+ <h4 className="font-semibold">Country Name</h4>
1023
+ <Button name="close" variant="tertiary" size={1}>
1024
+ <CloseLineIcon />
1025
+ </Button>
1026
+ </div>
1027
+ <p className="text-sm text-gray-600">This represents a MapPopup component with country information.</p>
1028
+ </div>
1029
+ </div>
1030
+ </div>
1031
+ </Container>
1032
+
1033
+ {/* Modals */}
1034
+ {showModal && (
1035
+ <div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
1036
+ <div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
1037
+ <div className="mt-3">
1038
+ <h3 className="text-lg font-medium text-gray-900 mb-4">Modal Example</h3>
1039
+ <p className="text-sm text-gray-500">This is a modal dialog. It can contain any content.</p>
1040
+ <div className="mt-4 flex gap-2">
1041
+ <Button name="modal-close" onClick={() => setShowModal(false)}>
1042
+ Close
1043
+ </Button>
1044
+ <Button name="modal-action" variant="secondary">
1045
+ Action
1046
+ </Button>
1047
+ </div>
1048
+ </div>
1049
+ </div>
1050
+ </div>
1051
+ )}
1052
+
1053
+ {showPopup && (
1054
+ <div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
1055
+ <div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
1056
+ <div className="mt-3">
1057
+ <h3 className="text-lg font-medium text-gray-900 mb-4">Popup Example</h3>
1058
+ <p className="text-sm text-gray-500">This is a popup. It's similar to a modal but with different styling.</p>
1059
+ <div className="mt-4">
1060
+ <Button name="popup-close" onClick={() => setShowPopup(false)}>
1061
+ Close
1062
+ </Button>
1063
+ </div>
1064
+ </div>
1065
+ </div>
1066
+ </div>
1067
+ )}
1068
+
1069
+ {/* Footer */}
1070
+ <Footer>
1071
+ <div className="text-center text-gray-600">
1072
+ <p>IFRC GO UI Components Demo Page</p>
1073
+ <p className="text-sm">Built with IFRC GO Design System</p>
1074
+ </div>
1075
+ </Footer>
1076
+ </div>
1077
+ </PageContainer>
1078
+ );
1079
+ }
frontend/src/pages/DevPage.tsx ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import {
3
+ PageContainer, Heading, Button, Container,
4
+ } from '@ifrc-go/ui';
5
+
6
+ // Local storage key for selected model
7
+ const SELECTED_MODEL_KEY = 'selectedVlmModel';
8
+
9
+ export default function DevPage() {
10
+ // Model selection state
11
+ const [availableModels, setAvailableModels] = useState<Array<{
12
+ m_code: string;
13
+ label: string;
14
+ model_type: string;
15
+ is_available: boolean;
16
+ }>>([]);
17
+ const [selectedModel, setSelectedModel] = useState<string>('');
18
+
19
+ // Fetch models on component mount
20
+ useEffect(() => {
21
+ fetchModels();
22
+ }, []);
23
+
24
+ const fetchModels = () => {
25
+ fetch('/api/models')
26
+ .then(r => r.json())
27
+ .then(modelsData => {
28
+ console.log('Models data:', modelsData);
29
+ console.log('Available models count:', modelsData.models?.length || 0);
30
+ setAvailableModels(modelsData.models || []);
31
+
32
+ // Load persisted model or set default model (first available model)
33
+ const persistedModel = localStorage.getItem(SELECTED_MODEL_KEY);
34
+ if (modelsData.models && modelsData.models.length > 0) {
35
+ if (persistedModel && modelsData.models.find((m: any) => m.m_code === persistedModel && m.is_available)) {
36
+ setSelectedModel(persistedModel);
37
+ } else {
38
+ const firstAvailableModel = modelsData.models.find((m: any) => m.is_available) || modelsData.models[0];
39
+ setSelectedModel(firstAvailableModel.m_code);
40
+ localStorage.setItem(SELECTED_MODEL_KEY, firstAvailableModel.m_code);
41
+ }
42
+ }
43
+ })
44
+ .catch(err => {
45
+ console.error('Failed to fetch models:', err);
46
+ });
47
+ };
48
+
49
+ const toggleModelAvailability = async (modelCode: string, currentStatus: boolean) => {
50
+ try {
51
+ const response = await fetch(`/api/models/${modelCode}/toggle`, {
52
+ method: 'POST',
53
+ headers: {
54
+ 'Content-Type': 'application/json',
55
+ },
56
+ body: JSON.stringify({
57
+ is_available: !currentStatus
58
+ })
59
+ });
60
+
61
+ if (response.ok) {
62
+ // Update local state
63
+ setAvailableModels(prev =>
64
+ prev.map(model =>
65
+ model.m_code === modelCode
66
+ ? { ...model, is_available: !currentStatus }
67
+ : model
68
+ )
69
+ );
70
+ console.log(`Model ${modelCode} availability toggled to ${!currentStatus}`);
71
+ } else {
72
+ const errorData = await response.json();
73
+ console.error('Failed to toggle model availability:', errorData);
74
+ alert(`Failed to toggle model availability: ${errorData.error || 'Unknown error'}`);
75
+ }
76
+ } catch (error) {
77
+ console.error('Error toggling model availability:', error);
78
+ alert('Error toggling model availability');
79
+ }
80
+ };
81
+
82
+ // Handle model selection change
83
+ const handleModelChange = (modelCode: string) => {
84
+ setSelectedModel(modelCode);
85
+ localStorage.setItem(SELECTED_MODEL_KEY, modelCode);
86
+ };
87
+
88
+ return (
89
+ <PageContainer>
90
+ <div className="mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8 py-6 sm:py-10">
91
+ <Heading level={1}>Development & Testing</Heading>
92
+
93
+ <div className="mt-8 space-y-8">
94
+ {/* Model Selection Section */}
95
+ <Container
96
+ heading="VLM Model Selection"
97
+ headingLevel={2}
98
+ withHeaderBorder
99
+ withInternalPadding
100
+ >
101
+ <div className="space-y-4">
102
+ <p className="text-gray-700">
103
+ Select which Vision Language Model to use for caption generation.
104
+ </p>
105
+
106
+ <div className="flex flex-col sm:flex-row items-center gap-4">
107
+ <label className="text-sm font-medium text-gray-700">Model:</label>
108
+ <select
109
+ value={selectedModel}
110
+ onChange={(e) => handleModelChange(e.target.value)}
111
+ className="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ifrcRed focus:border-transparent min-w-[200px]"
112
+ >
113
+ {availableModels
114
+ .filter(model => model.is_available)
115
+ .map(model => (
116
+ <option key={model.m_code} value={model.m_code}>
117
+ {model.label}
118
+ </option>
119
+ ))}
120
+ </select>
121
+ {selectedModel && (
122
+ <span className="text-xs text-green-600 bg-green-50 px-2 py-1 rounded">
123
+ ✓ Active for caption generation
124
+ </span>
125
+ )}
126
+ </div>
127
+
128
+
129
+ </div>
130
+ </Container>
131
+
132
+ {/* Model Information Section */}
133
+ <Container
134
+ heading="Model Information"
135
+ headingLevel={2}
136
+ withHeaderBorder
137
+ withInternalPadding
138
+ >
139
+ <div className="space-y-4">
140
+ <p className="text-gray-700">
141
+ Detailed information about available models and their status. Use the toggle buttons to enable/disable models.
142
+ </p>
143
+
144
+ <div className="overflow-x-auto">
145
+ <table className="min-w-full border border-gray-200">
146
+ <thead className="bg-gray-50">
147
+ <tr>
148
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Code</th>
149
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Label</th>
150
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
151
+ <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Available</th>
152
+ </tr>
153
+ </thead>
154
+ <tbody className="bg-white divide-y divide-gray-200">
155
+ {availableModels.map(model => (
156
+ <tr key={model.m_code}>
157
+ <td className="px-4 py-2 text-sm font-mono">{model.m_code}</td>
158
+ <td className="px-4 py-2 text-sm">{model.label}</td>
159
+ <td className="px-4 py-2 text-sm">{model.model_type}</td>
160
+ <td className="px-4 py-2 text-sm">
161
+ <Button
162
+ name={`toggle-${model.m_code}`}
163
+ variant={model.is_available ? "primary" : "secondary"}
164
+ size={1}
165
+ onClick={() => toggleModelAvailability(model.m_code, model.is_available)}
166
+ >
167
+ {model.is_available ? 'Enabled' : 'Disabled'}
168
+ </Button>
169
+ </td>
170
+ </tr>
171
+ ))}
172
+ </tbody>
173
+ </table>
174
+ </div>
175
+ </div>
176
+ </Container>
177
+
178
+ {/* API Testing Section */}
179
+ <Container
180
+ heading="API Testing"
181
+ headingLevel={2}
182
+ withHeaderBorder
183
+ withInternalPadding
184
+ >
185
+ <div className="space-y-4">
186
+ <p className="text-gray-700">
187
+ Test API endpoints and model functionality.
188
+ </p>
189
+
190
+ <div className="flex flex-wrap gap-4">
191
+ <Button
192
+ name="test-models-api"
193
+ variant="secondary"
194
+ onClick={() => {
195
+ fetch('/api/models')
196
+ .then(r => r.json())
197
+ .then(data => {
198
+ console.log('Models API response:', data);
199
+ alert('Check console for models API response');
200
+ })
201
+ .catch(err => {
202
+ console.error('Models API error:', err);
203
+ alert('Models API error - check console');
204
+ });
205
+ }}
206
+ >
207
+ Test Models API
208
+ </Button>
209
+
210
+ <Button
211
+ name="test-selected-model"
212
+ variant="secondary"
213
+ disabled={!selectedModel}
214
+ onClick={() => {
215
+ if (!selectedModel) return;
216
+ fetch(`/api/models/${selectedModel}/test`)
217
+ .then(r => r.json())
218
+ .then(data => {
219
+ console.log('Model test response:', data);
220
+ alert('Check console for model test response');
221
+ })
222
+ .catch(err => {
223
+ console.error('Model test error:', err);
224
+ alert('Model test error - check console');
225
+ });
226
+ }}
227
+ >
228
+ Test Selected Model
229
+ </Button>
230
+
231
+ <Button
232
+ name="refresh-models"
233
+ variant="secondary"
234
+ onClick={fetchModels}
235
+ >
236
+ Refresh Models
237
+ </Button>
238
+ </div>
239
+ </div>
240
+ </Container>
241
+ </div>
242
+ </div>
243
+ </PageContainer>
244
+ );
245
+ }
frontend/src/pages/ExplorePage.tsx CHANGED
@@ -1,6 +1,7 @@
1
- import { PageContainer, Heading, TextInput, SearchSelectInput, SearchMultiSelectInput } from '@ifrc-go/ui';
2
  import { useState, useEffect, useMemo } from 'react';
3
  import { useNavigate } from 'react-router-dom';
 
4
 
5
  interface MapOut {
6
  image_id: string;
@@ -12,8 +13,10 @@ interface MapOut {
12
  image_type: string;
13
  countries?: {c_code: string, label: string, r_code: string}[];
14
  caption?: {
 
15
  generated: string;
16
  edited?: string;
 
17
  };
18
  }
19
 
@@ -25,14 +28,15 @@ export default function ExplorePage() {
25
  const [catFilter, setCatFilter] = useState('');
26
  const [regionFilter, setRegionFilter] = useState('');
27
  const [countryFilter, setCountryFilter] = useState('');
 
28
  const [sources, setSources] = useState<{s_code: string, label: string}[]>([]);
29
  const [types, setTypes] = useState<{t_code: string, label: string}[]>([]);
30
  const [regions, setRegions] = useState<{r_code: string, label: string}[]>([]);
31
  const [countries, setCountries] = useState<{c_code: string, label: string, r_code: string}[]>([]);
32
- const [isLoading, setIsLoading] = useState(false);
33
 
34
  const fetchMaps = () => {
35
- setIsLoading(true);
36
  // Fetch maps
37
  fetch('/api/images/')
38
  .then(r => {
@@ -65,7 +69,7 @@ export default function ExplorePage() {
65
  setMaps([]);
66
  })
67
  .finally(() => {
68
- setIsLoading(false);
69
  });
70
  };
71
 
@@ -89,11 +93,30 @@ export default function ExplorePage() {
89
 
90
  useEffect(() => {
91
  // Fetch lookup data
 
 
 
92
  Promise.all([
93
- fetch('/api/sources').then(r => r.json()),
94
- fetch('/api/types').then(r => r.json()),
95
- fetch('/api/regions').then(r => r.json()),
96
- fetch('/api/countries').then(r => r.json())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  ]).then(([sourcesData, typesData, regionsData, countriesData]) => {
98
  console.log('Fetched filter data:', {
99
  sources: sourcesData.length,
@@ -129,16 +152,19 @@ export default function ExplorePage() {
129
  console.error('Expected array from /api/countries, got:', countriesData);
130
  setCountries([]);
131
  }
 
 
132
  }).catch(err => {
133
  console.error('Failed to fetch filter data:', err);
 
 
 
 
 
 
134
  });
135
  }, []);
136
 
137
- // Add refresh function
138
- const handleRefresh = () => {
139
- fetchMaps();
140
- };
141
-
142
  const filtered = useMemo(() => {
143
  // Ensure maps is an array before filtering
144
  if (!Array.isArray(maps)) {
@@ -147,12 +173,13 @@ export default function ExplorePage() {
147
  }
148
 
149
  return maps.filter(m => {
150
- // Search in filename, source, type, and caption
151
  const searchLower = search.toLowerCase();
152
  const searchMatch = !search ||
153
  m.file_key.toLowerCase().includes(searchLower) ||
154
  m.source.toLowerCase().includes(searchLower) ||
155
  m.type.toLowerCase().includes(searchLower) ||
 
156
  (m.caption?.edited && m.caption.edited.toLowerCase().includes(searchLower)) ||
157
  (m.caption?.generated && m.caption.generated.toLowerCase().includes(searchLower));
158
 
@@ -168,117 +195,170 @@ export default function ExplorePage() {
168
  // Filter by country (check if any country in the image matches the selected country)
169
  const countryMatch = !countryFilter || (m.countries && m.countries.some(c => c.c_code === countryFilter));
170
 
171
- return searchMatch && sourceMatch && typeMatch && regionMatch && countryMatch;
 
 
 
172
  });
173
  }, [maps, search, srcFilter, catFilter, regionFilter, countryFilter]);
174
 
175
-
176
-
177
  return (
178
  <PageContainer>
179
- <div className="flex justify-between items-center">
180
- <Heading level={2}>Explore Examples</Heading>
181
- <button
182
- onClick={handleRefresh}
183
- disabled={isLoading}
184
- className="px-4 py-2 bg-ifrcRed text-white rounded hover:bg-ifrcRed/90 disabled:opacity-50"
185
- >
186
- {isLoading ? 'Loading...' : 'Refresh'}
187
- </button>
188
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
- {/* ── Filters Bar ──────────────────────────────── */}
191
- <div className="mt-4 flex flex-wrap gap-4 items-center">
192
- <TextInput
193
- name="search"
194
- placeholder="Search by filename…"
195
- value={search}
196
- onChange={(e) => setSearch(e || '')}
197
- className="flex-1 min-w-[12rem]"
198
- />
 
199
 
200
- <SearchSelectInput
201
- name="source"
202
- placeholder="All Sources"
203
- options={sources}
204
- value={srcFilter || null}
205
- onChange={(v) => setSrcFilter(v as string || '')}
206
- keySelector={(o) => o.s_code}
207
- labelSelector={(o) => o.label}
208
- selectedOnTop={false}
209
- />
 
210
 
211
- <SearchMultiSelectInput
212
- name="type"
213
- placeholder="All Types"
214
- options={types}
215
- value={catFilter ? [catFilter] : []}
216
- onChange={(v) => setCatFilter((v as string[])[0] || '')}
217
- keySelector={(o) => o.t_code}
218
- labelSelector={(o) => o.label}
219
- selectedOnTop={false}
220
- />
 
221
 
222
- <SearchSelectInput
223
- name="region"
224
- placeholder="All Regions"
225
- options={regions}
226
- value={regionFilter || null}
227
- onChange={(v) => setRegionFilter(v as string || '')}
228
- keySelector={(o) => o.r_code}
229
- labelSelector={(o) => o.label}
230
- selectedOnTop={false}
231
- />
 
232
 
233
- <SearchSelectInput
234
- name="country"
235
- placeholder="All Countries"
236
- options={countries}
237
- value={countryFilter || null}
238
- onChange={(v) => setCountryFilter(v as string || '')}
239
- keySelector={(o) => o.c_code}
240
- labelSelector={(o) => o.label}
241
- selectedOnTop={false}
242
- />
243
- </div>
 
244
 
245
- {/* ── List ─────────────────────────────────────── */}
246
- <div className="mt-6 space-y-4">
247
- {filtered.map(m => (
248
- <div key={m.image_id} className="border rounded-lg p-4 flex gap-4 cursor-pointer" onClick={() => navigate(`/map/${m.image_id}`)}>
249
- <div className="bg-gray-100 flex items-center justify-center text-gray-400 text-xs overflow-hidden rounded" style={{ width: '120px', height: '80px' }}>
250
- {m.image_url ? (
251
- <img
252
- src={m.image_url}
253
- alt={m.file_key}
254
- className="w-full h-full object-cover"
255
- style={{ imageRendering: 'pixelated' }}
256
- onError={(e) => {
257
- // Fallback to placeholder if image fails to load
258
- const target = e.target as HTMLImageElement;
259
- target.style.display = 'none';
260
- target.parentElement!.innerHTML = 'Img';
261
- }}
262
- />
263
- ) : (
264
- 'Img'
265
- )}
266
- </div>
267
- <div className="flex-1 min-w-0">
268
- <div className="flex flex-wrap gap-2">
269
- <span className="px-2 py-1 bg-ifrcRed/10 text-ifrcRed text-xs rounded">{m.source}</span>
270
- <span className="px-2 py-1 bg-ifrcRed/10 text-ifrcRed text-xs rounded">{m.type}</span>
271
- </div>
272
- <p className="mt-2 text-sm text-gray-700 line-clamp-2">
273
- {m.caption?.edited || m.caption?.generated || '— no caption yet —'}
274
- </p>
275
- </div>
276
  </div>
277
- ))}
278
 
279
- {!filtered.length && (
280
- <p className="text-center text-gray-500">No examples found.</p>
281
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  </div>
283
  </PageContainer>
284
  );
 
1
+ import { PageContainer, Heading, TextInput, SelectInput, MultiSelectInput, Button } from '@ifrc-go/ui';
2
  import { useState, useEffect, useMemo } from 'react';
3
  import { useNavigate } from 'react-router-dom';
4
+ import { StarLineIcon } from '@ifrc-go/icons';
5
 
6
  interface MapOut {
7
  image_id: string;
 
13
  image_type: string;
14
  countries?: {c_code: string, label: string, r_code: string}[];
15
  caption?: {
16
+ title: string;
17
  generated: string;
18
  edited?: string;
19
+ starred?: boolean;
20
  };
21
  }
22
 
 
28
  const [catFilter, setCatFilter] = useState('');
29
  const [regionFilter, setRegionFilter] = useState('');
30
  const [countryFilter, setCountryFilter] = useState('');
31
+ const [showStarredOnly, setShowStarredOnly] = useState(false);
32
  const [sources, setSources] = useState<{s_code: string, label: string}[]>([]);
33
  const [types, setTypes] = useState<{t_code: string, label: string}[]>([]);
34
  const [regions, setRegions] = useState<{r_code: string, label: string}[]>([]);
35
  const [countries, setCountries] = useState<{c_code: string, label: string, r_code: string}[]>([]);
36
+ const [isLoadingFilters, setIsLoadingFilters] = useState(true);
37
 
38
  const fetchMaps = () => {
39
+ setIsLoadingFilters(true);
40
  // Fetch maps
41
  fetch('/api/images/')
42
  .then(r => {
 
69
  setMaps([]);
70
  })
71
  .finally(() => {
72
+ setIsLoadingFilters(false);
73
  });
74
  };
75
 
 
93
 
94
  useEffect(() => {
95
  // Fetch lookup data
96
+ console.log('Fetching filter data...');
97
+ setIsLoadingFilters(true);
98
+
99
  Promise.all([
100
+ fetch('/api/sources').then(r => {
101
+ console.log('Sources response:', r.status, r.statusText);
102
+ if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
103
+ return r.json();
104
+ }),
105
+ fetch('/api/types').then(r => {
106
+ console.log('Types response:', r.status, r.statusText);
107
+ if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
108
+ return r.json();
109
+ }),
110
+ fetch('/api/regions').then(r => {
111
+ console.log('Regions response:', r.status, r.statusText);
112
+ if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
113
+ return r.json();
114
+ }),
115
+ fetch('/api/countries').then(r => {
116
+ console.log('Countries response:', r.status, r.statusText);
117
+ if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
118
+ return r.json();
119
+ })
120
  ]).then(([sourcesData, typesData, regionsData, countriesData]) => {
121
  console.log('Fetched filter data:', {
122
  sources: sourcesData.length,
 
152
  console.error('Expected array from /api/countries, got:', countriesData);
153
  setCountries([]);
154
  }
155
+
156
+ setIsLoadingFilters(false);
157
  }).catch(err => {
158
  console.error('Failed to fetch filter data:', err);
159
+ // Set empty arrays on error to prevent undefined issues
160
+ setSources([]);
161
+ setTypes([]);
162
+ setRegions([]);
163
+ setCountries([]);
164
+ setIsLoadingFilters(false);
165
  });
166
  }, []);
167
 
 
 
 
 
 
168
  const filtered = useMemo(() => {
169
  // Ensure maps is an array before filtering
170
  if (!Array.isArray(maps)) {
 
173
  }
174
 
175
  return maps.filter(m => {
176
+ // Search in filename, source, type, title, and caption
177
  const searchLower = search.toLowerCase();
178
  const searchMatch = !search ||
179
  m.file_key.toLowerCase().includes(searchLower) ||
180
  m.source.toLowerCase().includes(searchLower) ||
181
  m.type.toLowerCase().includes(searchLower) ||
182
+ (m.caption?.title && m.caption.title.toLowerCase().includes(searchLower)) ||
183
  (m.caption?.edited && m.caption.edited.toLowerCase().includes(searchLower)) ||
184
  (m.caption?.generated && m.caption.generated.toLowerCase().includes(searchLower));
185
 
 
195
  // Filter by country (check if any country in the image matches the selected country)
196
  const countryMatch = !countryFilter || (m.countries && m.countries.some(c => c.c_code === countryFilter));
197
 
198
+ // Filter by starred status
199
+ const starredMatch = !showStarredOnly || (m.caption && m.caption.starred === true);
200
+
201
+ return searchMatch && sourceMatch && typeMatch && regionMatch && countryMatch && starredMatch;
202
  });
203
  }, [maps, search, srcFilter, catFilter, regionFilter, countryFilter]);
204
 
 
 
205
  return (
206
  <PageContainer>
207
+ <div className="space-y-6">
208
+ {/* Header Section */}
209
+ <div className="flex justify-between items-center">
210
+ <div>
211
+ <Heading level={2}>Explore Examples</Heading>
212
+ <p className="text-gray-600 mt-1">Browse and search through uploaded crisis maps</p>
213
+ </div>
214
+ <div className="flex gap-2">
215
+ <Button
216
+ name="reference-examples"
217
+ variant={showStarredOnly ? "primary" : "secondary"}
218
+ onClick={() => setShowStarredOnly(!showStarredOnly)}
219
+ >
220
+ <StarLineIcon className="w-4 h-4" />
221
+ <span className="inline ml-2">Reference Examples</span>
222
+ </Button>
223
+ <Button
224
+ name="export"
225
+ variant="secondary"
226
+ onClick={() => {
227
+ const data = {
228
+ maps: maps,
229
+ filters: {
230
+ sources: sources,
231
+ types: types,
232
+ regions: regions,
233
+ countries: countries
234
+ },
235
+ timestamp: new Date().toISOString()
236
+ };
237
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
238
+ const url = URL.createObjectURL(blob);
239
+ const a = document.createElement('a');
240
+ a.href = url;
241
+ a.download = `promptaid-vision-data-${new Date().toISOString().split('T')[0]}.json`;
242
+ document.body.appendChild(a);
243
+ a.click();
244
+ document.body.removeChild(a);
245
+ URL.revokeObjectURL(url);
246
+ }}
247
+ >
248
+ Export
249
+ </Button>
250
+ </div>
251
+ </div>
252
 
253
+ {/* Filters Bar */}
254
+ <div className="bg-gray-50 rounded-lg p-4">
255
+ <div className="flex flex-wrap gap-4 items-center">
256
+ <TextInput
257
+ name="search"
258
+ placeholder="Search by filename, title…"
259
+ value={search}
260
+ onChange={(e) => setSearch(e || '')}
261
+ className="flex-1 min-w-[12rem]"
262
+ />
263
 
264
+ <SelectInput
265
+ name="source"
266
+ placeholder={isLoadingFilters ? "Loading..." : "All Sources"}
267
+ options={sources}
268
+ value={srcFilter || null}
269
+ onChange={(v) => setSrcFilter(v as string || '')}
270
+ keySelector={(o) => o.s_code}
271
+ labelSelector={(o) => o.label}
272
+ required={false}
273
+ disabled={isLoadingFilters}
274
+ />
275
 
276
+ <SelectInput
277
+ name="type"
278
+ placeholder={isLoadingFilters ? "Loading..." : "All Types"}
279
+ options={types}
280
+ value={catFilter || null}
281
+ onChange={(v) => setCatFilter(v as string || '')}
282
+ keySelector={(o) => o.t_code}
283
+ labelSelector={(o) => o.label}
284
+ required={false}
285
+ disabled={isLoadingFilters}
286
+ />
287
 
288
+ <SelectInput
289
+ name="region"
290
+ placeholder={isLoadingFilters ? "Loading..." : "All Regions"}
291
+ options={regions}
292
+ value={regionFilter || null}
293
+ onChange={(v) => setRegionFilter(v as string || '')}
294
+ keySelector={(o) => o.r_code}
295
+ labelSelector={(o) => o.label}
296
+ required={false}
297
+ disabled={isLoadingFilters}
298
+ />
299
 
300
+ <MultiSelectInput
301
+ name="country"
302
+ placeholder={isLoadingFilters ? "Loading..." : "All Countries"}
303
+ options={countries}
304
+ value={countryFilter ? [countryFilter] : []}
305
+ onChange={(v) => setCountryFilter((v as string[])[0] || '')}
306
+ keySelector={(o) => o.c_code}
307
+ labelSelector={(o) => o.label}
308
+ disabled={isLoadingFilters}
309
+ />
310
+ </div>
311
+ </div>
312
 
313
+ {/* Results Section */}
314
+ <div className="space-y-4">
315
+ <div className="flex justify-between items-center">
316
+ <p className="text-sm text-gray-600">
317
+ {filtered.length} of {maps.length} examples
318
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  </div>
 
320
 
321
+ {/* List */}
322
+ <div className="space-y-4">
323
+ {filtered.map(m => (
324
+ <div key={m.image_id} className="border border-gray-200 rounded-lg p-4 flex gap-4 cursor-pointer hover:bg-gray-50 transition-colors" onClick={() => navigate(`/map/${m.image_id}`)}>
325
+ <div className="bg-gray-100 flex items-center justify-center text-gray-400 text-xs overflow-hidden rounded" style={{ width: '120px', height: '80px' }}>
326
+ {m.image_url ? (
327
+ <img
328
+ src={m.image_url}
329
+ alt={m.file_key}
330
+ className="w-full h-full object-cover"
331
+ style={{ imageRendering: 'pixelated' }}
332
+ onError={(e) => {
333
+ // Fallback to placeholder if image fails to load
334
+ const target = e.target as HTMLImageElement;
335
+ target.style.display = 'none';
336
+ target.parentElement!.innerHTML = 'Img';
337
+ }}
338
+ />
339
+ ) : (
340
+ 'Img'
341
+ )}
342
+ </div>
343
+ <div className="flex-1 min-w-0">
344
+ <h3 className="font-medium text-gray-900 mb-2">
345
+ {m.caption?.title || 'No title'}
346
+ </h3>
347
+ <div className="flex flex-wrap gap-2 mb-2">
348
+ <span className="px-2 py-1 bg-ifrcRed/10 text-ifrcRed text-xs rounded">{m.source}</span>
349
+ <span className="px-2 py-1 bg-ifrcRed text-xs rounded">{m.type}</span>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ ))}
354
+
355
+ {!filtered.length && (
356
+ <div className="text-center py-12">
357
+ <p className="text-gray-500">No examples found.</p>
358
+ </div>
359
+ )}
360
+ </div>
361
+ </div>
362
  </div>
363
  </PageContainer>
364
  );
frontend/src/pages/MapDetailPage.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { PageContainer, Heading, Button } from '@ifrc-go/ui';
2
  import { useState, useEffect } from 'react';
3
  import { useParams, useNavigate } from 'react-router-dom';
4
 
@@ -11,6 +11,7 @@ interface MapOut {
11
  epsg: string;
12
  image_type: string;
13
  caption?: {
 
14
  generated: string;
15
  edited?: string;
16
  };
@@ -111,13 +112,14 @@ export default function MapDetailPage() {
111
  return (
112
  <PageContainer>
113
  <div className="mb-4">
114
- <button
 
 
115
  onClick={() => navigate('/explore')}
116
- className="text-ifrcRed hover:text-ifrcRed/80 mb-4 flex items-center gap-2"
117
  >
118
  ← Back to Explore
119
- </button>
120
- <Heading level={2}>Map Details</Heading>
121
  </div>
122
 
123
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
@@ -142,10 +144,11 @@ export default function MapDetailPage() {
142
  {/* Details Section */}
143
  <div className="space-y-6">
144
  <div>
145
- <h3 className="text-lg font-semibold mb-2">File Information</h3>
146
  <div className="space-y-2 text-sm">
147
- <div><span className="font-medium">File:</span> {map.file_key}</div>
148
- <div><span className="font-medium">ID:</span> {map.image_id}</div>
 
149
  </div>
150
  </div>
151
 
@@ -179,20 +182,15 @@ export default function MapDetailPage() {
179
  </div>
180
 
181
  {/* Contribute Section */}
182
- <div className="mt-8 pt-6 border-t border-gray-200">
183
- <div className="text-center">
184
- <p className="text-gray-600 mb-4">
185
- Want to contribute to this map? Use this image as a starting point for your own analysis.
186
- </p>
187
- <Button
188
- name="contribute"
189
- onClick={handleContribute}
190
- disabled={contributing}
191
- className="bg-ifrcRed hover:bg-ifrcRed/90 text-white px-6 py-2 rounded-lg"
192
- >
193
- {contributing ? 'Contributing...' : 'Contribute'}
194
- </Button>
195
- </div>
196
  </div>
197
  </PageContainer>
198
  );
 
1
+ import { PageContainer, Button } from '@ifrc-go/ui';
2
  import { useState, useEffect } from 'react';
3
  import { useParams, useNavigate } from 'react-router-dom';
4
 
 
11
  epsg: string;
12
  image_type: string;
13
  caption?: {
14
+ title: string;
15
  generated: string;
16
  edited?: string;
17
  };
 
112
  return (
113
  <PageContainer>
114
  <div className="mb-4">
115
+ <Button
116
+ name="back"
117
+ variant="secondary"
118
  onClick={() => navigate('/explore')}
119
+ className="mb-4"
120
  >
121
  ← Back to Explore
122
+ </Button>
 
123
  </div>
124
 
125
  <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
 
144
  {/* Details Section */}
145
  <div className="space-y-6">
146
  <div>
147
+ <h3 className="text-lg font-semibold mb-2">Title</h3>
148
  <div className="space-y-2 text-sm">
149
+ <div className="text-gray-700">
150
+ {map.caption?.title || '— no title —'}
151
+ </div>
152
  </div>
153
  </div>
154
 
 
182
  </div>
183
 
184
  {/* Contribute Section */}
185
+ <div className="mt-8 pt-6 border-t border-gray-200 flex justify-center">
186
+ <Button
187
+ name="contribute"
188
+ onClick={handleContribute}
189
+ disabled={contributing}
190
+ className="bg-ifrcRed hover:bg-ifrcRed/90 text-white px-6 py-2 rounded-lg"
191
+ >
192
+ {contributing ? 'Contributing...' : 'Contribute'}
193
+ </Button>
 
 
 
 
 
194
  </div>
195
  </PageContainer>
196
  );
frontend/src/pages/UploadPage.tsx CHANGED
@@ -2,32 +2,33 @@ import { useCallback, useState, useEffect, useRef } from 'react';
2
  import type { DragEvent } from 'react';
3
  import {
4
  PageContainer, Heading, Button,
5
- SelectInput, MultiSelectInput,
6
- RawFileInput,
7
  } from '@ifrc-go/ui';
8
  import {
9
  UploadCloudLineIcon,
10
  ArrowRightLineIcon,
 
11
  } from '@ifrc-go/icons';
12
- import { Link, useNavigate, useSearchParams } from 'react-router-dom';
 
 
13
 
14
  export default function UploadPage() {
15
- const navigate = useNavigate();
16
  const [searchParams] = useSearchParams();
17
  const [step, setStep] = useState<1 | 2 | 3>(1);
 
18
  const stepRef = useRef(step);
19
  const uploadedImageIdRef = useRef<string | null>(null);
20
  const [preview, setPreview] = useState<string | null>(null);
21
  /* ---------------- local state ----------------- */
22
 
23
  const [file, setFile] = useState<File | null>(null);
24
- //const [source, setSource] = useState('');
25
- //const [type, setType] = useState('');
26
  const [source, setSource] = useState('');
27
  const [type, setType] = useState('');
28
  const [epsg, setEpsg] = useState('');
29
  const [imageType, setImageType] = useState('');
30
  const [countries, setCountries] = useState<string[]>([]);
 
31
 
32
  // Metadata options from database
33
  const [sources, setSources] = useState<{s_code: string, label: string}[]>([]);
@@ -38,7 +39,6 @@ export default function UploadPage() {
38
 
39
  // Track uploaded image data for potential deletion
40
  const [uploadedImageId, setUploadedImageId] = useState<string | null>(null);
41
- const [uploadedCaptionId, setUploadedCaptionId] = useState<string | null>(null);
42
 
43
  // Keep refs updated with current values
44
  stepRef.current = step;
@@ -58,7 +58,8 @@ export default function UploadPage() {
58
  fetch('/api/types').then(r => r.json()),
59
  fetch('/api/spatial-references').then(r => r.json()),
60
  fetch('/api/image-types').then(r => r.json()),
61
- fetch('/api/countries').then(r => r.json())
 
62
  ]).then(([sourcesData, typesData, spatialData, imageTypesData, countriesData]) => {
63
  setSources(sourcesData);
64
  setTypes(typesData);
@@ -66,7 +67,7 @@ export default function UploadPage() {
66
  setImageTypes(imageTypesData);
67
  setCountriesOptions(countriesData);
68
 
69
- // Set default values from the first available options
70
  if (sourcesData.length > 0) setSource(sourcesData[0].s_code);
71
  if (typesData.length > 0) setType(typesData[0].t_code);
72
  if (spatialData.length > 0) setEpsg(spatialData[0].epsg);
@@ -74,11 +75,8 @@ export default function UploadPage() {
74
  });
75
  }, []);
76
 
77
- // Cleanup effect for navigation away - delete if user hasn't submitted yet
78
  useEffect(() => {
79
  const handleBeforeUnload = () => {
80
- // Delete if user is in step 1 or 2 (hasn't submitted yet)
81
- // Only preserve if user has successfully submitted (step 3)
82
  if (uploadedImageIdRef.current && stepRef.current !== 3) {
83
  fetch(`/api/images/${uploadedImageIdRef.current}`, { method: "DELETE" }).catch(console.error);
84
  }
@@ -87,12 +85,11 @@ export default function UploadPage() {
87
  window.addEventListener('beforeunload', handleBeforeUnload);
88
  return () => {
89
  window.removeEventListener('beforeunload', handleBeforeUnload);
90
- // Only cleanup on component unmount if user hasn't submitted yet
91
  if (uploadedImageIdRef.current && stepRef.current !== 3) {
92
  fetch(`/api/images/${uploadedImageIdRef.current}`, { method: "DELETE" }).catch(console.error);
93
  }
94
  };
95
- }, []); // No dependencies - handler will always use current ref values
96
 
97
  const [captionId, setCaptionId] = useState<string | null>(null);
98
  const [imageUrl, setImageUrl] = useState<string|null>(null);
@@ -114,7 +111,6 @@ export default function UploadPage() {
114
  setEpsg(mapData.epsg);
115
  setImageType(mapData.image_type);
116
 
117
- // Generate caption for the existing map
118
  return fetch(`/api/images/${mapId}/caption`, {
119
  method: 'POST',
120
  headers: {
@@ -122,7 +118,10 @@ export default function UploadPage() {
122
  },
123
  body: new URLSearchParams({
124
  title: 'Generated Caption',
125
- prompt: 'Describe this crisis map in detail'
 
 
 
126
  })
127
  });
128
  })
@@ -139,19 +138,6 @@ export default function UploadPage() {
139
  }
140
  }, [searchParams]);
141
 
142
- // Handle navigation with confirmation
143
- const handleNavigation = () => {
144
- if (step === 2) {
145
- if (confirm("Changes will not be saved. Do you want to delete the uploaded image?")) {
146
- // Delete the uploaded image if user confirms
147
- if (uploadedImageId) {
148
- fetch(`/api/images/${uploadedImageId}`, { method: "DELETE" }).catch(console.error);
149
- }
150
- resetToStep1();
151
- }
152
- }
153
- };
154
-
155
  const resetToStep1 = () => {
156
  setStep(1);
157
  setFile(null);
@@ -159,9 +145,9 @@ export default function UploadPage() {
159
  setImageUrl(null);
160
  setCaptionId(null);
161
  setDraft('');
 
162
  setScores({ accuracy: 50, context: 50, usability: 50 });
163
  setUploadedImageId(null);
164
- setUploadedCaptionId(null);
165
  };
166
  const [scores, setScores] = useState({
167
  accuracy: 50,
@@ -169,6 +155,8 @@ export default function UploadPage() {
169
  usability: 50,
170
  });
171
 
 
 
172
  /* ---- drag-and-drop + file-picker handlers -------------------------- */
173
  const onDrop = useCallback((e: DragEvent<HTMLDivElement>) => {
174
  e.preventDefault();
@@ -211,6 +199,8 @@ export default function UploadPage() {
211
  async function handleGenerate() {
212
  if (!file) return;
213
 
 
 
214
  const fd = new FormData();
215
  fd.append('file', file);
216
  fd.append('source', source);
@@ -219,6 +209,11 @@ export default function UploadPage() {
219
  fd.append('image_type', imageType);
220
  countries.forEach((c) => fd.append('countries', c));
221
 
 
 
 
 
 
222
  try {
223
  /* 1) upload */
224
  const mapRes = await fetch('/api/images/', { method: 'POST', body: fd });
@@ -230,7 +225,7 @@ export default function UploadPage() {
230
  if (!mapIdVal) throw new Error('Upload failed: image_id not found');
231
  setUploadedImageId(mapIdVal);
232
 
233
- /* 2) caption */
234
  const capRes = await fetch(
235
  `/api/images/${mapIdVal}/caption`,
236
  {
@@ -239,22 +234,39 @@ export default function UploadPage() {
239
  'Content-Type': 'application/x-www-form-urlencoded',
240
  },
241
  body: new URLSearchParams({
242
- title: 'Generated Caption',
243
- prompt: 'Describe this crisis map in detail'
 
244
  })
245
  },
246
  );
247
  const capJson = await readJsonSafely(capRes);
248
  if (!capRes.ok) throw new Error(capJson.error || 'Caption failed');
249
  setCaptionId(capJson.cap_id);
250
- setUploadedCaptionId(capJson.cap_id);
251
  console.log(capJson);
252
 
253
- /* 3) continue workflow */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  setDraft(capJson.generated);
255
  setStep(2);
256
  } catch (err) {
257
  handleApiError(err, 'Upload');
 
 
258
  }
259
  }
260
 
@@ -281,6 +293,7 @@ export default function UploadPage() {
281
 
282
  // 2. Update caption
283
  const captionBody = {
 
284
  edited: draft || '', // Use draft if available, otherwise empty string
285
  accuracy: scores.accuracy,
286
  context: scores.context,
@@ -296,7 +309,6 @@ export default function UploadPage() {
296
 
297
  // Clear uploaded IDs since submission was successful
298
  setUploadedImageId(null);
299
- setUploadedCaptionId(null);
300
  setStep(3);
301
  } catch (err) {
302
  handleApiError(err, 'Submit');
@@ -330,70 +342,94 @@ export default function UploadPage() {
330
  /* ------------------------------------------------------------------- */
331
  return (
332
  <PageContainer>
333
- <div
334
- className="mx-auto max-w-screen-lg text-center px-2 sm:px-4 py-6 sm:py-10 overflow-x-hidden"
335
- data-step={step}
336
- >
337
- {/* Title & intro copy */}
338
- {step === 1 && <>
339
- <Heading level={2}>Upload Your Crisis Map</Heading>
340
- <p className="mt-3 text-gray-700 leading-relaxed">
341
- This app evaluates how well multimodal AI models turn emergency maps
342
- into meaningful text. Upload your map, let the AI generate a
343
- description, then review and rate the result based on your expertise.
344
- </p>
345
- {/* “More »” link */}
346
- <div className="mt-2 flex justify-center">
347
- <Link
348
- to="/help"
349
- className="text-ifrcRed text-xs hover:underline flex items-center gap-1"
350
- >
351
- More <ArrowRightLineIcon className="w-3 h-3" />
352
- </Link>
353
- </div>
354
- </>}
355
-
356
-
357
-
358
  {/* Drop-zone */}
359
  {step === 1 && (
360
- <div
361
- className="mt-6 sm:mt-10 border-2 border-dashed border-gray-300 bg-gray-50 rounded-xl py-12 px-8 flex flex-col items-center gap-6 hover:bg-gray-100 transition-colors max-w-md mx-auto min-h-[300px] justify-center"
362
- onDragOver={(e) => e.preventDefault()}
363
- onDrop={onDrop}
 
 
364
  >
365
- <UploadCloudLineIcon className="w-10 h-10 text-ifrcRed" />
366
-
367
- {file ? (
368
- <p className="text-sm font-medium text-gray-800">
369
- Selected file: {file.name}
370
  </p>
371
- ) : (
372
- <p className="text-sm text-gray-600">Drag &amp; Drop a file here</p>
373
- )}
374
-
375
- {/* File-picker button - always visible */}
376
- <label className="inline-block cursor-pointer">
377
- <input
378
- type="file"
379
- accept="image/*"
380
- className="sr-only"
381
- onChange={e => onFileChange(e.target.files?.[0], "file")}
382
- />
383
- <Button
384
- name="upload"
385
- size={1}
386
- onClick={() => (document.querySelector('input[type="file"]') as HTMLInputElement)?.click()}
 
387
  >
388
- {file ? 'Change File' : 'Upload'}
389
- </Button>
390
- </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  </div>
392
  )}
393
 
394
  {/* Generate button */}
395
- {step === 1 && (
396
- <div className="flex justify-center mt-12">
397
  <Button
398
  name="generate"
399
  disabled={!file}
@@ -404,143 +440,181 @@ export default function UploadPage() {
404
  </div>
405
  )}
406
 
407
- {step === 2 && imageUrl && (
408
- <div className="mt-6 flex justify-center">
409
- <div className="w-full max-w-screen-lg max-h-80 overflow-hidden bg-red-50">
410
- <img
411
- src={preview || undefined}
412
- alt="Uploaded map preview"
413
- className="w-full h-full object-contain rounded shadow"
414
- />
415
- </div>
416
- </div>
417
- )}
418
-
419
- {step === 2 && (
420
- <div className="space-y-10">
421
- {/* ────── METADATA FORM ────── */}
422
- <div className="grid gap-4 text-left grid-cols-1 lg:grid-cols-2">
423
- <SelectInput
424
- label="Source"
425
- name="source"
426
- value={source}
427
- onChange={handleSourceChange}
428
- options={sources}
429
- keySelector={(o) => o.s_code}
430
- labelSelector={(o) => o.label}
431
- required
432
- />
433
- <SelectInput
434
- label="Type"
435
- name="type"
436
- value={type}
437
- onChange={handleTypeChange}
438
- options={types}
439
- keySelector={(o) => o.t_code}
440
- labelSelector={(o) => o.label}
441
- required
442
- />
443
- <SelectInput
444
- label="EPSG"
445
- name="epsg"
446
- value={epsg}
447
- onChange={handleEpsgChange}
448
- options={spatialReferences}
449
- keySelector={(o) => o.epsg}
450
- labelSelector={(o) => `${o.srid} (EPSG:${o.epsg})`}
451
- required
452
- />
453
- <SelectInput
454
- label="Image Type"
455
- name="image_type"
456
- value={imageType}
457
- onChange={handleImageTypeChange}
458
- options={imageTypes}
459
- keySelector={(o) => o.image_type}
460
- labelSelector={(o) => o.label}
461
- required
462
- />
463
- <MultiSelectInput
464
- label="Countries (optional)"
465
- name="countries"
466
- value={countries}
467
- onChange={handleCountriesChange}
468
- options={countriesOptions}
469
- keySelector={(o) => o.c_code}
470
- labelSelector={(o) => o.label}
471
- placeholder="Select one or more"
472
- />
473
- </div>
474
-
475
- {/* ────── RATING SLIDERS ────── */}
476
- <div className="text-left">
477
- <Heading level={3}>How well did the AI perform on the task?</Heading>
478
- {(['accuracy', 'context', 'usability'] as const).map((k) => (
479
- <div key={k} className="mt-6 flex items-center gap-2 sm:gap-4">
480
- <label className="block text-sm font-medium capitalize w-20 sm:w-28 flex-shrink-0">{k}</label>
481
- <input
482
- type="range"
483
- min={0}
484
- max={100}
485
- value={scores[k]}
486
- onChange={(e) =>
487
- setScores((s) => ({ ...s, [k]: Number(e.target.value) }))
488
- }
489
- className="w-full accent-ifrcRed"
490
- />
491
- <span className="ml-2 w-8 sm:w-10 text-right tabular-nums flex-shrink-0">{scores[k]}</span>
492
- </div>
493
- ))}
494
- </div>
495
-
496
- {/* ────── AI‑GENERATED CAPTION ────── */}
497
- <div className="text-left">
498
- <Heading level={3}>AI‑Generated Caption</Heading>
499
- <textarea
500
- className="w-full border rounded p-2 sm:p-3 font-mono mt-2"
501
- rows={5}
502
- value={draft}
503
- onChange={(e) => setDraft(e.target.value)}
504
- />
505
- </div>
506
-
507
- {/* ────── SUBMIT BUTTON ────── */}
508
- <div className="flex justify-center gap-4 mt-10">
509
- <Button
510
- name="delete"
511
- variant="secondary"
512
- onClick={handleDelete}
513
- >
514
- Delete
515
- </Button>
516
- <Button
517
- name="submit"
518
- onClick={handleSubmit}
519
- >
520
- Submit
521
- </Button>
522
- </div>
523
- </div>
524
- )}
525
-
526
- {/* Success page */}
527
- {step === 3 && (
528
- <div className="text-center space-y-6">
529
- <Heading level={2}>Saved!</Heading>
530
- <p className="text-gray-700">Your caption has been successfully saved.</p>
531
- <div className="flex justify-center mt-6">
532
- <Button
533
- name="upload-another"
534
- onClick={resetToStep1}
535
  >
536
- Upload Another
537
- </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  </div>
539
- </div>
540
- )}
541
-
542
-
543
- </div>
544
- </PageContainer>
545
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
546
  }
 
2
  import type { DragEvent } from 'react';
3
  import {
4
  PageContainer, Heading, Button,
5
+ SelectInput, MultiSelectInput, Container, IconButton, TextInput, TextArea, Spinner,
 
6
  } from '@ifrc-go/ui';
7
  import {
8
  UploadCloudLineIcon,
9
  ArrowRightLineIcon,
10
+ DeleteBinLineIcon,
11
  } from '@ifrc-go/icons';
12
+ import { Link, useSearchParams } from 'react-router-dom';
13
+
14
+ const SELECTED_MODEL_KEY = 'selectedVlmModel';
15
 
16
  export default function UploadPage() {
 
17
  const [searchParams] = useSearchParams();
18
  const [step, setStep] = useState<1 | 2 | 3>(1);
19
+ const [isLoading, setIsLoading] = useState(false);
20
  const stepRef = useRef(step);
21
  const uploadedImageIdRef = useRef<string | null>(null);
22
  const [preview, setPreview] = useState<string | null>(null);
23
  /* ---------------- local state ----------------- */
24
 
25
  const [file, setFile] = useState<File | null>(null);
 
 
26
  const [source, setSource] = useState('');
27
  const [type, setType] = useState('');
28
  const [epsg, setEpsg] = useState('');
29
  const [imageType, setImageType] = useState('');
30
  const [countries, setCountries] = useState<string[]>([]);
31
+ const [title, setTitle] = useState('');
32
 
33
  // Metadata options from database
34
  const [sources, setSources] = useState<{s_code: string, label: string}[]>([]);
 
39
 
40
  // Track uploaded image data for potential deletion
41
  const [uploadedImageId, setUploadedImageId] = useState<string | null>(null);
 
42
 
43
  // Keep refs updated with current values
44
  stepRef.current = step;
 
58
  fetch('/api/types').then(r => r.json()),
59
  fetch('/api/spatial-references').then(r => r.json()),
60
  fetch('/api/image-types').then(r => r.json()),
61
+ fetch('/api/countries').then(r => r.json()),
62
+ fetch('/api/models').then(r => r.json())
63
  ]).then(([sourcesData, typesData, spatialData, imageTypesData, countriesData]) => {
64
  setSources(sourcesData);
65
  setTypes(typesData);
 
67
  setImageTypes(imageTypesData);
68
  setCountriesOptions(countriesData);
69
 
70
+ // Set default values
71
  if (sourcesData.length > 0) setSource(sourcesData[0].s_code);
72
  if (typesData.length > 0) setType(typesData[0].t_code);
73
  if (spatialData.length > 0) setEpsg(spatialData[0].epsg);
 
75
  });
76
  }, []);
77
 
 
78
  useEffect(() => {
79
  const handleBeforeUnload = () => {
 
 
80
  if (uploadedImageIdRef.current && stepRef.current !== 3) {
81
  fetch(`/api/images/${uploadedImageIdRef.current}`, { method: "DELETE" }).catch(console.error);
82
  }
 
85
  window.addEventListener('beforeunload', handleBeforeUnload);
86
  return () => {
87
  window.removeEventListener('beforeunload', handleBeforeUnload);
 
88
  if (uploadedImageIdRef.current && stepRef.current !== 3) {
89
  fetch(`/api/images/${uploadedImageIdRef.current}`, { method: "DELETE" }).catch(console.error);
90
  }
91
  };
92
+ }, []);
93
 
94
  const [captionId, setCaptionId] = useState<string | null>(null);
95
  const [imageUrl, setImageUrl] = useState<string|null>(null);
 
111
  setEpsg(mapData.epsg);
112
  setImageType(mapData.image_type);
113
 
 
114
  return fetch(`/api/images/${mapId}/caption`, {
115
  method: 'POST',
116
  headers: {
 
118
  },
119
  body: new URLSearchParams({
120
  title: 'Generated Caption',
121
+ prompt: 'Describe this crisis map in detail',
122
+ ...(localStorage.getItem(SELECTED_MODEL_KEY) && {
123
+ model_name: localStorage.getItem(SELECTED_MODEL_KEY)!
124
+ })
125
  })
126
  });
127
  })
 
138
  }
139
  }, [searchParams]);
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  const resetToStep1 = () => {
142
  setStep(1);
143
  setFile(null);
 
145
  setImageUrl(null);
146
  setCaptionId(null);
147
  setDraft('');
148
+ setTitle('');
149
  setScores({ accuracy: 50, context: 50, usability: 50 });
150
  setUploadedImageId(null);
 
151
  };
152
  const [scores, setScores] = useState({
153
  accuracy: 50,
 
155
  usability: 50,
156
  });
157
 
158
+
159
+
160
  /* ---- drag-and-drop + file-picker handlers -------------------------- */
161
  const onDrop = useCallback((e: DragEvent<HTMLDivElement>) => {
162
  e.preventDefault();
 
199
  async function handleGenerate() {
200
  if (!file) return;
201
 
202
+ setIsLoading(true);
203
+
204
  const fd = new FormData();
205
  fd.append('file', file);
206
  fd.append('source', source);
 
209
  fd.append('image_type', imageType);
210
  countries.forEach((c) => fd.append('countries', c));
211
 
212
+ const modelName = localStorage.getItem(SELECTED_MODEL_KEY);
213
+ if (modelName) {
214
+ fd.append('model_name', modelName);
215
+ }
216
+
217
  try {
218
  /* 1) upload */
219
  const mapRes = await fetch('/api/images/', { method: 'POST', body: fd });
 
225
  if (!mapIdVal) throw new Error('Upload failed: image_id not found');
226
  setUploadedImageId(mapIdVal);
227
 
228
+ /* 2) caption */
229
  const capRes = await fetch(
230
  `/api/images/${mapIdVal}/caption`,
231
  {
 
234
  'Content-Type': 'application/x-www-form-urlencoded',
235
  },
236
  body: new URLSearchParams({
237
+ title: title || 'Generated Caption',
238
+ prompt: 'Analyze this crisis map and provide a detailed description of the emergency situation, affected areas, and key information shown in the map.',
239
+ ...(modelName && { model_name: modelName })
240
  })
241
  },
242
  );
243
  const capJson = await readJsonSafely(capRes);
244
  if (!capRes.ok) throw new Error(capJson.error || 'Caption failed');
245
  setCaptionId(capJson.cap_id);
 
246
  console.log(capJson);
247
 
248
+ /* 3) Extract and apply metadata from AI response */
249
+ const extractedMetadata = capJson.raw_json?.extracted_metadata;
250
+ if (extractedMetadata) {
251
+ console.log('Extracted metadata:', extractedMetadata);
252
+
253
+ // Apply AI-extracted metadata to form fields
254
+ if (extractedMetadata.title) setTitle(extractedMetadata.title);
255
+ if (extractedMetadata.source) setSource(extractedMetadata.source);
256
+ if (extractedMetadata.type) setType(extractedMetadata.type);
257
+ if (extractedMetadata.epsg) setEpsg(extractedMetadata.epsg);
258
+ if (extractedMetadata.countries && Array.isArray(extractedMetadata.countries)) {
259
+ setCountries(extractedMetadata.countries);
260
+ }
261
+ }
262
+
263
+ /* 4) continue workflow */
264
  setDraft(capJson.generated);
265
  setStep(2);
266
  } catch (err) {
267
  handleApiError(err, 'Upload');
268
+ } finally {
269
+ setIsLoading(false);
270
  }
271
  }
272
 
 
293
 
294
  // 2. Update caption
295
  const captionBody = {
296
+ title: title,
297
  edited: draft || '', // Use draft if available, otherwise empty string
298
  accuracy: scores.accuracy,
299
  context: scores.context,
 
309
 
310
  // Clear uploaded IDs since submission was successful
311
  setUploadedImageId(null);
 
312
  setStep(3);
313
  } catch (err) {
314
  handleApiError(err, 'Submit');
 
342
  /* ------------------------------------------------------------------- */
343
  return (
344
  <PageContainer>
345
+ <div className="mx-auto max-w-screen-lg text-center px-4 sm:px-6 lg:px-8 py-6 sm:py-10 overflow-x-hidden" data-step={step}>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  {/* Drop-zone */}
347
  {step === 1 && (
348
+ <Container
349
+ heading="Upload Your Image"
350
+ headingLevel={2}
351
+ withHeaderBorder
352
+ withInternalPadding
353
+ headingClassName="text-center"
354
  >
355
+ <div className="space-y-6">
356
+ <p className="text-gray-700 leading-relaxed max-w-2xl mx-auto">
357
+ This app evaluates how well multimodal AI models turn emergency maps
358
+ into meaningful text. Upload your map, let the AI generate a
359
+ description, then review and rate the result based on your expertise.
360
  </p>
361
+
362
+ {/* "More »" link */}
363
+ <div className="flex justify-center">
364
+ <Link
365
+ to="/help"
366
+ className="text-red-600 text-xs hover:text-red-700 hover:underline flex items-center gap-1"
367
+ >
368
+ More <ArrowRightLineIcon className="w-3 h-3" />
369
+ </Link>
370
+ </div>
371
+
372
+ <div
373
+ className={`border-2 border-dashed border-gray-300 bg-gray-50 rounded-xl py-8 sm:py-12 px-4 sm:px-8 flex flex-col items-center gap-4 sm:gap-6 hover:bg-gray-100 transition-colors max-w-sm sm:max-w-md lg:max-w-lg mx-auto min-h-[250px] sm:min-h-[300px] justify-center ${
374
+ file ? 'bg-white' : ''
375
+ }`}
376
+ onDragOver={(e) => e.preventDefault()}
377
+ onDrop={onDrop}
378
  >
379
+ {file && preview ? (
380
+ <div className="w-full max-w-full">
381
+ <div className="relative w-1/2 mx-auto max-h-32 overflow-hidden rounded-lg bg-gray-100">
382
+ <img
383
+ src={preview}
384
+ alt="File preview"
385
+ className="w-full h-full object-contain"
386
+ />
387
+ </div>
388
+ <p className="text-sm font-medium text-gray-800 mt-2 text-center">
389
+ {file.name}
390
+ </p>
391
+ </div>
392
+ ) : (
393
+ <>
394
+ <UploadCloudLineIcon className="w-10 h-10 text-ifrcRed" />
395
+ <p className="text-sm text-gray-600">Drag &amp; Drop a file here</p>
396
+ <p className="text-sm text-gray-500 my-4">or</p>
397
+ </>
398
+ )}
399
+
400
+ {/* File-picker button - always visible */}
401
+ <label className="inline-block cursor-pointer">
402
+ <input
403
+ type="file"
404
+ accept="image/*"
405
+ className="sr-only"
406
+ onChange={e => onFileChange(e.target.files?.[0], "file")}
407
+ />
408
+ <Button
409
+ name="upload"
410
+ variant="secondary"
411
+ size={1}
412
+ onClick={() => (document.querySelector('input[type="file"]') as HTMLInputElement)?.click()}
413
+ >
414
+ {file ? 'Change File' : 'Browse Device'}
415
+ </Button>
416
+ </label>
417
+ </div>
418
+ </div>
419
+ </Container>
420
+ )}
421
+
422
+ {/* Loading state */}
423
+ {isLoading && (
424
+ <div className="flex flex-col items-center justify-center gap-4 mt-12">
425
+ <Spinner className="text-ifrcRed" />
426
+ <p className="text-gray-600">Generating caption...</p>
427
  </div>
428
  )}
429
 
430
  {/* Generate button */}
431
+ {step === 1 && !isLoading && (
432
+ <div className="flex flex-col items-center justify-center gap-4 mt-12">
433
  <Button
434
  name="generate"
435
  disabled={!file}
 
440
  </div>
441
  )}
442
 
443
+ {step === 2 && imageUrl && (
444
+ <Container
445
+ heading="Uploaded Map"
446
+ headingLevel={3}
447
+ withHeaderBorder
448
+ withInternalPadding
449
+ >
450
+ <div className="flex justify-center">
451
+ <div className="w-full max-w-screen-lg max-h-80 overflow-hidden bg-red-50">
452
+ <img
453
+ src={preview || undefined}
454
+ alt="Uploaded map preview"
455
+ className="w-full h-full object-contain rounded shadow"
456
+ />
457
+ </div>
458
+ </div>
459
+ </Container>
460
+ )}
461
+
462
+ {step === 2 && (
463
+ <div className="space-y-6">
464
+ {/* ────── METADATA FORM ────── */}
465
+ <Container
466
+ heading="Map Metadata"
467
+ headingLevel={3}
468
+ withHeaderBorder
469
+ withInternalPadding
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  >
471
+ <div className="grid gap-4 text-left grid-cols-1 lg:grid-cols-2">
472
+ <div className="lg:col-span-2">
473
+ <TextInput
474
+ label="Title"
475
+ name="title"
476
+ value={title}
477
+ onChange={(value) => setTitle(value || '')}
478
+ placeholder="Enter a title for this map..."
479
+ required
480
+ />
481
+ </div>
482
+ <SelectInput
483
+ label="Source"
484
+ name="source"
485
+ value={source}
486
+ onChange={handleSourceChange}
487
+ options={sources}
488
+ keySelector={(o) => o.s_code}
489
+ labelSelector={(o) => o.label}
490
+ required
491
+ />
492
+ <SelectInput
493
+ label="Type"
494
+ name="type"
495
+ value={type}
496
+ onChange={handleTypeChange}
497
+ options={types}
498
+ keySelector={(o) => o.t_code}
499
+ labelSelector={(o) => o.label}
500
+ required
501
+ />
502
+ <SelectInput
503
+ label="EPSG"
504
+ name="epsg"
505
+ value={epsg}
506
+ onChange={handleEpsgChange}
507
+ options={spatialReferences}
508
+ keySelector={(o) => o.epsg}
509
+ labelSelector={(o) => `${o.srid} (EPSG:${o.epsg})`}
510
+ required
511
+ />
512
+ <SelectInput
513
+ label="Image Type"
514
+ name="image_type"
515
+ value={imageType}
516
+ onChange={handleImageTypeChange}
517
+ options={imageTypes}
518
+ keySelector={(o) => o.image_type}
519
+ labelSelector={(o) => o.label}
520
+ required
521
+ />
522
+ <MultiSelectInput
523
+ label="Countries (optional)"
524
+ name="countries"
525
+ value={countries}
526
+ onChange={handleCountriesChange}
527
+ options={countriesOptions}
528
+ keySelector={(o) => o.c_code}
529
+ labelSelector={(o) => o.label}
530
+ placeholder="Select one or more"
531
+ />
532
+ </div>
533
+ </Container>
534
+
535
+ {/* ────── RATING SLIDERS ────── */}
536
+ <Container
537
+ heading="AI Performance Rating"
538
+ headingLevel={3}
539
+ withHeaderBorder
540
+ withInternalPadding
541
+ >
542
+ <div className="text-left">
543
+ <p className="text-gray-700 mb-4">How well did the AI perform on the task?</p>
544
+ {(['accuracy', 'context', 'usability'] as const).map((k) => (
545
+ <div key={k} className="mt-6 flex items-center gap-2 sm:gap-4">
546
+ <label className="block text-sm font-medium capitalize w-20 sm:w-28 flex-shrink-0">{k}</label>
547
+ <input
548
+ type="range"
549
+ min={0}
550
+ max={100}
551
+ value={scores[k]}
552
+ onChange={(e) =>
553
+ setScores((s) => ({ ...s, [k]: Number(e.target.value) }))
554
+ }
555
+ className="w-full accent-ifrcRed"
556
+ />
557
+ <span className="ml-2 w-8 sm:w-10 text-right tabular-nums flex-shrink-0">{scores[k]}</span>
558
+ </div>
559
+ ))}
560
+ </div>
561
+ </Container>
562
+
563
+ {/* ────── AI‑GENERATED CAPTION ────── */}
564
+ <Container
565
+ heading="AI‑Generated Caption"
566
+ headingLevel={3}
567
+ withHeaderBorder
568
+ withInternalPadding
569
+ >
570
+ <div className="text-left">
571
+ <TextArea
572
+ name="caption"
573
+ value={draft}
574
+ onChange={(value) => setDraft(value || '')}
575
+ rows={5}
576
+ placeholder="AI-generated caption will appear here..."
577
+ />
578
+ </div>
579
+ </Container>
580
+
581
+ {/* ────── SUBMIT BUTTON ────── */}
582
+ <div className="flex justify-center gap-4 mt-10">
583
+ <IconButton
584
+ name="delete"
585
+ variant="tertiary"
586
+ onClick={handleDelete}
587
+ title="Delete"
588
+ ariaLabel="Delete uploaded image"
589
+ >
590
+ <DeleteBinLineIcon />
591
+ </IconButton>
592
+ <Button
593
+ name="submit"
594
+ onClick={handleSubmit}
595
+ >
596
+ Submit
597
+ </Button>
598
+ </div>
599
  </div>
600
+ )}
601
+
602
+ {/* Success page */}
603
+ {step === 3 && (
604
+ <div className="text-center space-y-6">
605
+ <Heading level={2}>Saved!</Heading>
606
+ <p className="text-gray-700">Your caption has been successfully saved.</p>
607
+ <div className="flex justify-center mt-6">
608
+ <Button
609
+ name="upload-another"
610
+ onClick={resetToStep1}
611
+ >
612
+ Upload Another
613
+ </Button>
614
+ </div>
615
+ </div>
616
+ )}
617
+ </div>
618
+ </PageContainer>
619
+ );
620
  }
go-web-app-develop/.changeset/README.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/config@3.0.0/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 ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/add-to-project@v0.4.0
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 ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/github-actions-storybook-to-github-pages@v1.0.3
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 ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/typos@v1.29.4
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 ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1 @@
 
 
1
+ enable-pre-post-scripts=true
go-web-app-develop/COLLABORATING.md ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 git@github.com: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 ADDED
@@ -0,0 +1,729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/ui@1.5.1
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/ui@1.5.0
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/ui@1.4.0
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/ui@1.3.1
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/ui@1.3.0
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/ui@1.2.3
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/ui@1.2.2
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/ui@1.2.1
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/ui@1.2.0
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/ui@1.1.6
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/ui@1.1.5
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/ui@1.1.4
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/ui@1.1.3
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/ui@1.1.2
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/ui@1.1.1
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/ui@1.0.0
go-web-app-develop/app/env.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
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 ADDED
go-web-app-develop/app/scripts/translatte/README.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;