Spaces:
Running
Running
Integrate VLMs
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +24 -5
- frontend/src/App.tsx +83 -1
- frontend/src/components/Card.tsx +34 -0
- frontend/src/components/HeaderNav.tsx +47 -36
- frontend/src/index.css +41 -0
- frontend/src/main.tsx +1 -0
- frontend/src/pages/AnalyticsPage.tsx +569 -3
- frontend/src/pages/DemoPage.tsx +1079 -0
- frontend/src/pages/DevPage.tsx +245 -0
- frontend/src/pages/ExplorePage.tsx +192 -112
- frontend/src/pages/MapDetailPage.tsx +20 -22
- frontend/src/pages/UploadPage.tsx +302 -228
- go-web-app-develop/.changeset/README.md +8 -0
- go-web-app-develop/.changeset/config.json +15 -0
- go-web-app-develop/.changeset/lovely-kids-boil.md +5 -0
- go-web-app-develop/.changeset/pre.json +15 -0
- go-web-app-develop/.changeset/solid-clubs-care.md +8 -0
- go-web-app-develop/.changeset/sweet-gifts-cheer.md +9 -0
- go-web-app-develop/.changeset/whole-lions-guess.md +7 -0
- go-web-app-develop/.dockerignore +148 -0
- go-web-app-develop/.github/ISSUE_TEMPLATE/01_bug_report.yml +92 -0
- go-web-app-develop/.github/ISSUE_TEMPLATE/02_feature_request.yml +39 -0
- go-web-app-develop/.github/ISSUE_TEMPLATE/03_epic_request.yml +37 -0
- go-web-app-develop/.github/ISSUE_TEMPLATE/config.yml +5 -0
- go-web-app-develop/.github/dependabot.yml +27 -0
- go-web-app-develop/.github/pull_request_template.md +30 -0
- go-web-app-develop/.github/workflows/add-issue-to-backlog.yml +16 -0
- go-web-app-develop/.github/workflows/chromatic.yml +127 -0
- go-web-app-develop/.github/workflows/ci.yml +304 -0
- go-web-app-develop/.github/workflows/publish-nginx-serve.yml +147 -0
- go-web-app-develop/.github/workflows/publish-storybook-nginx-serve.yml +127 -0
- go-web-app-develop/.gitignore +43 -0
- go-web-app-develop/.npmrc +1 -0
- go-web-app-develop/COLLABORATING.md +18 -0
- go-web-app-develop/CONTRIBUTING.md +81 -0
- go-web-app-develop/LICENSE +21 -0
- go-web-app-develop/README.md +117 -0
- go-web-app-develop/app/CHANGELOG.md +729 -0
- go-web-app-develop/app/env.ts +29 -0
- go-web-app-develop/app/eslint.config.js +165 -0
- go-web-app-develop/app/index.html +69 -0
- go-web-app-develop/app/package.json +119 -0
- go-web-app-develop/app/postcss.config.cjs +8 -0
- go-web-app-develop/app/public/go-icon.svg +4 -0
- go-web-app-develop/app/scripts/translatte/README.md +59 -0
- go-web-app-develop/app/scripts/translatte/commands/applyMigrations.test.ts +104 -0
- go-web-app-develop/app/scripts/translatte/commands/applyMigrations.ts +177 -0
- go-web-app-develop/app/scripts/translatte/commands/exportMigration.ts +62 -0
- go-web-app-develop/app/scripts/translatte/commands/generateMigration.test.ts +102 -0
- 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 |
-
# ───
|
| 13 |
-
/
|
| 14 |
-
/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 <
|
| 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 {
|
|
|
|
| 2 |
import {
|
| 3 |
UploadCloudLineIcon,
|
| 4 |
AnalysisIcon,
|
| 5 |
SearchLineIcon,
|
| 6 |
QuestionLineIcon,
|
| 7 |
GoMainIcon,
|
|
|
|
| 8 |
} from "@ifrc-go/icons";
|
| 9 |
|
| 10 |
-
/*
|
| 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 |
-
<
|
| 39 |
-
<
|
| 40 |
-
|
|
|
|
|
|
|
| 41 |
{/* ── Logo + title ─────────────────────────── */}
|
| 42 |
-
<
|
| 43 |
-
<GoMainIcon className="h-
|
| 44 |
-
<span className="font-semibold text-
|
| 45 |
-
</
|
| 46 |
|
| 47 |
{/* ── Centre nav links ─────────────────────── */}
|
| 48 |
-
<nav className="flex
|
| 49 |
-
{navItems.map(({ to, label, Icon }) => (
|
| 50 |
-
<
|
| 51 |
-
<
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
))}
|
| 54 |
</nav>
|
| 55 |
|
| 56 |
{/* ── Right-side utility buttons ───────────── */}
|
| 57 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
<QuestionLineIcon className="w-4 h-4" />
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
export default function AnalyticsPage() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
return (
|
| 5 |
-
<PageContainer
|
| 6 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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 [
|
| 33 |
|
| 34 |
const fetchMaps = () => {
|
| 35 |
-
|
| 36 |
// Fetch maps
|
| 37 |
fetch('/api/images/')
|
| 38 |
.then(r => {
|
|
@@ -65,7 +69,7 @@ export default function ExplorePage() {
|
|
| 65 |
setMaps([]);
|
| 66 |
})
|
| 67 |
.finally(() => {
|
| 68 |
-
|
| 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 =>
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 172 |
});
|
| 173 |
}, [maps, search, srcFilter, catFilter, regionFilter, countryFilter]);
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
| 177 |
return (
|
| 178 |
<PageContainer>
|
| 179 |
-
<div className="
|
| 180 |
-
|
| 181 |
-
<
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
|
|
|
| 199 |
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
|
|
|
| 210 |
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
|
|
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
|
|
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
|
|
|
| 244 |
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 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 |
-
|
| 280 |
-
<
|
| 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,
|
| 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 |
-
<
|
|
|
|
|
|
|
| 115 |
onClick={() => navigate('/explore')}
|
| 116 |
-
className="
|
| 117 |
>
|
| 118 |
← Back to Explore
|
| 119 |
-
</
|
| 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">
|
| 146 |
<div className="space-y-2 text-sm">
|
| 147 |
-
<div
|
| 148 |
-
|
|
|
|
| 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 |
-
<
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 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,
|
|
|
|
|
|
|
| 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
|
| 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 |
-
}, []);
|
| 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: '
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
|
|
|
|
|
|
| 364 |
>
|
| 365 |
-
<
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
</p>
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
|
|
|
| 387 |
>
|
| 388 |
-
{file
|
| 389 |
-
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 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 |
-
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
</div>
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 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 & 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;
|