Spaces:
Running
Running
UI refine & clean up
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- docker-compose.yml +2 -2
- frontend/src/App.css +0 -1
- frontend/src/App.tsx +2 -4
- frontend/src/components/Card.tsx +0 -14
- frontend/src/components/HeaderNav.tsx +50 -32
- frontend/src/index.css +0 -1
- frontend/src/layouts/RootLayout.tsx +0 -1
- frontend/src/pages/AnalyticsPage/AnalyticsPage.module.css +110 -0
- frontend/src/pages/{AnalyticsPage.tsx → AnalyticsPage/AnalyticsPage.tsx} +40 -75
- frontend/src/pages/AnalyticsPage/index.ts +1 -0
- frontend/src/pages/DemoPage.tsx +25 -151
- frontend/src/pages/DevPage.tsx +5 -20
- frontend/src/pages/ExplorePage.tsx +0 -365
- frontend/src/pages/ExplorePage/ExplorePage.module.css +125 -0
- frontend/src/pages/ExplorePage/ExplorePage.tsx +357 -0
- frontend/src/pages/ExplorePage/index.ts +1 -0
- frontend/src/pages/MapDetailsPage/MapDetailPage.module.css +184 -0
- frontend/src/pages/{MapDetailPage.tsx → MapDetailsPage/MapDetailPage.tsx} +113 -85
- frontend/src/pages/MapDetailsPage/index.ts +1 -0
- frontend/src/pages/UploadPage/UploadPage.module.css +495 -0
- frontend/src/pages/{UploadPage.tsx → UploadPage/UploadPage.tsx} +401 -237
- frontend/src/pages/UploadPage/index.ts +1 -0
- frontend/src/types.ts +3 -3
- frontend/tailwind.config.js +0 -1
- frontend/tsconfig.app.json +0 -4
- frontend/tsconfig.node.json +0 -4
- frontend/vite.config.ts +0 -3
- go-web-app-develop/.changeset/README.md +0 -8
- go-web-app-develop/.changeset/config.json +0 -15
- go-web-app-develop/.changeset/lovely-kids-boil.md +0 -5
- go-web-app-develop/.changeset/pre.json +0 -15
- go-web-app-develop/.changeset/solid-clubs-care.md +0 -8
- go-web-app-develop/.changeset/sweet-gifts-cheer.md +0 -9
- go-web-app-develop/.changeset/whole-lions-guess.md +0 -7
- go-web-app-develop/.dockerignore +0 -148
- go-web-app-develop/.github/ISSUE_TEMPLATE/01_bug_report.yml +0 -92
- go-web-app-develop/.github/ISSUE_TEMPLATE/02_feature_request.yml +0 -39
- go-web-app-develop/.github/ISSUE_TEMPLATE/03_epic_request.yml +0 -37
- go-web-app-develop/.github/ISSUE_TEMPLATE/config.yml +0 -5
- go-web-app-develop/.github/dependabot.yml +0 -27
- go-web-app-develop/.github/pull_request_template.md +0 -30
- go-web-app-develop/.github/workflows/add-issue-to-backlog.yml +0 -16
- go-web-app-develop/.github/workflows/chromatic.yml +0 -127
- go-web-app-develop/.github/workflows/ci.yml +0 -304
- go-web-app-develop/.github/workflows/publish-nginx-serve.yml +0 -147
- go-web-app-develop/.github/workflows/publish-storybook-nginx-serve.yml +0 -127
- go-web-app-develop/.gitignore +0 -43
- go-web-app-develop/.npmrc +0 -1
- go-web-app-develop/COLLABORATING.md +0 -18
- go-web-app-develop/CONTRIBUTING.md +0 -81
docker-compose.yml
CHANGED
@@ -32,8 +32,8 @@ services:
|
|
32 |
MINIO_ROOT_USER: promptaid
|
33 |
MINIO_ROOT_PASSWORD: promptaid
|
34 |
ports:
|
35 |
-
- "9000:9000"
|
36 |
-
- "9001:9001"
|
37 |
volumes:
|
38 |
- minio_data:/data
|
39 |
depends_on:
|
|
|
32 |
MINIO_ROOT_USER: promptaid
|
33 |
MINIO_ROOT_PASSWORD: promptaid
|
34 |
ports:
|
35 |
+
- "9000:9000"
|
36 |
+
- "9001:9001"
|
37 |
volumes:
|
38 |
- minio_data:/data
|
39 |
depends_on:
|
frontend/src/App.css
CHANGED
@@ -17,7 +17,6 @@ html, body {
|
|
17 |
transform-origin: top center;
|
18 |
}
|
19 |
|
20 |
-
/* Responsive adjustments for different screen sizes */
|
21 |
@media (min-width: 640px) {
|
22 |
#root {
|
23 |
padding: 1.5rem;
|
|
|
17 |
transform-origin: top center;
|
18 |
}
|
19 |
|
|
|
20 |
@media (min-width: 640px) {
|
21 |
#root {
|
22 |
padding: 1.5rem;
|
frontend/src/App.tsx
CHANGED
@@ -7,13 +7,13 @@ 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/
|
11 |
import DemoPage from './pages/DemoPage';
|
12 |
import DevPage from './pages/DevPage';
|
13 |
|
14 |
const router = createBrowserRouter([
|
15 |
{
|
16 |
-
element: <RootLayout />,
|
17 |
children: [
|
18 |
{ path: '/', element: <UploadPage /> },
|
19 |
{ path: '/upload', element: <UploadPage /> },
|
@@ -28,7 +28,6 @@ const router = createBrowserRouter([
|
|
28 |
]);
|
29 |
|
30 |
function Application() {
|
31 |
-
// ALERTS
|
32 |
const [alerts, setAlerts] = useState<AlertParams[]>([]);
|
33 |
|
34 |
const addAlert = useCallback((alert: AlertParams) => {
|
@@ -79,7 +78,6 @@ function Application() {
|
|
79 |
[alerts, addAlert, removeAlert, updateAlert],
|
80 |
);
|
81 |
|
82 |
-
// LANGUAGE
|
83 |
const languageContextValue = useMemo<LanguageContextProps>(
|
84 |
() => ({
|
85 |
languageNamespaceStatus: {},
|
|
|
7 |
import AnalyticsPage from './pages/AnalyticsPage';
|
8 |
import ExplorePage from './pages/ExplorePage';
|
9 |
import HelpPage from './pages/HelpPage';
|
10 |
+
import MapDetailPage from './pages/MapDetailsPage';
|
11 |
import DemoPage from './pages/DemoPage';
|
12 |
import DevPage from './pages/DevPage';
|
13 |
|
14 |
const router = createBrowserRouter([
|
15 |
{
|
16 |
+
element: <RootLayout />,
|
17 |
children: [
|
18 |
{ path: '/', element: <UploadPage /> },
|
19 |
{ path: '/upload', element: <UploadPage /> },
|
|
|
28 |
]);
|
29 |
|
30 |
function Application() {
|
|
|
31 |
const [alerts, setAlerts] = useState<AlertParams[]>([]);
|
32 |
|
33 |
const addAlert = useCallback((alert: AlertParams) => {
|
|
|
78 |
[alerts, addAlert, removeAlert, updateAlert],
|
79 |
);
|
80 |
|
|
|
81 |
const languageContextValue = useMemo<LanguageContextProps>(
|
82 |
() => ({
|
83 |
languageNamespaceStatus: {},
|
frontend/src/components/Card.tsx
CHANGED
@@ -1,24 +1,10 @@
|
|
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
|
|
|
|
|
1 |
import React from 'react'
|
2 |
|
3 |
export interface CardProps {
|
|
|
4 |
className?: string
|
|
|
5 |
children: React.ReactNode
|
6 |
}
|
7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
export default function Card({ children, className = '' }: CardProps) {
|
9 |
return (
|
10 |
<div
|
frontend/src/components/HeaderNav.tsx
CHANGED
@@ -9,7 +9,6 @@ import {
|
|
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 },
|
@@ -22,51 +21,70 @@ export default function HeaderNav() {
|
|
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-
|
29 |
>
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
</div>
|
35 |
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
<
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
}
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
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-
|
70 |
</Button>
|
71 |
</PageContainer>
|
72 |
</nav>
|
|
|
9 |
SettingsIcon,
|
10 |
} from "@ifrc-go/icons";
|
11 |
|
|
|
12 |
const navItems = [
|
13 |
{ to: "/upload", label: "Upload", Icon: UploadCloudLineIcon },
|
14 |
{ to: "/explore", label: "Explore", Icon: SearchLineIcon },
|
|
|
21 |
const navigate = useNavigate();
|
22 |
|
23 |
return (
|
24 |
+
<nav className="border-b border-gray-200 bg-white shadow-sm sticky top-0 z-50 backdrop-blur-sm bg-white/95">
|
25 |
<PageContainer
|
26 |
+
className="border-b-2 border-ifrcRed"
|
27 |
+
contentClassName="flex items-center justify-between py-6"
|
28 |
>
|
29 |
+
<div
|
30 |
+
className="flex items-center gap-4 min-w-0 cursor-pointer group transition-all duration-200 hover:scale-105"
|
31 |
+
onClick={() => navigate('/')}
|
32 |
+
>
|
33 |
+
<div className="p-2 rounded-lg bg-gradient-to-br from-ifrcRed/10 to-ifrcRed/20 group-hover:from-ifrcRed/20 group-hover:to-ifrcRed/30 transition-all duration-200">
|
34 |
+
<GoMainIcon className="h-8 w-8 flex-shrink-0 text-ifrcRed" />
|
35 |
+
</div>
|
36 |
+
<div className="flex flex-col">
|
37 |
+
<span className="font-bold text-xl text-gray-900 leading-tight">PromptAid Vision</span>
|
38 |
+
<span className="text-sm text-gray-500 font-medium">AI-Powered Image Analysis</span>
|
39 |
+
</div>
|
40 |
</div>
|
41 |
|
42 |
+
<nav className="flex items-center space-x-2 bg-gray-50/80 rounded-xl p-2 backdrop-blur-sm">
|
43 |
+
{navItems.map(({ to, label, Icon }) => {
|
44 |
+
const isActive = location.pathname === to;
|
45 |
+
return (
|
46 |
+
<div key={to} className="relative">
|
47 |
+
<Button
|
48 |
+
name={label.toLowerCase()}
|
49 |
+
variant={isActive ? "primary" : "tertiary"}
|
50 |
+
size={1}
|
51 |
+
className={`transition-all duration-200 ${
|
52 |
+
isActive
|
53 |
+
? 'shadow-lg shadow-ifrcRed/20 transform scale-105'
|
54 |
+
: 'hover:bg-white hover:shadow-md hover:scale-105'
|
55 |
+
}`}
|
56 |
+
onClick={() => {
|
57 |
+
if (location.pathname === "/upload") {
|
58 |
+
const uploadPage = document.querySelector('[data-step="2"]');
|
59 |
+
if (uploadPage && !confirm("Changes will not be saved")) {
|
60 |
+
return;
|
61 |
+
}
|
62 |
}
|
63 |
+
navigate(to);
|
64 |
+
}}
|
65 |
+
>
|
66 |
+
<Icon className={`w-4 h-4 transition-transform duration-200 ${
|
67 |
+
isActive ? 'scale-110' : 'group-hover:scale-110'
|
68 |
+
}`} />
|
69 |
+
<span className="inline ml-2 font-semibold">{label}</span>
|
70 |
+
</Button>
|
71 |
+
{isActive && (
|
72 |
+
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-8 h-1 bg-ifrcRed rounded-full animate-pulse"></div>
|
73 |
+
)}
|
74 |
+
</div>
|
75 |
+
);
|
76 |
+
})}
|
77 |
</nav>
|
78 |
|
|
|
79 |
<Button
|
80 |
name="help"
|
81 |
variant="tertiary"
|
82 |
size={1}
|
83 |
+
className="transition-all duration-200 hover:bg-blue-50 hover:text-blue-600 hover:shadow-md hover:scale-105"
|
84 |
onClick={() => navigate('/help')}
|
85 |
>
|
86 |
<QuestionLineIcon className="w-4 h-4" />
|
87 |
+
<span className="inline ml-2 font-semibold">Help & Support</span>
|
88 |
</Button>
|
89 |
</PageContainer>
|
90 |
</nav>
|
frontend/src/index.css
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
-
/* src/index.css */
|
2 |
@tailwind base;
|
3 |
@tailwind components;
|
4 |
@tailwind utilities;
|
|
|
|
|
1 |
@tailwind base;
|
2 |
@tailwind components;
|
3 |
@tailwind utilities;
|
frontend/src/layouts/RootLayout.tsx
CHANGED
@@ -5,7 +5,6 @@ export default function RootLayout() {
|
|
5 |
return (
|
6 |
<>
|
7 |
<HeaderNav />
|
8 |
-
{/* All routed pages render here */}
|
9 |
<Outlet />
|
10 |
</>
|
11 |
);
|
|
|
5 |
return (
|
6 |
<>
|
7 |
<HeaderNav />
|
|
|
8 |
<Outlet />
|
9 |
</>
|
10 |
);
|
frontend/src/pages/AnalyticsPage/AnalyticsPage.module.css
ADDED
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.tabSelector {
|
2 |
+
display: flex;
|
3 |
+
justify-content: center;
|
4 |
+
margin: var(--go-ui-spacing-xl) 0;
|
5 |
+
}
|
6 |
+
|
7 |
+
.summaryStats {
|
8 |
+
display: grid;
|
9 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
10 |
+
gap: var(--go-ui-spacing-lg);
|
11 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
12 |
+
}
|
13 |
+
|
14 |
+
.progressSection {
|
15 |
+
margin-top: var(--go-ui-spacing-lg);
|
16 |
+
padding-top: var(--go-ui-spacing-lg);
|
17 |
+
border-top: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
18 |
+
}
|
19 |
+
|
20 |
+
.progressLabel {
|
21 |
+
display: flex;
|
22 |
+
justify-content: space-between;
|
23 |
+
margin-bottom: var(--go-ui-spacing-sm);
|
24 |
+
color: var(--go-ui-color-text);
|
25 |
+
font-weight: var(--go-ui-font-weight-medium);
|
26 |
+
}
|
27 |
+
|
28 |
+
.chartGrid {
|
29 |
+
display: grid;
|
30 |
+
grid-template-columns: 1fr;
|
31 |
+
gap: var(--go-ui-spacing-xl);
|
32 |
+
}
|
33 |
+
|
34 |
+
.chartSection {
|
35 |
+
display: grid;
|
36 |
+
grid-template-columns: 1fr;
|
37 |
+
gap: var(--go-ui-spacing-lg);
|
38 |
+
}
|
39 |
+
|
40 |
+
.chartContainer {
|
41 |
+
display: flex;
|
42 |
+
justify-content: center;
|
43 |
+
align-items: center;
|
44 |
+
min-height: 300px;
|
45 |
+
background-color: var(--go-ui-color-gray-10);
|
46 |
+
border-radius: var(--go-ui-border-radius-lg);
|
47 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
48 |
+
padding: var(--go-ui-spacing-lg);
|
49 |
+
}
|
50 |
+
|
51 |
+
.tableContainer {
|
52 |
+
background-color: var(--go-ui-color-white);
|
53 |
+
border-radius: var(--go-ui-border-radius-lg);
|
54 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
55 |
+
overflow: hidden;
|
56 |
+
box-shadow: var(--go-ui-box-shadow-sm);
|
57 |
+
}
|
58 |
+
|
59 |
+
.modelPerformance {
|
60 |
+
background-color: var(--go-ui-color-white);
|
61 |
+
border-radius: var(--go-ui-border-radius-lg);
|
62 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
63 |
+
overflow: hidden;
|
64 |
+
box-shadow: var(--go-ui-box-shadow-sm);
|
65 |
+
}
|
66 |
+
|
67 |
+
.loadingContainer {
|
68 |
+
display: flex;
|
69 |
+
align-items: center;
|
70 |
+
justify-content: center;
|
71 |
+
min-height: 400px;
|
72 |
+
color: var(--go-ui-color-gray-60);
|
73 |
+
font-size: var(--go-ui-font-size-lg);
|
74 |
+
font-weight: var(--go-ui-font-weight-medium);
|
75 |
+
}
|
76 |
+
|
77 |
+
.errorContainer {
|
78 |
+
display: flex;
|
79 |
+
align-items: center;
|
80 |
+
justify-content: center;
|
81 |
+
min-height: 400px;
|
82 |
+
color: var(--go-ui-color-negative);
|
83 |
+
font-size: var(--go-ui-font-size-lg);
|
84 |
+
font-weight: var(--go-ui-font-weight-medium);
|
85 |
+
}
|
86 |
+
|
87 |
+
|
88 |
+
|
89 |
+
/* Responsive adjustments */
|
90 |
+
@media (min-width: 1024px) {
|
91 |
+
.chartSection {
|
92 |
+
grid-template-columns: 1fr 1fr;
|
93 |
+
}
|
94 |
+
}
|
95 |
+
|
96 |
+
@media (max-width: 768px) {
|
97 |
+
.summaryStats {
|
98 |
+
grid-template-columns: 1fr;
|
99 |
+
gap: var(--go-ui-spacing-md);
|
100 |
+
}
|
101 |
+
|
102 |
+
.chartContainer {
|
103 |
+
min-height: 250px;
|
104 |
+
padding: var(--go-ui-spacing-md);
|
105 |
+
}
|
106 |
+
|
107 |
+
.tabSelector {
|
108 |
+
margin: var(--go-ui-spacing-lg) 0;
|
109 |
+
}
|
110 |
+
}
|
frontend/src/pages/{AnalyticsPage.tsx → AnalyticsPage/AnalyticsPage.tsx}
RENAMED
@@ -1,5 +1,3 @@
|
|
1 |
-
// src/pages/AnalyticsPage.tsx
|
2 |
-
|
3 |
import {
|
4 |
PageContainer,
|
5 |
PieChart,
|
@@ -16,7 +14,7 @@ import {
|
|
16 |
numericIdSelector
|
17 |
} from '@ifrc-go/ui/utils';
|
18 |
import { useState, useEffect, useMemo } from 'react';
|
19 |
-
|
20 |
|
21 |
interface AnalyticsData {
|
22 |
totalCaptions: number;
|
@@ -80,7 +78,6 @@ export default function AnalyticsPage() {
|
|
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' }
|
@@ -107,23 +104,22 @@ export default function AnalyticsPage() {
|
|
107 |
|
108 |
maps.forEach((map: any) => {
|
109 |
if (map.source) analytics.sources[map.source] = (analytics.sources[map.source] || 0) + 1;
|
110 |
-
if (map.
|
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.
|
117 |
-
const m = map.
|
118 |
const ctr = analytics.models[m] ||= { count: 0, avgAccuracy: 0, avgContext: 0, avgUsability: 0, totalScore: 0 };
|
119 |
ctr.count++;
|
120 |
-
if (map.
|
121 |
-
if (map.
|
122 |
-
if (map.
|
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;
|
@@ -136,14 +132,12 @@ export default function AnalyticsPage() {
|
|
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]) {
|
@@ -153,16 +147,16 @@ export default function AnalyticsPage() {
|
|
153 |
|
154 |
Object.values(analytics.models).forEach(m => {
|
155 |
if (m.count > 0) {
|
156 |
-
m.avgAccuracy
|
157 |
-
m.avgContext
|
158 |
m.avgUsability = Math.round(m.avgUsability / m.count);
|
159 |
-
m.totalScore
|
160 |
}
|
161 |
});
|
162 |
|
163 |
setData(analytics);
|
164 |
} catch (e) {
|
165 |
-
|
166 |
setData(null);
|
167 |
} finally {
|
168 |
setLoading(false);
|
@@ -183,7 +177,7 @@ export default function AnalyticsPage() {
|
|
183 |
setTypesLookup(types);
|
184 |
setRegionsLookup(regions);
|
185 |
} catch (e) {
|
186 |
-
|
187 |
}
|
188 |
}
|
189 |
|
@@ -197,16 +191,9 @@ export default function AnalyticsPage() {
|
|
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] = {
|
@@ -217,7 +204,6 @@ export default function AnalyticsPage() {
|
|
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) => ({
|
@@ -228,7 +214,6 @@ export default function AnalyticsPage() {
|
|
228 |
}));
|
229 |
}, [data, regionsLookup]);
|
230 |
|
231 |
-
// Transform types data for IFRC Table
|
232 |
const typesTableData = useMemo(() => {
|
233 |
if (!data) return [];
|
234 |
|
@@ -242,7 +227,6 @@ export default function AnalyticsPage() {
|
|
242 |
}));
|
243 |
}, [data, typesLookup]);
|
244 |
|
245 |
-
// Transform sources data for IFRC Table
|
246 |
const sourcesTableData = useMemo(() => {
|
247 |
if (!data) return [];
|
248 |
|
@@ -256,7 +240,6 @@ export default function AnalyticsPage() {
|
|
256 |
}));
|
257 |
}, [data, sourcesLookup]);
|
258 |
|
259 |
-
// Transform models data for IFRC Table
|
260 |
const modelsTableData = useMemo(() => {
|
261 |
if (!data) return [];
|
262 |
|
@@ -273,7 +256,6 @@ export default function AnalyticsPage() {
|
|
273 |
}));
|
274 |
}, [data]);
|
275 |
|
276 |
-
// Create columns for regions table
|
277 |
const regionsColumns = useMemo(() => [
|
278 |
createStringColumn<RegionData, number>(
|
279 |
'name',
|
@@ -296,7 +278,6 @@ export default function AnalyticsPage() {
|
|
296 |
),
|
297 |
], []);
|
298 |
|
299 |
-
// Create columns for types table
|
300 |
const typesColumns = useMemo(() => [
|
301 |
createStringColumn<TypeData, number>(
|
302 |
'name',
|
@@ -319,7 +300,6 @@ export default function AnalyticsPage() {
|
|
319 |
),
|
320 |
], []);
|
321 |
|
322 |
-
// Create columns for sources table
|
323 |
const sourcesColumns = useMemo(() => [
|
324 |
createStringColumn<SourceData, number>(
|
325 |
'name',
|
@@ -342,7 +322,6 @@ export default function AnalyticsPage() {
|
|
342 |
),
|
343 |
], []);
|
344 |
|
345 |
-
// Create columns for models table
|
346 |
const modelsColumns = useMemo(() => [
|
347 |
createStringColumn<ModelData, number>(
|
348 |
'name',
|
@@ -395,7 +374,7 @@ export default function AnalyticsPage() {
|
|
395 |
if (loading) {
|
396 |
return (
|
397 |
<PageContainer>
|
398 |
-
<div className=
|
399 |
<Spinner />
|
400 |
</div>
|
401 |
</PageContainer>
|
@@ -405,7 +384,7 @@ export default function AnalyticsPage() {
|
|
405 |
if (!data) {
|
406 |
return (
|
407 |
<PageContainer>
|
408 |
-
<div className=
|
409 |
<div className="text-red-500">Failed to load analytics data. Please try again.</div>
|
410 |
</div>
|
411 |
</PageContainer>
|
@@ -413,22 +392,13 @@ export default function AnalyticsPage() {
|
|
413 |
}
|
414 |
|
415 |
const sourcesChartData = Object.entries(data.sources).filter(([, value]) => value > 0).map(([name, value]) => ({ name, value }));
|
416 |
-
const typesChartData
|
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',
|
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
|
@@ -438,8 +408,7 @@ export default function AnalyticsPage() {
|
|
438 |
withInternalPadding
|
439 |
className="max-w-7xl mx-auto"
|
440 |
>
|
441 |
-
{
|
442 |
-
<div className="flex justify-center my-6">
|
443 |
<SegmentInput
|
444 |
name="analytics-view"
|
445 |
value={view}
|
@@ -455,10 +424,9 @@ export default function AnalyticsPage() {
|
|
455 |
</div>
|
456 |
|
457 |
{view === 'general' ? (
|
458 |
-
<div className=
|
459 |
-
{/* Summary Statistics */}
|
460 |
<Container heading="Summary Statistics" headingLevel={3} withHeaderBorder withInternalPadding>
|
461 |
-
<div className=
|
462 |
<KeyFigure
|
463 |
value={data.totalCaptions}
|
464 |
label="Total Captions"
|
@@ -470,8 +438,8 @@ export default function AnalyticsPage() {
|
|
470 |
compactValue
|
471 |
/>
|
472 |
</div>
|
473 |
-
<div className=
|
474 |
-
<div className=
|
475 |
<span>Progress towards target</span>
|
476 |
<span>{Math.round((data.totalCaptions / 2000) * 100)}%</span>
|
477 |
</div>
|
@@ -479,11 +447,9 @@ export default function AnalyticsPage() {
|
|
479 |
</div>
|
480 |
</Container>
|
481 |
|
482 |
-
|
483 |
-
{/* Regions Chart & Data */}
|
484 |
<Container heading="Regions Distribution" headingLevel={3} withHeaderBorder withInternalPadding>
|
485 |
-
<div className=
|
486 |
-
<div className=
|
487 |
<PieChart
|
488 |
data={regionsChartData}
|
489 |
valueSelector={d => d.value}
|
@@ -493,7 +459,7 @@ export default function AnalyticsPage() {
|
|
493 |
showPercentageInLegend
|
494 |
/>
|
495 |
</div>
|
496 |
-
<div className=
|
497 |
<Table
|
498 |
data={regionsTableData}
|
499 |
columns={regionsColumns}
|
@@ -505,10 +471,9 @@ export default function AnalyticsPage() {
|
|
505 |
</div>
|
506 |
</Container>
|
507 |
|
508 |
-
{/* Sources Chart & Data */}
|
509 |
<Container heading="Sources Distribution" headingLevel={3} withHeaderBorder withInternalPadding>
|
510 |
-
<div className=
|
511 |
-
<div className=
|
512 |
<PieChart
|
513 |
data={sourcesChartData}
|
514 |
valueSelector={d => d.value}
|
@@ -518,7 +483,7 @@ export default function AnalyticsPage() {
|
|
518 |
showPercentageInLegend
|
519 |
/>
|
520 |
</div>
|
521 |
-
<div className=
|
522 |
<Table
|
523 |
data={sourcesTableData}
|
524 |
columns={sourcesColumns}
|
@@ -530,10 +495,9 @@ export default function AnalyticsPage() {
|
|
530 |
</div>
|
531 |
</Container>
|
532 |
|
533 |
-
{/* Types Chart & Data */}
|
534 |
<Container heading="Types Distribution" headingLevel={3} withHeaderBorder withInternalPadding>
|
535 |
-
<div className=
|
536 |
-
<div className=
|
537 |
<PieChart
|
538 |
data={typesChartData}
|
539 |
valueSelector={d => d.value}
|
@@ -543,7 +507,7 @@ export default function AnalyticsPage() {
|
|
543 |
showPercentageInLegend
|
544 |
/>
|
545 |
</div>
|
546 |
-
<div className=
|
547 |
<Table
|
548 |
data={typesTableData}
|
549 |
columns={typesColumns}
|
@@ -556,16 +520,17 @@ export default function AnalyticsPage() {
|
|
556 |
</Container>
|
557 |
</div>
|
558 |
) : (
|
559 |
-
<div className=
|
560 |
-
{/* Model Performance */}
|
561 |
<Container heading="Model Performance" headingLevel={3} withHeaderBorder withInternalPadding>
|
562 |
-
<
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
|
|
|
|
569 |
</Container>
|
570 |
</div>
|
571 |
)}
|
|
|
|
|
|
|
1 |
import {
|
2 |
PageContainer,
|
3 |
PieChart,
|
|
|
14 |
numericIdSelector
|
15 |
} from '@ifrc-go/ui/utils';
|
16 |
import { useState, useEffect, useMemo } from 'react';
|
17 |
+
import styles from './AnalyticsPage.module.css';
|
18 |
|
19 |
interface AnalyticsData {
|
20 |
totalCaptions: number;
|
|
|
78 |
const [typesLookup, setTypesLookup] = useState<LookupData[]>([]);
|
79 |
const [regionsLookup, setRegionsLookup] = useState<LookupData[]>([]);
|
80 |
|
|
|
81 |
const viewOptions = [
|
82 |
{ key: 'general' as const, label: 'General Analytics' },
|
83 |
{ key: 'vlm' as const, label: 'VLM Analytics' }
|
|
|
104 |
|
105 |
maps.forEach((map: any) => {
|
106 |
if (map.source) analytics.sources[map.source] = (analytics.sources[map.source] || 0) + 1;
|
107 |
+
if (map.event_type) analytics.types[map.event_type] = (analytics.types[map.event_type] || 0) + 1;
|
108 |
if (map.countries) {
|
109 |
map.countries.forEach((c: any) => {
|
110 |
if (c.r_code) analytics.regions[c.r_code] = (analytics.regions[c.r_code] || 0) + 1;
|
111 |
});
|
112 |
}
|
113 |
+
if (map.captions && map.captions.length > 0 && map.captions[0].model) {
|
114 |
+
const m = map.captions[0].model;
|
115 |
const ctr = analytics.models[m] ||= { count: 0, avgAccuracy: 0, avgContext: 0, avgUsability: 0, totalScore: 0 };
|
116 |
ctr.count++;
|
117 |
+
if (map.captions[0].accuracy != null) ctr.avgAccuracy += map.captions[0].accuracy;
|
118 |
+
if (map.captions[0].context != null) ctr.avgContext += map.captions[0].context;
|
119 |
+
if (map.captions[0].usability != null) ctr.avgUsability += map.captions[0].usability;
|
120 |
}
|
121 |
});
|
122 |
|
|
|
123 |
sourcesLookup.forEach(source => {
|
124 |
if (source.s_code && !analytics.sources[source.s_code]) {
|
125 |
analytics.sources[source.s_code] = 0;
|
|
|
132 |
}
|
133 |
});
|
134 |
|
|
|
135 |
regionsLookup.forEach(region => {
|
136 |
if (region.r_code && !analytics.regions[region.r_code]) {
|
137 |
analytics.regions[region.r_code] = 0;
|
138 |
}
|
139 |
});
|
140 |
|
|
|
141 |
const allModels = ['GPT-4', 'Claude', 'Gemini', 'Llama', 'Other'];
|
142 |
allModels.forEach(model => {
|
143 |
if (!analytics.models[model]) {
|
|
|
147 |
|
148 |
Object.values(analytics.models).forEach(m => {
|
149 |
if (m.count > 0) {
|
150 |
+
m.avgAccuracy = Math.round(m.avgAccuracy / m.count);
|
151 |
+
m.avgContext = Math.round(m.avgContext / m.count);
|
152 |
m.avgUsability = Math.round(m.avgUsability / m.count);
|
153 |
+
m.totalScore = Math.round((m.avgAccuracy + m.avgContext + m.avgUsability) / 3);
|
154 |
}
|
155 |
});
|
156 |
|
157 |
setData(analytics);
|
158 |
} catch (e) {
|
159 |
+
|
160 |
setData(null);
|
161 |
} finally {
|
162 |
setLoading(false);
|
|
|
177 |
setTypesLookup(types);
|
178 |
setRegionsLookup(regions);
|
179 |
} catch (e) {
|
180 |
+
|
181 |
}
|
182 |
}
|
183 |
|
|
|
191 |
return type ? type.label : code;
|
192 |
};
|
193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
194 |
const regionsTableData = useMemo(() => {
|
195 |
if (!data || !regionsLookup.length) return [];
|
196 |
|
|
|
197 |
const allRegions = regionsLookup.reduce((acc, region) => {
|
198 |
if (region.r_code) {
|
199 |
acc[region.r_code] = {
|
|
|
204 |
return acc;
|
205 |
}, {} as Record<string, { name: string; count: number }>);
|
206 |
|
|
|
207 |
return Object.entries(allRegions)
|
208 |
.sort(([,a], [,b]) => b.count - a.count)
|
209 |
.map(([_, { name, count }], index) => ({
|
|
|
214 |
}));
|
215 |
}, [data, regionsLookup]);
|
216 |
|
|
|
217 |
const typesTableData = useMemo(() => {
|
218 |
if (!data) return [];
|
219 |
|
|
|
227 |
}));
|
228 |
}, [data, typesLookup]);
|
229 |
|
|
|
230 |
const sourcesTableData = useMemo(() => {
|
231 |
if (!data) return [];
|
232 |
|
|
|
240 |
}));
|
241 |
}, [data, sourcesLookup]);
|
242 |
|
|
|
243 |
const modelsTableData = useMemo(() => {
|
244 |
if (!data) return [];
|
245 |
|
|
|
256 |
}));
|
257 |
}, [data]);
|
258 |
|
|
|
259 |
const regionsColumns = useMemo(() => [
|
260 |
createStringColumn<RegionData, number>(
|
261 |
'name',
|
|
|
278 |
),
|
279 |
], []);
|
280 |
|
|
|
281 |
const typesColumns = useMemo(() => [
|
282 |
createStringColumn<TypeData, number>(
|
283 |
'name',
|
|
|
300 |
),
|
301 |
], []);
|
302 |
|
|
|
303 |
const sourcesColumns = useMemo(() => [
|
304 |
createStringColumn<SourceData, number>(
|
305 |
'name',
|
|
|
322 |
),
|
323 |
], []);
|
324 |
|
|
|
325 |
const modelsColumns = useMemo(() => [
|
326 |
createStringColumn<ModelData, number>(
|
327 |
'name',
|
|
|
374 |
if (loading) {
|
375 |
return (
|
376 |
<PageContainer>
|
377 |
+
<div className={styles.loadingContainer}>
|
378 |
<Spinner />
|
379 |
</div>
|
380 |
</PageContainer>
|
|
|
384 |
if (!data) {
|
385 |
return (
|
386 |
<PageContainer>
|
387 |
+
<div className={styles.errorContainer}>
|
388 |
<div className="text-red-500">Failed to load analytics data. Please try again.</div>
|
389 |
</div>
|
390 |
</PageContainer>
|
|
|
392 |
}
|
393 |
|
394 |
const sourcesChartData = Object.entries(data.sources).filter(([, value]) => value > 0).map(([name, value]) => ({ name, value }));
|
395 |
+
const typesChartData = Object.entries(data.types).filter(([, value]) => value > 0).map(([name, value]) => ({ name, value }));
|
396 |
const regionsChartData = Object.entries(data.regions).filter(([, value]) => value > 0).map(([name, value]) => ({ name, value }));
|
397 |
|
|
|
398 |
const ifrcColors = [
|
399 |
+
'#F5333F', '#F64752', '#F75C65', '#F87079', '#F9858C', '#FA999F', '#FBADB2', '#FCC2C5'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
400 |
];
|
401 |
|
|
|
402 |
return (
|
403 |
<PageContainer>
|
404 |
<Container
|
|
|
408 |
withInternalPadding
|
409 |
className="max-w-7xl mx-auto"
|
410 |
>
|
411 |
+
<div className={styles.tabSelector}>
|
|
|
412 |
<SegmentInput
|
413 |
name="analytics-view"
|
414 |
value={view}
|
|
|
424 |
</div>
|
425 |
|
426 |
{view === 'general' ? (
|
427 |
+
<div className={styles.chartGrid}>
|
|
|
428 |
<Container heading="Summary Statistics" headingLevel={3} withHeaderBorder withInternalPadding>
|
429 |
+
<div className={styles.summaryStats}>
|
430 |
<KeyFigure
|
431 |
value={data.totalCaptions}
|
432 |
label="Total Captions"
|
|
|
438 |
compactValue
|
439 |
/>
|
440 |
</div>
|
441 |
+
<div className={styles.progressSection}>
|
442 |
+
<div className={styles.progressLabel}>
|
443 |
<span>Progress towards target</span>
|
444 |
<span>{Math.round((data.totalCaptions / 2000) * 100)}%</span>
|
445 |
</div>
|
|
|
447 |
</div>
|
448 |
</Container>
|
449 |
|
|
|
|
|
450 |
<Container heading="Regions Distribution" headingLevel={3} withHeaderBorder withInternalPadding>
|
451 |
+
<div className={styles.chartSection}>
|
452 |
+
<div className={styles.chartContainer}>
|
453 |
<PieChart
|
454 |
data={regionsChartData}
|
455 |
valueSelector={d => d.value}
|
|
|
459 |
showPercentageInLegend
|
460 |
/>
|
461 |
</div>
|
462 |
+
<div className={styles.tableContainer}>
|
463 |
<Table
|
464 |
data={regionsTableData}
|
465 |
columns={regionsColumns}
|
|
|
471 |
</div>
|
472 |
</Container>
|
473 |
|
|
|
474 |
<Container heading="Sources Distribution" headingLevel={3} withHeaderBorder withInternalPadding>
|
475 |
+
<div className={styles.chartSection}>
|
476 |
+
<div className={styles.chartContainer}>
|
477 |
<PieChart
|
478 |
data={sourcesChartData}
|
479 |
valueSelector={d => d.value}
|
|
|
483 |
showPercentageInLegend
|
484 |
/>
|
485 |
</div>
|
486 |
+
<div className={styles.tableContainer}>
|
487 |
<Table
|
488 |
data={sourcesTableData}
|
489 |
columns={sourcesColumns}
|
|
|
495 |
</div>
|
496 |
</Container>
|
497 |
|
|
|
498 |
<Container heading="Types Distribution" headingLevel={3} withHeaderBorder withInternalPadding>
|
499 |
+
<div className={styles.chartSection}>
|
500 |
+
<div className={styles.chartContainer}>
|
501 |
<PieChart
|
502 |
data={typesChartData}
|
503 |
valueSelector={d => d.value}
|
|
|
507 |
showPercentageInLegend
|
508 |
/>
|
509 |
</div>
|
510 |
+
<div className={styles.tableContainer}>
|
511 |
<Table
|
512 |
data={typesTableData}
|
513 |
columns={typesColumns}
|
|
|
520 |
</Container>
|
521 |
</div>
|
522 |
) : (
|
523 |
+
<div className={styles.chartGrid}>
|
|
|
524 |
<Container heading="Model Performance" headingLevel={3} withHeaderBorder withInternalPadding>
|
525 |
+
<div className={styles.modelPerformance}>
|
526 |
+
<Table
|
527 |
+
data={modelsTableData}
|
528 |
+
columns={modelsColumns}
|
529 |
+
keySelector={numericIdSelector}
|
530 |
+
filtered={false}
|
531 |
+
pending={false}
|
532 |
+
/>
|
533 |
+
</div>
|
534 |
</Container>
|
535 |
</div>
|
536 |
)}
|
frontend/src/pages/AnalyticsPage/index.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export { default } from './AnalyticsPage';
|
frontend/src/pages/DemoPage.tsx
CHANGED
@@ -10,132 +10,51 @@ import {
|
|
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 |
-
|
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('');
|
@@ -149,7 +68,7 @@ export default function DemoPage() {
|
|
149 |
const [booleanValue, setBooleanValue] = useState(false);
|
150 |
const [segmentValue, setSegmentValue] = useState('option1');
|
151 |
|
152 |
-
|
153 |
const dummyOptions = [
|
154 |
{ key: 'option1', label: 'Option 1' },
|
155 |
{ key: 'option2', label: 'Option 2' },
|
@@ -168,12 +87,7 @@ export default function DemoPage() {
|
|
168 |
{ c_code: 'FR', label: 'France', r_code: 'EUR' },
|
169 |
];
|
170 |
|
171 |
-
|
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 },
|
@@ -182,14 +96,7 @@ export default function DemoPage() {
|
|
182 |
{ name: 'WFP', value: 10 },
|
183 |
];
|
184 |
|
185 |
-
|
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 },
|
@@ -203,47 +110,47 @@ export default function DemoPage() {
|
|
203 |
setTimeout(() => setLoading(false), 2000);
|
204 |
};
|
205 |
|
206 |
-
const handleTextChange = (value: string | undefined
|
207 |
setTextValue(value || '');
|
208 |
};
|
209 |
|
210 |
-
const handlePasswordChange = (value: string | undefined
|
211 |
setPasswordValue(value || '');
|
212 |
};
|
213 |
|
214 |
-
const handleNumberChange = (value: number | undefined
|
215 |
setNumberValue(value);
|
216 |
};
|
217 |
|
218 |
-
const handleDateChange = (value: string | undefined
|
219 |
setDateValue(value || '');
|
220 |
};
|
221 |
|
222 |
-
const handleSelectChange = (value: string | undefined
|
223 |
setSelectValue(value || '');
|
224 |
};
|
225 |
|
226 |
-
const handleMultiSelectChange = (value: string[]
|
227 |
setMultiSelectValue(value);
|
228 |
};
|
229 |
|
230 |
-
const handleCheckboxChange = (value: boolean
|
231 |
setCheckboxValue(value);
|
232 |
};
|
233 |
|
234 |
-
const handleRadioChange = (value: string
|
235 |
setRadioValue(value);
|
236 |
};
|
237 |
|
238 |
-
const handleSwitchChange = (value: boolean
|
239 |
setSwitchValue(value);
|
240 |
};
|
241 |
|
242 |
-
const handleBooleanChange = (value: boolean
|
243 |
setBooleanValue(value);
|
244 |
};
|
245 |
|
246 |
-
const handleSegmentChange = (value: string
|
247 |
setSegmentValue(value);
|
248 |
};
|
249 |
|
@@ -255,7 +162,7 @@ export default function DemoPage() {
|
|
255 |
<div className="space-y-6">
|
256 |
{/* Navigation Tabs */}
|
257 |
<div>
|
258 |
-
<
|
259 |
<NavigationTabList variant="primary">
|
260 |
<Button name="upload" variant="primary">Upload</Button>
|
261 |
<Button name="analytics" variant="secondary">Analytics</Button>
|
@@ -266,7 +173,7 @@ export default function DemoPage() {
|
|
266 |
|
267 |
{/* Top Banner */}
|
268 |
<div>
|
269 |
-
<
|
270 |
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
271 |
<div className="flex justify-between items-start">
|
272 |
<div>
|
@@ -282,7 +189,7 @@ export default function DemoPage() {
|
|
282 |
|
283 |
{/* Breadcrumbs */}
|
284 |
<div>
|
285 |
-
<
|
286 |
<nav className="flex" aria-label="Breadcrumb">
|
287 |
<ol className="flex items-center space-x-2">
|
288 |
<li>
|
@@ -311,7 +218,7 @@ export default function DemoPage() {
|
|
311 |
<div className="space-y-6">
|
312 |
{/* Buttons */}
|
313 |
<div>
|
314 |
-
<
|
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>
|
@@ -332,7 +239,7 @@ export default function DemoPage() {
|
|
332 |
|
333 |
{/* Icon Buttons */}
|
334 |
<div>
|
335 |
-
<
|
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 />
|
@@ -547,7 +454,7 @@ export default function DemoPage() {
|
|
547 |
name="radio"
|
548 |
value="option1"
|
549 |
checked={radioValue === 'option1'}
|
550 |
-
onChange={(e) => handleRadioChange(e.target.value
|
551 |
className="mr-2"
|
552 |
/>
|
553 |
<span className="text-sm">Option 1</span>
|
@@ -558,7 +465,7 @@ export default function DemoPage() {
|
|
558 |
name="radio"
|
559 |
value="option2"
|
560 |
checked={radioValue === 'option2'}
|
561 |
-
onChange={(e) => handleRadioChange(e.target.value
|
562 |
className="mr-2"
|
563 |
/>
|
564 |
<span className="text-sm">Option 2</span>
|
@@ -569,7 +476,7 @@ export default function DemoPage() {
|
|
569 |
name="radio"
|
570 |
value="option3"
|
571 |
checked={radioValue === 'option3'}
|
572 |
-
onChange={(e) => handleRadioChange(e.target.value
|
573 |
className="mr-2"
|
574 |
/>
|
575 |
<span className="text-sm">Option 3</span>
|
@@ -642,7 +549,7 @@ export default function DemoPage() {
|
|
642 |
valueSelector={(d) => d.value}
|
643 |
labelSelector={(d) => d.name}
|
644 |
keySelector={(d) => d.name}
|
645 |
-
colorSelector={(
|
646 |
showPercentageInLegend
|
647 |
/>
|
648 |
</div>
|
@@ -670,40 +577,7 @@ export default function DemoPage() {
|
|
670 |
</div>
|
671 |
</div>
|
672 |
|
673 |
-
|
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>
|
|
|
10 |
SearchMultiSelectInput,
|
11 |
TextArea,
|
12 |
Checkbox,
|
|
|
13 |
Switch,
|
14 |
DateInput,
|
15 |
NumberInput,
|
16 |
PasswordInput,
|
17 |
RawFileInput,
|
18 |
Container,
|
|
|
|
|
19 |
Spinner,
|
20 |
ProgressBar,
|
21 |
StackedProgressBar,
|
22 |
KeyFigure,
|
23 |
PieChart,
|
24 |
BarChart,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
IconButton,
|
26 |
ConfirmButton,
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
InputLabel,
|
28 |
InputHint,
|
|
|
29 |
InputSection,
|
30 |
BooleanInput,
|
31 |
BooleanOutput,
|
32 |
DateOutput,
|
|
|
33 |
NumberOutput,
|
34 |
TextOutput,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
Footer,
|
36 |
NavigationTabList,
|
|
|
|
|
|
|
|
|
|
|
37 |
SegmentInput,
|
38 |
+
BlockLoading,
|
|
|
|
|
|
|
39 |
} from '@ifrc-go/ui';
|
40 |
import {
|
41 |
UploadCloudLineIcon,
|
|
|
42 |
SearchLineIcon,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
ChevronDownLineIcon,
|
|
|
44 |
CloseLineIcon,
|
45 |
EditLineIcon,
|
46 |
DeleteBinLineIcon,
|
47 |
DownloadLineIcon,
|
48 |
ShareLineIcon,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
LocationIcon,
|
|
|
|
|
|
|
|
|
|
|
50 |
CheckLineIcon,
|
|
|
51 |
AlertLineIcon,
|
52 |
InfoIcon,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
} from '@ifrc-go/icons';
|
54 |
|
55 |
export default function DemoPage() {
|
56 |
const [showModal, setShowModal] = useState(false);
|
57 |
const [showPopup, setShowPopup] = useState(false);
|
|
|
58 |
const [loading, setLoading] = useState(false);
|
59 |
const [textValue, setTextValue] = useState('');
|
60 |
const [selectValue, setSelectValue] = useState('');
|
|
|
68 |
const [booleanValue, setBooleanValue] = useState(false);
|
69 |
const [segmentValue, setSegmentValue] = useState('option1');
|
70 |
|
71 |
+
|
72 |
const dummyOptions = [
|
73 |
{ key: 'option1', label: 'Option 1' },
|
74 |
{ key: 'option2', label: 'Option 2' },
|
|
|
87 |
{ c_code: 'FR', label: 'France', r_code: 'EUR' },
|
88 |
];
|
89 |
|
90 |
+
|
|
|
|
|
|
|
|
|
|
|
91 |
|
92 |
const dummyChartData = [
|
93 |
{ name: 'Red Cross', value: 45 },
|
|
|
96 |
{ name: 'WFP', value: 10 },
|
97 |
];
|
98 |
|
99 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
100 |
|
101 |
const dummyBarData = [
|
102 |
{ name: 'Q1', value: 100 },
|
|
|
110 |
setTimeout(() => setLoading(false), 2000);
|
111 |
};
|
112 |
|
113 |
+
const handleTextChange = (value: string | undefined) => {
|
114 |
setTextValue(value || '');
|
115 |
};
|
116 |
|
117 |
+
const handlePasswordChange = (value: string | undefined) => {
|
118 |
setPasswordValue(value || '');
|
119 |
};
|
120 |
|
121 |
+
const handleNumberChange = (value: number | undefined) => {
|
122 |
setNumberValue(value);
|
123 |
};
|
124 |
|
125 |
+
const handleDateChange = (value: string | undefined) => {
|
126 |
setDateValue(value || '');
|
127 |
};
|
128 |
|
129 |
+
const handleSelectChange = (value: string | undefined) => {
|
130 |
setSelectValue(value || '');
|
131 |
};
|
132 |
|
133 |
+
const handleMultiSelectChange = (value: string[]) => {
|
134 |
setMultiSelectValue(value);
|
135 |
};
|
136 |
|
137 |
+
const handleCheckboxChange = (value: boolean) => {
|
138 |
setCheckboxValue(value);
|
139 |
};
|
140 |
|
141 |
+
const handleRadioChange = (value: string) => {
|
142 |
setRadioValue(value);
|
143 |
};
|
144 |
|
145 |
+
const handleSwitchChange = (value: boolean) => {
|
146 |
setSwitchValue(value);
|
147 |
};
|
148 |
|
149 |
+
const handleBooleanChange = (value: boolean) => {
|
150 |
setBooleanValue(value);
|
151 |
};
|
152 |
|
153 |
+
const handleSegmentChange = (value: string) => {
|
154 |
setSegmentValue(value);
|
155 |
};
|
156 |
|
|
|
162 |
<div className="space-y-6">
|
163 |
{/* Navigation Tabs */}
|
164 |
<div>
|
165 |
+
<Heading level={3} className="mb-4">Navigation Tab List</Heading>
|
166 |
<NavigationTabList variant="primary">
|
167 |
<Button name="upload" variant="primary">Upload</Button>
|
168 |
<Button name="analytics" variant="secondary">Analytics</Button>
|
|
|
173 |
|
174 |
{/* Top Banner */}
|
175 |
<div>
|
176 |
+
<Heading level={3} className="mb-4">Top Banner</Heading>
|
177 |
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
178 |
<div className="flex justify-between items-start">
|
179 |
<div>
|
|
|
189 |
|
190 |
{/* Breadcrumbs */}
|
191 |
<div>
|
192 |
+
<Heading level={3} className="mb-4">Breadcrumbs</Heading>
|
193 |
<nav className="flex" aria-label="Breadcrumb">
|
194 |
<ol className="flex items-center space-x-2">
|
195 |
<li>
|
|
|
218 |
<div className="space-y-6">
|
219 |
{/* Buttons */}
|
220 |
<div>
|
221 |
+
<Heading level={3} className="mb-4">Buttons</Heading>
|
222 |
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
223 |
<Button name="primary" variant="primary">Primary Button</Button>
|
224 |
<Button name="secondary" variant="secondary">Secondary Button</Button>
|
|
|
239 |
|
240 |
{/* Icon Buttons */}
|
241 |
<div>
|
242 |
+
<Heading level={3} className="mb-4">Icon Buttons</Heading>
|
243 |
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
244 |
<IconButton name="upload" variant="primary" title="Upload" ariaLabel="Upload">
|
245 |
<UploadCloudLineIcon />
|
|
|
454 |
name="radio"
|
455 |
value="option1"
|
456 |
checked={radioValue === 'option1'}
|
457 |
+
onChange={(e) => handleRadioChange(e.target.value)}
|
458 |
className="mr-2"
|
459 |
/>
|
460 |
<span className="text-sm">Option 1</span>
|
|
|
465 |
name="radio"
|
466 |
value="option2"
|
467 |
checked={radioValue === 'option2'}
|
468 |
+
onChange={(e) => handleRadioChange(e.target.value)}
|
469 |
className="mr-2"
|
470 |
/>
|
471 |
<span className="text-sm">Option 2</span>
|
|
|
476 |
name="radio"
|
477 |
value="option3"
|
478 |
checked={radioValue === 'option3'}
|
479 |
+
onChange={(e) => handleRadioChange(e.target.value)}
|
480 |
className="mr-2"
|
481 |
/>
|
482 |
<span className="text-sm">Option 3</span>
|
|
|
549 |
valueSelector={(d) => d.value}
|
550 |
labelSelector={(d) => d.name}
|
551 |
keySelector={(d) => d.name}
|
552 |
+
colorSelector={() => '#dc2626'}
|
553 |
showPercentageInLegend
|
554 |
/>
|
555 |
</div>
|
|
|
577 |
</div>
|
578 |
</div>
|
579 |
|
580 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
581 |
|
582 |
{/* Lists */}
|
583 |
<div>
|
frontend/src/pages/DevPage.tsx
CHANGED
@@ -3,11 +3,9 @@ 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;
|
@@ -16,7 +14,6 @@ export default function DevPage() {
|
|
16 |
}>>([]);
|
17 |
const [selectedModel, setSelectedModel] = useState<string>('');
|
18 |
|
19 |
-
// Fetch models on component mount
|
20 |
useEffect(() => {
|
21 |
fetchModels();
|
22 |
}, []);
|
@@ -25,11 +22,8 @@ export default function DevPage() {
|
|
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)) {
|
@@ -42,7 +36,7 @@ export default function DevPage() {
|
|
42 |
}
|
43 |
})
|
44 |
.catch(err => {
|
45 |
-
|
46 |
});
|
47 |
};
|
48 |
|
@@ -59,7 +53,6 @@ export default function DevPage() {
|
|
59 |
});
|
60 |
|
61 |
if (response.ok) {
|
62 |
-
// Update local state
|
63 |
setAvailableModels(prev =>
|
64 |
prev.map(model =>
|
65 |
model.m_code === modelCode
|
@@ -67,19 +60,15 @@ export default function DevPage() {
|
|
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);
|
@@ -195,12 +184,10 @@ export default function DevPage() {
|
|
195 |
fetch('/api/models')
|
196 |
.then(r => r.json())
|
197 |
.then(data => {
|
198 |
-
|
199 |
-
alert('Check console for models API response');
|
200 |
})
|
201 |
.catch(err => {
|
202 |
-
|
203 |
-
alert('Models API error - check console');
|
204 |
});
|
205 |
}}
|
206 |
>
|
@@ -216,12 +203,10 @@ export default function DevPage() {
|
|
216 |
fetch(`/api/models/${selectedModel}/test`)
|
217 |
.then(r => r.json())
|
218 |
.then(data => {
|
219 |
-
|
220 |
-
alert('Check console for model test response');
|
221 |
})
|
222 |
.catch(err => {
|
223 |
-
|
224 |
-
alert('Model test error - check console');
|
225 |
});
|
226 |
}}
|
227 |
>
|
|
|
3 |
PageContainer, Heading, Button, Container,
|
4 |
} from '@ifrc-go/ui';
|
5 |
|
|
|
6 |
const SELECTED_MODEL_KEY = 'selectedVlmModel';
|
7 |
|
8 |
export default function DevPage() {
|
|
|
9 |
const [availableModels, setAvailableModels] = useState<Array<{
|
10 |
m_code: string;
|
11 |
label: string;
|
|
|
14 |
}>>([]);
|
15 |
const [selectedModel, setSelectedModel] = useState<string>('');
|
16 |
|
|
|
17 |
useEffect(() => {
|
18 |
fetchModels();
|
19 |
}, []);
|
|
|
22 |
fetch('/api/models')
|
23 |
.then(r => r.json())
|
24 |
.then(modelsData => {
|
|
|
|
|
25 |
setAvailableModels(modelsData.models || []);
|
26 |
|
|
|
27 |
const persistedModel = localStorage.getItem(SELECTED_MODEL_KEY);
|
28 |
if (modelsData.models && modelsData.models.length > 0) {
|
29 |
if (persistedModel && modelsData.models.find((m: any) => m.m_code === persistedModel && m.is_available)) {
|
|
|
36 |
}
|
37 |
})
|
38 |
.catch(err => {
|
39 |
+
|
40 |
});
|
41 |
};
|
42 |
|
|
|
53 |
});
|
54 |
|
55 |
if (response.ok) {
|
|
|
56 |
setAvailableModels(prev =>
|
57 |
prev.map(model =>
|
58 |
model.m_code === modelCode
|
|
|
60 |
: model
|
61 |
)
|
62 |
);
|
|
|
63 |
} else {
|
64 |
const errorData = await response.json();
|
|
|
65 |
alert(`Failed to toggle model availability: ${errorData.error || 'Unknown error'}`);
|
66 |
}
|
67 |
} catch (error) {
|
|
|
68 |
alert('Error toggling model availability');
|
69 |
}
|
70 |
};
|
71 |
|
|
|
72 |
const handleModelChange = (modelCode: string) => {
|
73 |
setSelectedModel(modelCode);
|
74 |
localStorage.setItem(SELECTED_MODEL_KEY, modelCode);
|
|
|
184 |
fetch('/api/models')
|
185 |
.then(r => r.json())
|
186 |
.then(data => {
|
187 |
+
alert('Models API response received successfully');
|
|
|
188 |
})
|
189 |
.catch(err => {
|
190 |
+
alert('Models API error occurred');
|
|
|
191 |
});
|
192 |
}}
|
193 |
>
|
|
|
203 |
fetch(`/api/models/${selectedModel}/test`)
|
204 |
.then(r => r.json())
|
205 |
.then(data => {
|
206 |
+
alert('Model test completed successfully');
|
|
|
207 |
})
|
208 |
.catch(err => {
|
209 |
+
alert('Model test failed');
|
|
|
210 |
});
|
211 |
}}
|
212 |
>
|
frontend/src/pages/ExplorePage.tsx
DELETED
@@ -1,365 +0,0 @@
|
|
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;
|
8 |
-
file_key: string;
|
9 |
-
image_url: string;
|
10 |
-
source: string;
|
11 |
-
type: string;
|
12 |
-
epsg: 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 |
-
|
23 |
-
export default function ExplorePage() {
|
24 |
-
const navigate = useNavigate();
|
25 |
-
const [maps, setMaps] = useState<MapOut[]>([]);
|
26 |
-
const [search, setSearch] = useState('');
|
27 |
-
const [srcFilter, setSrcFilter] = useState('');
|
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 => {
|
43 |
-
if (!r.ok) {
|
44 |
-
throw new Error(`HTTP ${r.status}: ${r.statusText}`);
|
45 |
-
}
|
46 |
-
return r.json();
|
47 |
-
})
|
48 |
-
.then(data => {
|
49 |
-
// Ensure data is an array
|
50 |
-
if (Array.isArray(data)) {
|
51 |
-
setMaps(data);
|
52 |
-
console.log(`Loaded ${data.length} maps`);
|
53 |
-
if (data.length > 0) {
|
54 |
-
console.log('Sample map data:', {
|
55 |
-
image_id: data[0].image_id,
|
56 |
-
source: data[0].source,
|
57 |
-
type: data[0].type,
|
58 |
-
countries: data[0].countries?.length || 0,
|
59 |
-
caption: data[0].caption ? 'has caption' : 'no caption'
|
60 |
-
});
|
61 |
-
}
|
62 |
-
} else {
|
63 |
-
console.error('Expected array from /api/images/, got:', data);
|
64 |
-
setMaps([]);
|
65 |
-
}
|
66 |
-
})
|
67 |
-
.catch(err => {
|
68 |
-
console.error('Failed to fetch maps:', err);
|
69 |
-
setMaps([]);
|
70 |
-
})
|
71 |
-
.finally(() => {
|
72 |
-
setIsLoadingFilters(false);
|
73 |
-
});
|
74 |
-
};
|
75 |
-
|
76 |
-
useEffect(() => {
|
77 |
-
fetchMaps();
|
78 |
-
}, []);
|
79 |
-
|
80 |
-
// Auto-refresh when component becomes visible (user navigates back)
|
81 |
-
useEffect(() => {
|
82 |
-
const handleVisibilityChange = () => {
|
83 |
-
if (!document.hidden) {
|
84 |
-
fetchMaps();
|
85 |
-
}
|
86 |
-
};
|
87 |
-
|
88 |
-
document.addEventListener('visibilitychange', handleVisibilityChange);
|
89 |
-
return () => {
|
90 |
-
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
91 |
-
};
|
92 |
-
}, []);
|
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,
|
123 |
-
types: typesData.length,
|
124 |
-
regions: regionsData.length,
|
125 |
-
countries: countriesData.length
|
126 |
-
});
|
127 |
-
|
128 |
-
if (Array.isArray(sourcesData)) {
|
129 |
-
setSources(sourcesData);
|
130 |
-
} else {
|
131 |
-
console.error('Expected array from /api/sources, got:', sourcesData);
|
132 |
-
setSources([]);
|
133 |
-
}
|
134 |
-
|
135 |
-
if (Array.isArray(typesData)) {
|
136 |
-
setTypes(typesData);
|
137 |
-
} else {
|
138 |
-
console.error('Expected array from /api/types, got:', typesData);
|
139 |
-
setTypes([]);
|
140 |
-
}
|
141 |
-
|
142 |
-
if (Array.isArray(regionsData)) {
|
143 |
-
setRegions(regionsData);
|
144 |
-
} else {
|
145 |
-
console.error('Expected array from /api/regions, got:', regionsData);
|
146 |
-
setRegions([]);
|
147 |
-
}
|
148 |
-
|
149 |
-
if (Array.isArray(countriesData)) {
|
150 |
-
setCountries(countriesData);
|
151 |
-
} else {
|
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)) {
|
171 |
-
console.warn('maps is not an array:', maps);
|
172 |
-
return [];
|
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 |
-
|
186 |
-
// Filter by source
|
187 |
-
const sourceMatch = !srcFilter || m.source === srcFilter;
|
188 |
-
|
189 |
-
// Filter by type
|
190 |
-
const typeMatch = !catFilter || m.type === catFilter;
|
191 |
-
|
192 |
-
// Filter by region (check if any country in the image belongs to the selected region)
|
193 |
-
const regionMatch = !regionFilter || (m.countries && m.countries.some(c => c.r_code === regionFilter));
|
194 |
-
|
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 |
-
);
|
365 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/ExplorePage/ExplorePage.module.css
ADDED
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.metadataTags {
|
2 |
+
display: flex;
|
3 |
+
flex-wrap: wrap;
|
4 |
+
gap: var(--go-ui-spacing-sm);
|
5 |
+
align-items: center;
|
6 |
+
}
|
7 |
+
|
8 |
+
.metadataTag {
|
9 |
+
padding: var(--go-ui-spacing-xs) var(--go-ui-spacing-sm);
|
10 |
+
background-color: var(--go-ui-color-red-10);
|
11 |
+
color: var(--go-ui-color-red-90);
|
12 |
+
font-size: var(--go-ui-font-size-xs);
|
13 |
+
border-radius: var(--go-ui-border-radius-md);
|
14 |
+
font-weight: var(--go-ui-font-weight-medium);
|
15 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-red-20);
|
16 |
+
transition: all var(--go-ui-duration-transition-fast) ease;
|
17 |
+
white-space: nowrap;
|
18 |
+
}
|
19 |
+
|
20 |
+
.metadataTag:hover {
|
21 |
+
background-color: var(--go-ui-color-red-20);
|
22 |
+
transform: translateY(-1px);
|
23 |
+
box-shadow: var(--go-ui-box-shadow-xs);
|
24 |
+
}
|
25 |
+
|
26 |
+
.metadataTagSource {
|
27 |
+
padding: var(--go-ui-spacing-xs) var(--go-ui-spacing-sm);
|
28 |
+
background-color: var(--go-ui-color-blue-10);
|
29 |
+
color: var(--go-ui-color-blue-90);
|
30 |
+
font-size: var(--go-ui-font-size-xs);
|
31 |
+
border-radius: var(--go-ui-border-radius-md);
|
32 |
+
font-weight: var(--go-ui-font-weight-medium);
|
33 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-blue-20);
|
34 |
+
white-space: nowrap;
|
35 |
+
}
|
36 |
+
|
37 |
+
.metadataTagType {
|
38 |
+
padding: var(--go-ui-spacing-xs) var(--go-ui-spacing-sm);
|
39 |
+
background-color: var(--go-ui-color-red-90);
|
40 |
+
color: var(--go-ui-color-white);
|
41 |
+
font-size: var(--go-ui-font-size-xs);
|
42 |
+
border-radius: var(--go-ui-border-radius-md);
|
43 |
+
font-weight: var(--go-ui-font-weight-medium);
|
44 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-red-90);
|
45 |
+
white-space: nowrap;
|
46 |
+
}
|
47 |
+
|
48 |
+
.mapItem {
|
49 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
50 |
+
border-radius: var(--go-ui-border-radius-lg);
|
51 |
+
padding: var(--go-ui-spacing-lg);
|
52 |
+
display: flex;
|
53 |
+
gap: var(--go-ui-spacing-lg);
|
54 |
+
cursor: pointer;
|
55 |
+
transition: all var(--go-ui-duration-transition-medium) ease;
|
56 |
+
background-color: var(--go-ui-color-white);
|
57 |
+
}
|
58 |
+
|
59 |
+
.mapItem:hover {
|
60 |
+
background-color: var(--go-ui-color-gray-10);
|
61 |
+
border-color: var(--go-ui-color-gray-30);
|
62 |
+
box-shadow: var(--go-ui-box-shadow-sm);
|
63 |
+
transform: translateY(-1px);
|
64 |
+
}
|
65 |
+
|
66 |
+
.mapItemImage {
|
67 |
+
background-color: var(--go-ui-color-gray-20);
|
68 |
+
display: flex;
|
69 |
+
align-items: center;
|
70 |
+
justify-content: center;
|
71 |
+
color: var(--go-ui-color-gray-60);
|
72 |
+
font-size: var(--go-ui-font-size-xs);
|
73 |
+
overflow: hidden;
|
74 |
+
border-radius: var(--go-ui-border-radius-md);
|
75 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
76 |
+
flex-shrink: 0;
|
77 |
+
}
|
78 |
+
|
79 |
+
.mapItemImage img {
|
80 |
+
width: 100%;
|
81 |
+
height: 100%;
|
82 |
+
object-fit: cover;
|
83 |
+
image-rendering: pixelated;
|
84 |
+
}
|
85 |
+
|
86 |
+
.mapItemContent {
|
87 |
+
flex: 1;
|
88 |
+
min-width: 0;
|
89 |
+
}
|
90 |
+
|
91 |
+
.mapItemTitle {
|
92 |
+
font-weight: var(--go-ui-font-weight-medium);
|
93 |
+
color: var(--go-ui-color-text);
|
94 |
+
margin-bottom: var(--go-ui-spacing-sm);
|
95 |
+
font-size: var(--go-ui-font-size-md);
|
96 |
+
line-height: var(--go-ui-line-height-md);
|
97 |
+
}
|
98 |
+
|
99 |
+
.mapItemMetadata {
|
100 |
+
margin-bottom: var(--go-ui-spacing-sm);
|
101 |
+
}
|
102 |
+
|
103 |
+
/* Responsive adjustments */
|
104 |
+
@media (max-width: 768px) {
|
105 |
+
.mapItem {
|
106 |
+
flex-direction: column;
|
107 |
+
gap: var(--go-ui-spacing-md);
|
108 |
+
}
|
109 |
+
|
110 |
+
.mapItemImage {
|
111 |
+
width: 100%;
|
112 |
+
height: 120px;
|
113 |
+
}
|
114 |
+
|
115 |
+
.metadataTags {
|
116 |
+
gap: var(--go-ui-spacing-xs);
|
117 |
+
}
|
118 |
+
|
119 |
+
.metadataTag,
|
120 |
+
.metadataTagSource,
|
121 |
+
.metadataTagType {
|
122 |
+
font-size: var(--go-ui-font-size-xs);
|
123 |
+
padding: var(--go-ui-spacing-2xs) var(--go-ui-spacing-xs);
|
124 |
+
}
|
125 |
+
}
|
frontend/src/pages/ExplorePage/ExplorePage.tsx
ADDED
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { PageContainer, TextInput, SelectInput, MultiSelectInput, Button, Container } 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 |
+
import styles from './ExplorePage.module.css';
|
6 |
+
|
7 |
+
interface CaptionWithImageOut {
|
8 |
+
cap_id: string;
|
9 |
+
image_id: string;
|
10 |
+
title: string;
|
11 |
+
prompt: string;
|
12 |
+
model: string;
|
13 |
+
schema_id: string;
|
14 |
+
raw_json: any;
|
15 |
+
generated: string;
|
16 |
+
edited?: string;
|
17 |
+
accuracy?: number;
|
18 |
+
context?: number;
|
19 |
+
usability?: number;
|
20 |
+
starred: boolean;
|
21 |
+
created_at?: string;
|
22 |
+
updated_at?: string;
|
23 |
+
file_key: string;
|
24 |
+
image_url: string;
|
25 |
+
source: string;
|
26 |
+
event_type: string;
|
27 |
+
epsg: string;
|
28 |
+
image_type: string;
|
29 |
+
countries: {c_code: string, label: string, r_code: string}[];
|
30 |
+
}
|
31 |
+
|
32 |
+
export default function ExplorePage() {
|
33 |
+
const navigate = useNavigate();
|
34 |
+
const [captions, setCaptions] = useState<CaptionWithImageOut[]>([]);
|
35 |
+
const [search, setSearch] = useState('');
|
36 |
+
const [srcFilter, setSrcFilter] = useState('');
|
37 |
+
const [catFilter, setCatFilter] = useState('');
|
38 |
+
const [regionFilter, setRegionFilter] = useState('');
|
39 |
+
const [countryFilter, setCountryFilter] = useState('');
|
40 |
+
const [showStarredOnly, setShowStarredOnly] = useState(false);
|
41 |
+
const [sources, setSources] = useState<{s_code: string, label: string}[]>([]);
|
42 |
+
const [types, setTypes] = useState<{t_code: string, label: string}[]>([]);
|
43 |
+
const [regions, setRegions] = useState<{r_code: string, label: string}[]>([]);
|
44 |
+
const [countries, setCountries] = useState<{c_code: string, label: string, r_code: string}[]>([]);
|
45 |
+
const [isLoadingFilters, setIsLoadingFilters] = useState(true);
|
46 |
+
|
47 |
+
const fetchCaptions = () => {
|
48 |
+
setIsLoadingFilters(true);
|
49 |
+
fetch('/api/captions')
|
50 |
+
.then(r => {
|
51 |
+
if (!r.ok) {
|
52 |
+
throw new Error(`HTTP ${r.status}: ${r.statusText}`);
|
53 |
+
}
|
54 |
+
return r.json();
|
55 |
+
})
|
56 |
+
.then(data => {
|
57 |
+
if (Array.isArray(data)) {
|
58 |
+
setCaptions(data);
|
59 |
+
|
60 |
+
} else {
|
61 |
+
|
62 |
+
setCaptions([]);
|
63 |
+
}
|
64 |
+
})
|
65 |
+
.catch(() => {
|
66 |
+
setCaptions([]);
|
67 |
+
})
|
68 |
+
.finally(() => {
|
69 |
+
setIsLoadingFilters(false);
|
70 |
+
});
|
71 |
+
};
|
72 |
+
|
73 |
+
useEffect(() => {
|
74 |
+
fetchCaptions();
|
75 |
+
}, []);
|
76 |
+
|
77 |
+
useEffect(() => {
|
78 |
+
const handleVisibilityChange = () => {
|
79 |
+
if (!document.hidden) {
|
80 |
+
fetchCaptions();
|
81 |
+
}
|
82 |
+
};
|
83 |
+
|
84 |
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
85 |
+
return () => {
|
86 |
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
87 |
+
};
|
88 |
+
}, []);
|
89 |
+
|
90 |
+
useEffect(() => {
|
91 |
+
|
92 |
+
setIsLoadingFilters(true);
|
93 |
+
|
94 |
+
Promise.all([
|
95 |
+
fetch('/api/sources').then(r => {
|
96 |
+
if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
|
97 |
+
return r.json();
|
98 |
+
}),
|
99 |
+
fetch('/api/types').then(r => {
|
100 |
+
if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
|
101 |
+
return r.json();
|
102 |
+
}),
|
103 |
+
fetch('/api/regions').then(r => {
|
104 |
+
if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
|
105 |
+
return r.json();
|
106 |
+
}),
|
107 |
+
fetch('/api/countries').then(r => {
|
108 |
+
if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
|
109 |
+
return r.json();
|
110 |
+
})
|
111 |
+
]).then(([sourcesData, typesData, regionsData, countriesData]) => {
|
112 |
+
|
113 |
+
|
114 |
+
if (Array.isArray(sourcesData)) {
|
115 |
+
setSources(sourcesData);
|
116 |
+
} else {
|
117 |
+
|
118 |
+
setSources([]);
|
119 |
+
}
|
120 |
+
|
121 |
+
if (Array.isArray(typesData)) {
|
122 |
+
setTypes(typesData);
|
123 |
+
} else {
|
124 |
+
|
125 |
+
setTypes([]);
|
126 |
+
}
|
127 |
+
|
128 |
+
if (Array.isArray(regionsData)) {
|
129 |
+
setRegions(regionsData);
|
130 |
+
} else {
|
131 |
+
|
132 |
+
setRegions([]);
|
133 |
+
}
|
134 |
+
|
135 |
+
if (Array.isArray(countriesData)) {
|
136 |
+
setCountries(countriesData);
|
137 |
+
} else {
|
138 |
+
|
139 |
+
setCountries([]);
|
140 |
+
}
|
141 |
+
|
142 |
+
setIsLoadingFilters(false);
|
143 |
+
}).catch(() => {
|
144 |
+
|
145 |
+
setSources([]);
|
146 |
+
setTypes([]);
|
147 |
+
setRegions([]);
|
148 |
+
setCountries([]);
|
149 |
+
setIsLoadingFilters(false);
|
150 |
+
});
|
151 |
+
}, []);
|
152 |
+
|
153 |
+
const filtered = useMemo(() => {
|
154 |
+
if (!Array.isArray(captions)) {
|
155 |
+
|
156 |
+
return [];
|
157 |
+
}
|
158 |
+
|
159 |
+
return captions.filter(c => {
|
160 |
+
const searchLower = search.toLowerCase();
|
161 |
+
const searchMatch = !search ||
|
162 |
+
c.file_key.toLowerCase().includes(searchLower) ||
|
163 |
+
c.source.toLowerCase().includes(searchLower) ||
|
164 |
+
c.event_type.toLowerCase().includes(searchLower) ||
|
165 |
+
c.title.toLowerCase().includes(searchLower) ||
|
166 |
+
(c.edited && c.edited.toLowerCase().includes(searchLower)) ||
|
167 |
+
(c.generated && c.generated.toLowerCase().includes(searchLower));
|
168 |
+
|
169 |
+
const sourceMatch = !srcFilter || c.source === srcFilter;
|
170 |
+
const typeMatch = !catFilter || c.event_type === catFilter;
|
171 |
+
const regionMatch = !regionFilter || (c.countries && c.countries.some(c => c.r_code === regionFilter));
|
172 |
+
const countryMatch = !countryFilter || (c.countries && c.countries.some(c => c.c_code === countryFilter));
|
173 |
+
const starredMatch = !showStarredOnly || c.starred === true;
|
174 |
+
|
175 |
+
return searchMatch && sourceMatch && typeMatch && regionMatch && countryMatch && starredMatch;
|
176 |
+
});
|
177 |
+
}, [captions, search, srcFilter, catFilter, regionFilter, countryFilter]);
|
178 |
+
|
179 |
+
return (
|
180 |
+
<PageContainer>
|
181 |
+
<Container
|
182 |
+
heading="Explore Examples"
|
183 |
+
headingLevel={2}
|
184 |
+
withHeaderBorder
|
185 |
+
withInternalPadding
|
186 |
+
className="max-w-7xl mx-auto"
|
187 |
+
>
|
188 |
+
<div className="space-y-6">
|
189 |
+
{/* Header Section */}
|
190 |
+
<div className="flex justify-between items-center">
|
191 |
+
<div>
|
192 |
+
<p className="text-gray-600 mt-1">Browse and search through uploaded crisis maps</p>
|
193 |
+
</div>
|
194 |
+
<div className="flex gap-2">
|
195 |
+
<Button
|
196 |
+
name="reference-examples"
|
197 |
+
variant={showStarredOnly ? "primary" : "secondary"}
|
198 |
+
onClick={() => setShowStarredOnly(!showStarredOnly)}
|
199 |
+
>
|
200 |
+
<StarLineIcon className="w-4 h-4" />
|
201 |
+
<span className="inline ml-2">Reference Examples</span>
|
202 |
+
</Button>
|
203 |
+
<Button
|
204 |
+
name="export"
|
205 |
+
variant="secondary"
|
206 |
+
onClick={() => {
|
207 |
+
const data = {
|
208 |
+
captions: captions,
|
209 |
+
filters: {
|
210 |
+
sources: sources,
|
211 |
+
types: types,
|
212 |
+
regions: regions,
|
213 |
+
countries: countries
|
214 |
+
},
|
215 |
+
timestamp: new Date().toISOString()
|
216 |
+
};
|
217 |
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
218 |
+
const url = URL.createObjectURL(blob);
|
219 |
+
const a = document.createElement('a');
|
220 |
+
a.href = url;
|
221 |
+
a.download = `promptaid-vision-captions-${new Date().toISOString().split('T')[0]}.json`;
|
222 |
+
document.body.appendChild(a);
|
223 |
+
a.click();
|
224 |
+
document.body.removeChild(a);
|
225 |
+
URL.revokeObjectURL(url);
|
226 |
+
}}
|
227 |
+
>
|
228 |
+
Export
|
229 |
+
</Button>
|
230 |
+
</div>
|
231 |
+
</div>
|
232 |
+
|
233 |
+
{/* Filters Bar */}
|
234 |
+
<Container heading="Search & Filters" headingLevel={3} withHeaderBorder withInternalPadding>
|
235 |
+
<div className="flex flex-wrap gap-4 items-center">
|
236 |
+
<TextInput
|
237 |
+
name="search"
|
238 |
+
placeholder="Search by filename, title…"
|
239 |
+
value={search}
|
240 |
+
onChange={(e) => setSearch(e || '')}
|
241 |
+
className="flex-1 min-w-[12rem]"
|
242 |
+
/>
|
243 |
+
|
244 |
+
<SelectInput
|
245 |
+
name="source"
|
246 |
+
placeholder={isLoadingFilters ? "Loading..." : "All Sources"}
|
247 |
+
options={sources}
|
248 |
+
value={srcFilter || null}
|
249 |
+
onChange={(v) => setSrcFilter(v as string || '')}
|
250 |
+
keySelector={(o) => o.s_code}
|
251 |
+
labelSelector={(o) => o.label}
|
252 |
+
required={false}
|
253 |
+
disabled={isLoadingFilters}
|
254 |
+
/>
|
255 |
+
|
256 |
+
<SelectInput
|
257 |
+
name="type"
|
258 |
+
placeholder={isLoadingFilters ? "Loading..." : "All Types"}
|
259 |
+
options={types}
|
260 |
+
value={catFilter || null}
|
261 |
+
onChange={(v) => setCatFilter(v as string || '')}
|
262 |
+
keySelector={(o) => o.t_code}
|
263 |
+
labelSelector={(o) => o.label}
|
264 |
+
required={false}
|
265 |
+
disabled={isLoadingFilters}
|
266 |
+
/>
|
267 |
+
|
268 |
+
<SelectInput
|
269 |
+
name="region"
|
270 |
+
placeholder={isLoadingFilters ? "Loading..." : "All Regions"}
|
271 |
+
options={regions}
|
272 |
+
value={regionFilter || null}
|
273 |
+
onChange={(v) => setRegionFilter(v as string || '')}
|
274 |
+
keySelector={(o) => o.r_code}
|
275 |
+
labelSelector={(o) => o.label}
|
276 |
+
required={false}
|
277 |
+
disabled={isLoadingFilters}
|
278 |
+
/>
|
279 |
+
|
280 |
+
<MultiSelectInput
|
281 |
+
name="country"
|
282 |
+
placeholder={isLoadingFilters ? "Loading..." : "All Countries"}
|
283 |
+
options={countries}
|
284 |
+
value={countryFilter ? [countryFilter] : []}
|
285 |
+
onChange={(v) => setCountryFilter((v as string[])[0] || '')}
|
286 |
+
keySelector={(o) => o.c_code}
|
287 |
+
labelSelector={(o) => o.label}
|
288 |
+
disabled={isLoadingFilters}
|
289 |
+
/>
|
290 |
+
</div>
|
291 |
+
</Container>
|
292 |
+
|
293 |
+
{/* Results Section */}
|
294 |
+
<Container heading="Results" headingLevel={3} withHeaderBorder withInternalPadding>
|
295 |
+
<div className="space-y-4">
|
296 |
+
<div className="flex justify-between items-center">
|
297 |
+
<p className="text-sm text-gray-600">
|
298 |
+
{filtered.length} of {captions.length} examples
|
299 |
+
</p>
|
300 |
+
</div>
|
301 |
+
|
302 |
+
{/* List */}
|
303 |
+
<div className="space-y-4">
|
304 |
+
{filtered.map(c => (
|
305 |
+
<div key={c.cap_id} className={styles.mapItem} onClick={() => navigate(`/map/${c.image_id}?captionId=${c.cap_id}`)}>
|
306 |
+
<div className={styles.mapItemImage} style={{ width: '120px', height: '80px' }}>
|
307 |
+
{c.image_url ? (
|
308 |
+
<img
|
309 |
+
src={c.image_url}
|
310 |
+
alt={c.file_key}
|
311 |
+
onError={(e) => {
|
312 |
+
const target = e.target as HTMLImageElement;
|
313 |
+
target.style.display = 'none';
|
314 |
+
target.parentElement!.innerHTML = 'Img';
|
315 |
+
}}
|
316 |
+
/>
|
317 |
+
) : (
|
318 |
+
'Img'
|
319 |
+
)}
|
320 |
+
</div>
|
321 |
+
<div className={styles.mapItemContent}>
|
322 |
+
<h3 className={styles.mapItemTitle}>
|
323 |
+
{c.title}
|
324 |
+
</h3>
|
325 |
+
<div className={styles.mapItemMetadata}>
|
326 |
+
<div className={styles.metadataTags}>
|
327 |
+
<span className={styles.metadataTagSource}>
|
328 |
+
{c.source}
|
329 |
+
</span>
|
330 |
+
<span className={styles.metadataTagType}>
|
331 |
+
{c.event_type}
|
332 |
+
</span>
|
333 |
+
<span className={styles.metadataTag}>
|
334 |
+
{c.epsg}
|
335 |
+
</span>
|
336 |
+
<span className={styles.metadataTag}>
|
337 |
+
{c.image_type}
|
338 |
+
</span>
|
339 |
+
</div>
|
340 |
+
</div>
|
341 |
+
</div>
|
342 |
+
</div>
|
343 |
+
))}
|
344 |
+
|
345 |
+
{!filtered.length && (
|
346 |
+
<div className="text-center py-12">
|
347 |
+
<p className="text-gray-500">No examples found.</p>
|
348 |
+
</div>
|
349 |
+
)}
|
350 |
+
</div>
|
351 |
+
</div>
|
352 |
+
</Container>
|
353 |
+
</div>
|
354 |
+
</Container>
|
355 |
+
</PageContainer>
|
356 |
+
);
|
357 |
+
}
|
frontend/src/pages/ExplorePage/index.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export { default } from './ExplorePage';
|
frontend/src/pages/MapDetailsPage/MapDetailPage.module.css
ADDED
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.backButton {
|
2 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
3 |
+
}
|
4 |
+
|
5 |
+
.imageContainer {
|
6 |
+
background-color: var(--go-ui-color-gray-20);
|
7 |
+
border-radius: var(--go-ui-border-radius-lg);
|
8 |
+
overflow: hidden;
|
9 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
10 |
+
box-shadow: var(--go-ui-box-shadow-sm);
|
11 |
+
transition: box-shadow var(--go-ui-duration-transition-medium) ease;
|
12 |
+
}
|
13 |
+
|
14 |
+
.imageContainer:hover {
|
15 |
+
box-shadow: var(--go-ui-box-shadow-md);
|
16 |
+
}
|
17 |
+
|
18 |
+
.imageContainer img {
|
19 |
+
width: 100%;
|
20 |
+
height: auto;
|
21 |
+
object-fit: contain;
|
22 |
+
image-rendering: pixelated;
|
23 |
+
display: block;
|
24 |
+
}
|
25 |
+
|
26 |
+
.imagePlaceholder {
|
27 |
+
width: 100%;
|
28 |
+
height: 16rem;
|
29 |
+
background-color: var(--go-ui-color-gray-30);
|
30 |
+
display: flex;
|
31 |
+
align-items: center;
|
32 |
+
justify-content: center;
|
33 |
+
color: var(--go-ui-color-gray-60);
|
34 |
+
font-size: var(--go-ui-font-size-sm);
|
35 |
+
font-weight: var(--go-ui-font-weight-medium);
|
36 |
+
}
|
37 |
+
|
38 |
+
.metadataTags {
|
39 |
+
display: flex;
|
40 |
+
flex-wrap: wrap;
|
41 |
+
gap: var(--go-ui-spacing-sm);
|
42 |
+
}
|
43 |
+
|
44 |
+
.metadataTag {
|
45 |
+
padding: var(--go-ui-spacing-xs) var(--go-ui-spacing-sm);
|
46 |
+
background-color: var(--go-ui-color-red-10);
|
47 |
+
color: var(--go-ui-color-red-90);
|
48 |
+
font-size: var(--go-ui-font-size-sm);
|
49 |
+
border-radius: var(--go-ui-border-radius-md);
|
50 |
+
font-weight: var(--go-ui-font-weight-medium);
|
51 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-red-20);
|
52 |
+
transition: all var(--go-ui-duration-transition-fast) ease;
|
53 |
+
}
|
54 |
+
|
55 |
+
.metadataTag:hover {
|
56 |
+
background-color: var(--go-ui-color-red-20);
|
57 |
+
transform: translateY(-1px);
|
58 |
+
box-shadow: var(--go-ui-box-shadow-xs);
|
59 |
+
}
|
60 |
+
|
61 |
+
.captionContainer {
|
62 |
+
padding: var(--go-ui-spacing-md);
|
63 |
+
background-color: var(--go-ui-color-gray-10);
|
64 |
+
border-radius: var(--go-ui-border-radius-md);
|
65 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
66 |
+
}
|
67 |
+
|
68 |
+
.captionText {
|
69 |
+
margin-bottom: var(--go-ui-spacing-md);
|
70 |
+
line-height: 1.6;
|
71 |
+
color: var(--go-ui-color-gray-900);
|
72 |
+
}
|
73 |
+
|
74 |
+
.captionText:last-child {
|
75 |
+
margin-bottom: 0;
|
76 |
+
}
|
77 |
+
|
78 |
+
.highlightedCaption {
|
79 |
+
background-color: var(--go-ui-color-blue-10);
|
80 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-blue-30);
|
81 |
+
border-radius: var(--go-ui-border-radius-md);
|
82 |
+
padding: var(--go-ui-spacing-md);
|
83 |
+
margin: var(--go-ui-spacing-md) 0;
|
84 |
+
}
|
85 |
+
|
86 |
+
.captionHighlight {
|
87 |
+
margin-top: var(--go-ui-spacing-sm);
|
88 |
+
font-size: var(--go-ui-font-size-sm);
|
89 |
+
color: var(--go-ui-color-blue-70);
|
90 |
+
font-style: italic;
|
91 |
+
}
|
92 |
+
|
93 |
+
.contributeSection {
|
94 |
+
margin-top: var(--go-ui-spacing-2xl);
|
95 |
+
padding-top: var(--go-ui-spacing-lg);
|
96 |
+
border-top: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
97 |
+
display: flex;
|
98 |
+
justify-content: center;
|
99 |
+
}
|
100 |
+
|
101 |
+
.contributeButton {
|
102 |
+
background-color: var(--go-ui-color-red-90);
|
103 |
+
color: var(--go-ui-color-white);
|
104 |
+
padding: var(--go-ui-spacing-sm) var(--go-ui-spacing-xl);
|
105 |
+
border-radius: var(--go-ui-border-radius-lg);
|
106 |
+
font-weight: var(--go-ui-font-weight-medium);
|
107 |
+
transition: all var(--go-ui-duration-transition-medium) ease;
|
108 |
+
box-shadow: var(--go-ui-box-shadow-sm);
|
109 |
+
border: none;
|
110 |
+
cursor: pointer;
|
111 |
+
font-size: var(--go-ui-font-size-md);
|
112 |
+
}
|
113 |
+
|
114 |
+
.contributeButton:hover {
|
115 |
+
background-color: var(--go-ui-color-red-hover);
|
116 |
+
transform: translateY(-2px);
|
117 |
+
box-shadow: var(--go-ui-box-shadow-md);
|
118 |
+
}
|
119 |
+
|
120 |
+
.contributeButton:active {
|
121 |
+
transform: translateY(0);
|
122 |
+
box-shadow: var(--go-ui-box-shadow-sm);
|
123 |
+
}
|
124 |
+
|
125 |
+
.gridLayout {
|
126 |
+
display: grid;
|
127 |
+
grid-template-columns: 1fr;
|
128 |
+
gap: var(--go-ui-spacing-2xl);
|
129 |
+
}
|
130 |
+
|
131 |
+
@media (min-width: 1024px) {
|
132 |
+
.gridLayout {
|
133 |
+
grid-template-columns: 1fr 1fr;
|
134 |
+
}
|
135 |
+
}
|
136 |
+
|
137 |
+
.detailsSection {
|
138 |
+
display: flex;
|
139 |
+
flex-direction: column;
|
140 |
+
gap: var(--go-ui-spacing-lg);
|
141 |
+
}
|
142 |
+
|
143 |
+
.loadingContainer {
|
144 |
+
display: flex;
|
145 |
+
align-items: center;
|
146 |
+
justify-content: center;
|
147 |
+
min-height: 400px;
|
148 |
+
color: var(--go-ui-color-gray-60);
|
149 |
+
font-size: var(--go-ui-font-size-lg);
|
150 |
+
font-weight: var(--go-ui-font-weight-medium);
|
151 |
+
}
|
152 |
+
|
153 |
+
.errorContainer {
|
154 |
+
display: flex;
|
155 |
+
align-items: center;
|
156 |
+
justify-content: center;
|
157 |
+
min-height: 400px;
|
158 |
+
color: var(--go-ui-color-negative);
|
159 |
+
font-size: var(--go-ui-font-size-lg);
|
160 |
+
font-weight: var(--go-ui-font-weight-medium);
|
161 |
+
}
|
162 |
+
|
163 |
+
|
164 |
+
|
165 |
+
/* Responsive adjustments */
|
166 |
+
@media (max-width: 768px) {
|
167 |
+
.gridLayout {
|
168 |
+
gap: var(--go-ui-spacing-lg);
|
169 |
+
}
|
170 |
+
|
171 |
+
.metadataTags {
|
172 |
+
gap: var(--go-ui-spacing-xs);
|
173 |
+
}
|
174 |
+
|
175 |
+
.metadataTag {
|
176 |
+
font-size: var(--go-ui-font-size-xs);
|
177 |
+
padding: var(--go-ui-spacing-2xs) var(--go-ui-spacing-xs);
|
178 |
+
}
|
179 |
+
|
180 |
+
.contributeButton {
|
181 |
+
padding: var(--go-ui-spacing-sm) var(--go-ui-spacing-lg);
|
182 |
+
font-size: var(--go-ui-font-size-sm);
|
183 |
+
}
|
184 |
+
}
|
frontend/src/pages/{MapDetailPage.tsx → MapDetailsPage/MapDetailPage.tsx}
RENAMED
@@ -1,29 +1,38 @@
|
|
1 |
-
import { PageContainer, Button } from '@ifrc-go/ui';
|
2 |
import { useState, useEffect } from 'react';
|
3 |
-
import { useParams, useNavigate } from 'react-router-dom';
|
|
|
4 |
|
5 |
interface MapOut {
|
6 |
image_id: string;
|
7 |
file_key: string;
|
8 |
image_url: string;
|
9 |
source: string;
|
10 |
-
|
11 |
epsg: string;
|
12 |
image_type: string;
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
14 |
title: string;
|
15 |
generated: string;
|
16 |
edited?: string;
|
17 |
-
|
|
|
18 |
}
|
19 |
|
20 |
export default function MapDetailPage() {
|
21 |
const { mapId } = useParams<{ mapId: string }>();
|
22 |
const navigate = useNavigate();
|
|
|
|
|
23 |
const [map, setMap] = useState<MapOut | null>(null);
|
24 |
const [loading, setLoading] = useState(true);
|
25 |
const [error, setError] = useState<string | null>(null);
|
26 |
-
|
27 |
|
28 |
useEffect(() => {
|
29 |
if (!mapId) {
|
@@ -32,7 +41,6 @@ export default function MapDetailPage() {
|
|
32 |
return;
|
33 |
}
|
34 |
|
35 |
-
// Fetch the specific map
|
36 |
fetch(`/api/images/${mapId}`)
|
37 |
.then(response => {
|
38 |
if (!response.ok) {
|
@@ -50,50 +58,23 @@ export default function MapDetailPage() {
|
|
50 |
});
|
51 |
}, [mapId]);
|
52 |
|
53 |
-
const handleContribute =
|
54 |
if (!map) return;
|
55 |
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
formData.append('source', map.source);
|
61 |
-
formData.append('type', map.type);
|
62 |
-
formData.append('epsg', map.epsg);
|
63 |
-
formData.append('image_type', map.image_type);
|
64 |
-
|
65 |
-
// We'll need to fetch the image and create a file from it
|
66 |
-
const imageResponse = await fetch(map.image_url);
|
67 |
-
const imageBlob = await imageResponse.blob();
|
68 |
-
const file = new File([imageBlob], map.file_key, { type: 'image/jpeg' });
|
69 |
-
formData.append('file', file);
|
70 |
-
|
71 |
-
const response = await fetch('/api/images/', {
|
72 |
-
method: 'POST',
|
73 |
-
body: formData,
|
74 |
-
});
|
75 |
-
|
76 |
-
if (!response.ok) {
|
77 |
-
throw new Error('Failed to contribute image');
|
78 |
-
}
|
79 |
-
|
80 |
-
const result = await response.json();
|
81 |
-
|
82 |
-
// Navigate to the upload page with the new map ID and step 2
|
83 |
-
navigate(`/upload?mapId=${result.image_id}&step=2`);
|
84 |
-
} catch (err) {
|
85 |
-
console.error('Contribution failed:', err);
|
86 |
-
alert('Failed to contribute image. Please try again.');
|
87 |
-
} finally {
|
88 |
-
setContributing(false);
|
89 |
-
}
|
90 |
};
|
91 |
|
92 |
if (loading) {
|
93 |
return (
|
94 |
<PageContainer>
|
95 |
-
<div className=
|
96 |
-
<div className="
|
|
|
|
|
|
|
97 |
</div>
|
98 |
</PageContainer>
|
99 |
);
|
@@ -102,8 +83,19 @@ export default function MapDetailPage() {
|
|
102 |
if (error || !map) {
|
103 |
return (
|
104 |
<PageContainer>
|
105 |
-
<div className=
|
106 |
-
<div className="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
</div>
|
108 |
</PageContainer>
|
109 |
);
|
@@ -111,85 +103,121 @@ export default function MapDetailPage() {
|
|
111 |
|
112 |
return (
|
113 |
<PageContainer>
|
114 |
-
<div className=
|
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=
|
126 |
{/* Image Section */}
|
127 |
-
<
|
128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
129 |
{map.image_url ? (
|
130 |
<img
|
131 |
src={map.image_url}
|
132 |
alt={map.file_key}
|
133 |
-
className="w-full h-auto object-contain"
|
134 |
-
style={{ imageRendering: 'pixelated' }}
|
135 |
/>
|
136 |
) : (
|
137 |
-
<div className=
|
138 |
No image available
|
139 |
</div>
|
140 |
)}
|
141 |
</div>
|
142 |
-
</
|
143 |
|
144 |
{/* Details Section */}
|
145 |
-
<div className=
|
146 |
-
<
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
|
|
|
|
|
|
152 |
</div>
|
153 |
-
</
|
154 |
-
|
155 |
-
<
|
156 |
-
|
157 |
-
|
158 |
-
|
|
|
|
|
|
|
|
|
|
|
159 |
{map.source}
|
160 |
</span>
|
161 |
-
<span className=
|
162 |
-
{map.
|
163 |
</span>
|
164 |
-
<span className=
|
165 |
{map.epsg}
|
166 |
</span>
|
167 |
-
<span className=
|
168 |
{map.image_type}
|
169 |
</span>
|
170 |
</div>
|
171 |
-
</
|
172 |
-
|
173 |
-
<
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
179 |
</div>
|
180 |
-
</
|
181 |
</div>
|
182 |
</div>
|
183 |
|
184 |
{/* Contribute Section */}
|
185 |
-
<div className=
|
186 |
<Button
|
187 |
name="contribute"
|
188 |
onClick={handleContribute}
|
189 |
-
|
190 |
-
className="bg-ifrcRed hover:bg-ifrcRed/90 text-white px-6 py-2 rounded-lg"
|
191 |
>
|
192 |
-
|
193 |
</Button>
|
194 |
</div>
|
195 |
</PageContainer>
|
|
|
1 |
+
import { PageContainer, Button, Container, Spinner } from '@ifrc-go/ui';
|
2 |
import { useState, useEffect } from 'react';
|
3 |
+
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
4 |
+
import styles from './MapDetailPage.module.css';
|
5 |
|
6 |
interface MapOut {
|
7 |
image_id: string;
|
8 |
file_key: string;
|
9 |
image_url: string;
|
10 |
source: string;
|
11 |
+
event_type: string;
|
12 |
epsg: string;
|
13 |
image_type: string;
|
14 |
+
countries?: Array<{
|
15 |
+
c_code: string;
|
16 |
+
label: string;
|
17 |
+
r_code: string;
|
18 |
+
}>;
|
19 |
+
captions?: Array<{
|
20 |
title: string;
|
21 |
generated: string;
|
22 |
edited?: string;
|
23 |
+
cap_id?: string;
|
24 |
+
}>;
|
25 |
}
|
26 |
|
27 |
export default function MapDetailPage() {
|
28 |
const { mapId } = useParams<{ mapId: string }>();
|
29 |
const navigate = useNavigate();
|
30 |
+
const [searchParams] = useSearchParams();
|
31 |
+
const captionId = searchParams.get('captionId');
|
32 |
const [map, setMap] = useState<MapOut | null>(null);
|
33 |
const [loading, setLoading] = useState(true);
|
34 |
const [error, setError] = useState<string | null>(null);
|
35 |
+
|
36 |
|
37 |
useEffect(() => {
|
38 |
if (!mapId) {
|
|
|
41 |
return;
|
42 |
}
|
43 |
|
|
|
44 |
fetch(`/api/images/${mapId}`)
|
45 |
.then(response => {
|
46 |
if (!response.ok) {
|
|
|
58 |
});
|
59 |
}, [mapId]);
|
60 |
|
61 |
+
const handleContribute = () => {
|
62 |
if (!map) return;
|
63 |
|
64 |
+
const url = captionId ?
|
65 |
+
`/upload?mapId=${map.image_id}&step=2&captionId=${captionId}` :
|
66 |
+
`/upload?mapId=${map.image_id}&step=2`;
|
67 |
+
navigate(url);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
};
|
69 |
|
70 |
if (loading) {
|
71 |
return (
|
72 |
<PageContainer>
|
73 |
+
<div className={styles.loadingContainer}>
|
74 |
+
<div className="flex flex-col items-center gap-4">
|
75 |
+
<Spinner className="text-ifrcRed" />
|
76 |
+
<div>Loading map details...</div>
|
77 |
+
</div>
|
78 |
</div>
|
79 |
</PageContainer>
|
80 |
);
|
|
|
83 |
if (error || !map) {
|
84 |
return (
|
85 |
<PageContainer>
|
86 |
+
<div className={styles.errorContainer}>
|
87 |
+
<div className="flex flex-col items-center gap-4 text-center">
|
88 |
+
<div className="text-4xl">⚠️</div>
|
89 |
+
<div className="text-xl font-semibold">Unable to load map</div>
|
90 |
+
<div>{error || 'Map not found'}</div>
|
91 |
+
<Button
|
92 |
+
name="back-to-explore"
|
93 |
+
variant="secondary"
|
94 |
+
onClick={() => navigate('/explore')}
|
95 |
+
>
|
96 |
+
Return to Explore
|
97 |
+
</Button>
|
98 |
+
</div>
|
99 |
</div>
|
100 |
</PageContainer>
|
101 |
);
|
|
|
103 |
|
104 |
return (
|
105 |
<PageContainer>
|
106 |
+
<div className={styles.backButton}>
|
107 |
<Button
|
108 |
name="back"
|
109 |
variant="secondary"
|
110 |
onClick={() => navigate('/explore')}
|
|
|
111 |
>
|
112 |
← Back to Explore
|
113 |
</Button>
|
114 |
</div>
|
115 |
|
116 |
+
<div className={styles.gridLayout}>
|
117 |
{/* Image Section */}
|
118 |
+
<Container
|
119 |
+
heading="Map Image"
|
120 |
+
headingLevel={3}
|
121 |
+
withHeaderBorder
|
122 |
+
withInternalPadding
|
123 |
+
spacing="comfortable"
|
124 |
+
>
|
125 |
+
<div className={styles.imageContainer}>
|
126 |
{map.image_url ? (
|
127 |
<img
|
128 |
src={map.image_url}
|
129 |
alt={map.file_key}
|
|
|
|
|
130 |
/>
|
131 |
) : (
|
132 |
+
<div className={styles.imagePlaceholder}>
|
133 |
No image available
|
134 |
</div>
|
135 |
)}
|
136 |
</div>
|
137 |
+
</Container>
|
138 |
|
139 |
{/* Details Section */}
|
140 |
+
<div className={styles.detailsSection}>
|
141 |
+
<Container
|
142 |
+
heading="Title"
|
143 |
+
headingLevel={3}
|
144 |
+
withHeaderBorder
|
145 |
+
withInternalPadding
|
146 |
+
spacing="comfortable"
|
147 |
+
>
|
148 |
+
<div className="text-gray-700">
|
149 |
+
{map.captions && map.captions.length > 0 ? map.captions[0].title : '— no title —'}
|
150 |
</div>
|
151 |
+
</Container>
|
152 |
+
|
153 |
+
<Container
|
154 |
+
heading="Metadata"
|
155 |
+
headingLevel={3}
|
156 |
+
withHeaderBorder
|
157 |
+
withInternalPadding
|
158 |
+
spacing="comfortable"
|
159 |
+
>
|
160 |
+
<div className={styles.metadataTags}>
|
161 |
+
<span className={styles.metadataTag}>
|
162 |
{map.source}
|
163 |
</span>
|
164 |
+
<span className={styles.metadataTag}>
|
165 |
+
{map.event_type}
|
166 |
</span>
|
167 |
+
<span className={styles.metadataTag}>
|
168 |
{map.epsg}
|
169 |
</span>
|
170 |
+
<span className={styles.metadataTag}>
|
171 |
{map.image_type}
|
172 |
</span>
|
173 |
</div>
|
174 |
+
</Container>
|
175 |
+
|
176 |
+
<Container
|
177 |
+
heading="Generated Caption"
|
178 |
+
headingLevel={3}
|
179 |
+
withHeaderBorder
|
180 |
+
withInternalPadding
|
181 |
+
spacing="comfortable"
|
182 |
+
>
|
183 |
+
<div className={styles.captionContainer}>
|
184 |
+
{map.captions && map.captions.length > 0 ? (
|
185 |
+
map.captions.map((caption, index) => (
|
186 |
+
<div
|
187 |
+
key={index}
|
188 |
+
className={`${styles.captionText} ${
|
189 |
+
captionId && map.captions && map.captions[index] &&
|
190 |
+
'cap_id' in map.captions[index] &&
|
191 |
+
map.captions[index].cap_id === captionId ?
|
192 |
+
styles.highlightedCaption : ''
|
193 |
+
}`}
|
194 |
+
>
|
195 |
+
<p>{caption.edited || caption.generated}</p>
|
196 |
+
{captionId && map.captions && map.captions[index] &&
|
197 |
+
'cap_id' in map.captions[index] &&
|
198 |
+
map.captions[index].cap_id === captionId && (
|
199 |
+
<div className={styles.captionHighlight}>
|
200 |
+
← This is the caption you selected
|
201 |
+
</div>
|
202 |
+
)}
|
203 |
+
</div>
|
204 |
+
))
|
205 |
+
) : (
|
206 |
+
<p>— no caption yet —</p>
|
207 |
+
)}
|
208 |
</div>
|
209 |
+
</Container>
|
210 |
</div>
|
211 |
</div>
|
212 |
|
213 |
{/* Contribute Section */}
|
214 |
+
<div className={styles.contributeSection}>
|
215 |
<Button
|
216 |
name="contribute"
|
217 |
onClick={handleContribute}
|
218 |
+
className={styles.contributeButton}
|
|
|
219 |
>
|
220 |
+
Contribute
|
221 |
</Button>
|
222 |
</div>
|
223 |
</PageContainer>
|
frontend/src/pages/MapDetailsPage/index.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export { default } from './MapDetailPage';
|
frontend/src/pages/UploadPage/UploadPage.module.css
ADDED
@@ -0,0 +1,495 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.uploadContainer {
|
2 |
+
margin: 0 auto;
|
3 |
+
max-width: var(--go-ui-width-screen-lg);
|
4 |
+
text-align: center;
|
5 |
+
padding: var(--go-ui-spacing-lg) var(--go-ui-spacing-md) var(--go-ui-spacing-2xl) var(--go-ui-spacing-md);
|
6 |
+
overflow-x: hidden;
|
7 |
+
}
|
8 |
+
|
9 |
+
.dropZone {
|
10 |
+
border: var(--go-ui-width-separator-thick) dashed var(--go-ui-color-gray-40);
|
11 |
+
background-color: var(--go-ui-color-gray-20);
|
12 |
+
border-radius: var(--go-ui-border-radius-xl);
|
13 |
+
padding: var(--go-ui-spacing-2xl) var(--go-ui-spacing-lg);
|
14 |
+
display: flex;
|
15 |
+
flex-direction: column;
|
16 |
+
align-items: center;
|
17 |
+
gap: var(--go-ui-spacing-lg);
|
18 |
+
transition: all var(--go-ui-duration-transition-medium) ease;
|
19 |
+
max-width: var(--go-ui-width-screen-md);
|
20 |
+
margin: 0 auto;
|
21 |
+
min-height: 250px;
|
22 |
+
justify-content: center;
|
23 |
+
}
|
24 |
+
|
25 |
+
.dropZone:hover {
|
26 |
+
background-color: var(--go-ui-color-gray-30);
|
27 |
+
border-color: var(--go-ui-color-gray-50);
|
28 |
+
}
|
29 |
+
|
30 |
+
.dropZone.hasFile {
|
31 |
+
background-color: var(--go-ui-color-white);
|
32 |
+
border-color: var(--go-ui-color-gray-30);
|
33 |
+
min-height: 300px;
|
34 |
+
padding: var(--go-ui-spacing-lg);
|
35 |
+
}
|
36 |
+
|
37 |
+
.dropZoneIcon {
|
38 |
+
width: 2.5rem;
|
39 |
+
height: 2.5rem;
|
40 |
+
color: var(--go-ui-color-red-90);
|
41 |
+
}
|
42 |
+
|
43 |
+
.dropZoneText {
|
44 |
+
font-size: var(--go-ui-font-size-sm);
|
45 |
+
color: var(--go-ui-color-gray-70);
|
46 |
+
text-align: center;
|
47 |
+
}
|
48 |
+
|
49 |
+
.dropZoneSubtext {
|
50 |
+
font-size: var(--go-ui-font-size-sm);
|
51 |
+
color: var(--go-ui-color-gray-50);
|
52 |
+
margin: var(--go-ui-spacing-md) 0;
|
53 |
+
}
|
54 |
+
|
55 |
+
.filePreview {
|
56 |
+
width: 100%;
|
57 |
+
max-width: 100%;
|
58 |
+
display: flex;
|
59 |
+
flex-direction: column;
|
60 |
+
align-items: center;
|
61 |
+
animation: fadeIn 0.3s ease-in-out;
|
62 |
+
}
|
63 |
+
|
64 |
+
.filePreviewImage {
|
65 |
+
position: relative;
|
66 |
+
max-width: 100%;
|
67 |
+
max-height: 20rem;
|
68 |
+
overflow: visible;
|
69 |
+
border-radius: var(--go-ui-border-radius-lg);
|
70 |
+
background-color: var(--go-ui-color-gray-20);
|
71 |
+
display: flex;
|
72 |
+
justify-content: center;
|
73 |
+
align-items: center;
|
74 |
+
padding: var(--go-ui-spacing-sm);
|
75 |
+
transition: all var(--go-ui-duration-transition-medium) ease;
|
76 |
+
}
|
77 |
+
|
78 |
+
.filePreviewImage:hover {
|
79 |
+
background-color: var(--go-ui-color-gray-30);
|
80 |
+
transform: translateY(-2px);
|
81 |
+
box-shadow: var(--go-ui-box-shadow-md);
|
82 |
+
}
|
83 |
+
|
84 |
+
.filePreviewImage img {
|
85 |
+
max-width: 100%;
|
86 |
+
max-height: 18rem;
|
87 |
+
width: auto;
|
88 |
+
height: auto;
|
89 |
+
object-fit: contain;
|
90 |
+
border-radius: var(--go-ui-border-radius-md);
|
91 |
+
box-shadow: var(--go-ui-box-shadow-sm);
|
92 |
+
transition: transform var(--go-ui-duration-transition-medium) ease;
|
93 |
+
}
|
94 |
+
|
95 |
+
.filePreviewImage img:hover {
|
96 |
+
transform: scale(1.02);
|
97 |
+
}
|
98 |
+
|
99 |
+
@keyframes fadeIn {
|
100 |
+
from {
|
101 |
+
opacity: 0;
|
102 |
+
transform: translateY(10px);
|
103 |
+
}
|
104 |
+
to {
|
105 |
+
opacity: 1;
|
106 |
+
transform: translateY(0);
|
107 |
+
}
|
108 |
+
}
|
109 |
+
|
110 |
+
.fileName {
|
111 |
+
font-size: var(--go-ui-font-size-sm);
|
112 |
+
font-weight: var(--go-ui-font-weight-medium);
|
113 |
+
color: var(--go-ui-color-gray-80);
|
114 |
+
margin-top: var(--go-ui-spacing-sm);
|
115 |
+
text-align: center;
|
116 |
+
}
|
117 |
+
|
118 |
+
.fileInfo {
|
119 |
+
font-size: var(--go-ui-font-size-xs);
|
120 |
+
color: var(--go-ui-color-gray-60);
|
121 |
+
margin-top: var(--go-ui-spacing-xs);
|
122 |
+
text-align: center;
|
123 |
+
}
|
124 |
+
|
125 |
+
.helpLink {
|
126 |
+
display: flex;
|
127 |
+
justify-content: center;
|
128 |
+
margin-top: var(--go-ui-spacing-md);
|
129 |
+
}
|
130 |
+
|
131 |
+
.helpLink a {
|
132 |
+
color: var(--go-ui-color-red-90);
|
133 |
+
font-size: var(--go-ui-font-size-xs);
|
134 |
+
transition: color var(--go-ui-duration-transition-fast) ease;
|
135 |
+
display: flex;
|
136 |
+
align-items: center;
|
137 |
+
gap: var(--go-ui-spacing-2xs);
|
138 |
+
}
|
139 |
+
|
140 |
+
.helpLink a:hover {
|
141 |
+
color: var(--go-ui-color-red-hover);
|
142 |
+
text-decoration: underline;
|
143 |
+
}
|
144 |
+
|
145 |
+
.loadingContainer {
|
146 |
+
display: flex;
|
147 |
+
flex-direction: column;
|
148 |
+
align-items: center;
|
149 |
+
gap: var(--go-ui-spacing-lg);
|
150 |
+
margin-top: var(--go-ui-spacing-2xl);
|
151 |
+
}
|
152 |
+
|
153 |
+
.loadingText {
|
154 |
+
color: var(--go-ui-color-gray-60);
|
155 |
+
}
|
156 |
+
|
157 |
+
.generateButtonContainer {
|
158 |
+
display: flex;
|
159 |
+
flex-direction: column;
|
160 |
+
align-items: center;
|
161 |
+
gap: var(--go-ui-spacing-lg);
|
162 |
+
margin-top: var(--go-ui-spacing-2xl);
|
163 |
+
}
|
164 |
+
|
165 |
+
.uploadedMapContainer {
|
166 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
167 |
+
}
|
168 |
+
|
169 |
+
.uploadedMapImage {
|
170 |
+
width: 100%;
|
171 |
+
max-width: var(--go-ui-width-screen-lg);
|
172 |
+
max-height: 20rem;
|
173 |
+
overflow: visible;
|
174 |
+
background-color: var(--go-ui-color-gray-20);
|
175 |
+
border-radius: var(--go-ui-border-radius-lg);
|
176 |
+
box-shadow: var(--go-ui-box-shadow-sm);
|
177 |
+
display: flex;
|
178 |
+
justify-content: center;
|
179 |
+
align-items: center;
|
180 |
+
padding: var(--go-ui-spacing-sm);
|
181 |
+
transition: all var(--go-ui-duration-transition-medium) ease;
|
182 |
+
}
|
183 |
+
|
184 |
+
.uploadedMapImage:hover {
|
185 |
+
background-color: var(--go-ui-color-gray-30);
|
186 |
+
transform: translateY(-2px);
|
187 |
+
box-shadow: var(--go-ui-box-shadow-md);
|
188 |
+
}
|
189 |
+
|
190 |
+
.uploadedMapImage img {
|
191 |
+
max-width: 100%;
|
192 |
+
max-height: 18rem;
|
193 |
+
width: auto;
|
194 |
+
height: auto;
|
195 |
+
object-fit: contain;
|
196 |
+
border-radius: var(--go-ui-border-radius-md);
|
197 |
+
box-shadow: var(--go-ui-box-shadow-sm);
|
198 |
+
transition: transform var(--go-ui-duration-transition-medium) ease;
|
199 |
+
}
|
200 |
+
|
201 |
+
.uploadedMapImage img:hover {
|
202 |
+
transform: scale(1.02);
|
203 |
+
}
|
204 |
+
|
205 |
+
.formSection {
|
206 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
207 |
+
}
|
208 |
+
|
209 |
+
.formGrid {
|
210 |
+
display: grid;
|
211 |
+
gap: var(--go-ui-spacing-lg);
|
212 |
+
grid-template-columns: 1fr;
|
213 |
+
text-align: left;
|
214 |
+
}
|
215 |
+
|
216 |
+
@media (min-width: 1024px) {
|
217 |
+
.formGrid {
|
218 |
+
grid-template-columns: 1fr 1fr;
|
219 |
+
}
|
220 |
+
}
|
221 |
+
|
222 |
+
.titleField {
|
223 |
+
grid-column: 1 / -1;
|
224 |
+
}
|
225 |
+
|
226 |
+
.ratingSection {
|
227 |
+
text-align: left;
|
228 |
+
}
|
229 |
+
|
230 |
+
.ratingDescription {
|
231 |
+
color: var(--go-ui-color-gray-70);
|
232 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
233 |
+
}
|
234 |
+
|
235 |
+
.ratingSlider {
|
236 |
+
margin-top: var(--go-ui-spacing-lg);
|
237 |
+
display: flex;
|
238 |
+
align-items: center;
|
239 |
+
gap: var(--go-ui-spacing-sm);
|
240 |
+
}
|
241 |
+
|
242 |
+
.ratingLabel {
|
243 |
+
display: block;
|
244 |
+
font-size: var(--go-ui-font-size-sm);
|
245 |
+
font-weight: var(--go-ui-font-weight-medium);
|
246 |
+
text-transform: capitalize;
|
247 |
+
width: 5rem;
|
248 |
+
flex-shrink: 0;
|
249 |
+
}
|
250 |
+
|
251 |
+
.ratingInput {
|
252 |
+
width: 100%;
|
253 |
+
accent-color: var(--go-ui-color-red-90);
|
254 |
+
}
|
255 |
+
|
256 |
+
.ratingValue {
|
257 |
+
margin-left: var(--go-ui-spacing-sm);
|
258 |
+
width: 2.5rem;
|
259 |
+
text-align: right;
|
260 |
+
tabular-nums: true;
|
261 |
+
flex-shrink: 0;
|
262 |
+
font-size: var(--go-ui-font-size-sm);
|
263 |
+
color: var(--go-ui-color-gray-70);
|
264 |
+
}
|
265 |
+
|
266 |
+
.submitSection {
|
267 |
+
display: flex;
|
268 |
+
justify-content: center;
|
269 |
+
gap: var(--go-ui-spacing-md);
|
270 |
+
margin-top: var(--go-ui-spacing-2xl);
|
271 |
+
flex-wrap: wrap;
|
272 |
+
}
|
273 |
+
|
274 |
+
/* Success page styles */
|
275 |
+
.successContainer {
|
276 |
+
text-align: center;
|
277 |
+
padding: var(--go-ui-spacing-2xl);
|
278 |
+
}
|
279 |
+
|
280 |
+
.successHeading {
|
281 |
+
color: var(--go-ui-color-green-90);
|
282 |
+
margin-bottom: var(--go-ui-spacing-lg);
|
283 |
+
}
|
284 |
+
|
285 |
+
.successText {
|
286 |
+
color: var(--go-ui-color-gray-700);
|
287 |
+
margin-bottom: var(--go-ui-spacing-xl);
|
288 |
+
font-size: var(--go-ui-font-size-lg);
|
289 |
+
}
|
290 |
+
|
291 |
+
.successButton {
|
292 |
+
display: flex;
|
293 |
+
justify-content: center;
|
294 |
+
}
|
295 |
+
|
296 |
+
/* View Full Size Button */
|
297 |
+
.viewFullSizeButton {
|
298 |
+
display: flex;
|
299 |
+
justify-content: center;
|
300 |
+
margin-top: var(--go-ui-spacing-md);
|
301 |
+
padding-top: var(--go-ui-spacing-md);
|
302 |
+
border-top: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
303 |
+
}
|
304 |
+
|
305 |
+
/* Full Size Modal */
|
306 |
+
.fullSizeModalOverlay {
|
307 |
+
position: fixed;
|
308 |
+
top: 0;
|
309 |
+
left: 0;
|
310 |
+
right: 0;
|
311 |
+
bottom: 0;
|
312 |
+
background-color: rgba(0, 0, 0, 0.8);
|
313 |
+
display: flex;
|
314 |
+
justify-content: center;
|
315 |
+
align-items: center;
|
316 |
+
z-index: 1000;
|
317 |
+
padding: var(--go-ui-spacing-lg);
|
318 |
+
}
|
319 |
+
|
320 |
+
.fullSizeModalContent {
|
321 |
+
background-color: var(--go-ui-color-white);
|
322 |
+
border-radius: var(--go-ui-border-radius-lg);
|
323 |
+
max-width: 95vw;
|
324 |
+
max-height: 95vh;
|
325 |
+
overflow: hidden;
|
326 |
+
box-shadow: var(--go-ui-box-shadow-xl);
|
327 |
+
display: flex;
|
328 |
+
flex-direction: column;
|
329 |
+
}
|
330 |
+
|
331 |
+
.fullSizeModalHeader {
|
332 |
+
display: flex;
|
333 |
+
justify-content: space-between;
|
334 |
+
align-items: center;
|
335 |
+
padding: var(--go-ui-spacing-lg);
|
336 |
+
border-bottom: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
337 |
+
background-color: var(--go-ui-color-gray-10);
|
338 |
+
}
|
339 |
+
|
340 |
+
.fullSizeModalTitle {
|
341 |
+
margin: 0;
|
342 |
+
font-size: var(--go-ui-font-size-lg);
|
343 |
+
font-weight: var(--go-ui-font-weight-semibold);
|
344 |
+
color: var(--go-ui-color-gray-900);
|
345 |
+
}
|
346 |
+
|
347 |
+
.fullSizeModalImage {
|
348 |
+
flex: 1;
|
349 |
+
display: flex;
|
350 |
+
justify-content: center;
|
351 |
+
align-items: center;
|
352 |
+
padding: var(--go-ui-spacing-lg);
|
353 |
+
overflow: auto;
|
354 |
+
}
|
355 |
+
|
356 |
+
.fullSizeModalImage img {
|
357 |
+
max-width: 100%;
|
358 |
+
max-height: 100%;
|
359 |
+
object-fit: contain;
|
360 |
+
border-radius: var(--go-ui-border-radius-md);
|
361 |
+
box-shadow: var(--go-ui-box-shadow-md);
|
362 |
+
}
|
363 |
+
|
364 |
+
/* Responsive adjustments for modal */
|
365 |
+
@media (max-width: 768px) {
|
366 |
+
.fullSizeModalOverlay {
|
367 |
+
padding: var(--go-ui-spacing-sm);
|
368 |
+
}
|
369 |
+
|
370 |
+
.fullSizeModalContent {
|
371 |
+
max-width: 100vw;
|
372 |
+
max-height: 100vh;
|
373 |
+
}
|
374 |
+
|
375 |
+
.fullSizeModalHeader {
|
376 |
+
padding: var(--go-ui-spacing-md);
|
377 |
+
}
|
378 |
+
|
379 |
+
.fullSizeModalImage {
|
380 |
+
padding: var(--go-ui-spacing-md);
|
381 |
+
}
|
382 |
+
}
|
383 |
+
|
384 |
+
.confirmSection {
|
385 |
+
display: flex;
|
386 |
+
justify-content: center;
|
387 |
+
gap: var(--go-ui-spacing-md);
|
388 |
+
margin-top: var(--go-ui-spacing-xl);
|
389 |
+
padding-top: var(--go-ui-spacing-lg);
|
390 |
+
border-top: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
391 |
+
}
|
392 |
+
|
393 |
+
.step2Layout {
|
394 |
+
display: grid;
|
395 |
+
grid-template-columns: 1fr 1fr;
|
396 |
+
gap: var(--go-ui-spacing-2xl);
|
397 |
+
align-items: start;
|
398 |
+
}
|
399 |
+
|
400 |
+
.mapColumn {
|
401 |
+
position: sticky;
|
402 |
+
top: var(--go-ui-spacing-lg);
|
403 |
+
}
|
404 |
+
|
405 |
+
.contentColumn {
|
406 |
+
display: flex;
|
407 |
+
flex-direction: column;
|
408 |
+
gap: var(--go-ui-spacing-lg);
|
409 |
+
}
|
410 |
+
|
411 |
+
.contentColumn .formGrid {
|
412 |
+
display: grid;
|
413 |
+
gap: var(--go-ui-spacing-lg);
|
414 |
+
grid-template-columns: 1fr;
|
415 |
+
text-align: left;
|
416 |
+
}
|
417 |
+
|
418 |
+
/* Responsive adjustments */
|
419 |
+
@media (max-width: 1024px) {
|
420 |
+
.step2Layout {
|
421 |
+
grid-template-columns: 1fr;
|
422 |
+
gap: var(--go-ui-spacing-lg);
|
423 |
+
}
|
424 |
+
|
425 |
+
.mapColumn {
|
426 |
+
position: static;
|
427 |
+
}
|
428 |
+
}
|
429 |
+
|
430 |
+
@media (max-width: 768px) {
|
431 |
+
.uploadContainer {
|
432 |
+
padding: var(--go-ui-spacing-md) var(--go-ui-spacing-sm) var(--go-ui-spacing-xl) var(--go-ui-spacing-sm);
|
433 |
+
}
|
434 |
+
|
435 |
+
.dropZone {
|
436 |
+
padding: var(--go-ui-spacing-lg) var(--go-ui-spacing-md);
|
437 |
+
min-height: 200px;
|
438 |
+
}
|
439 |
+
|
440 |
+
.dropZone.hasFile {
|
441 |
+
min-height: 250px;
|
442 |
+
padding: var(--go-ui-spacing-md);
|
443 |
+
}
|
444 |
+
|
445 |
+
.filePreviewImage {
|
446 |
+
max-width: 100%;
|
447 |
+
max-height: 15rem;
|
448 |
+
padding: var(--go-ui-spacing-xs);
|
449 |
+
}
|
450 |
+
|
451 |
+
.filePreviewImage img {
|
452 |
+
max-height: 13rem;
|
453 |
+
}
|
454 |
+
|
455 |
+
.ratingSlider {
|
456 |
+
gap: var(--go-ui-spacing-xs);
|
457 |
+
}
|
458 |
+
|
459 |
+
.ratingLabel {
|
460 |
+
width: 4rem;
|
461 |
+
font-size: var(--go-ui-font-size-xs);
|
462 |
+
}
|
463 |
+
|
464 |
+
.ratingValue {
|
465 |
+
width: 2rem;
|
466 |
+
font-size: var(--go-ui-font-size-xs);
|
467 |
+
}
|
468 |
+
}
|
469 |
+
|
470 |
+
@media (max-width: 480px) {
|
471 |
+
.dropZone {
|
472 |
+
padding: var(--go-ui-spacing-md) var(--go-ui-spacing-sm);
|
473 |
+
min-height: 180px;
|
474 |
+
}
|
475 |
+
|
476 |
+
.dropZone.hasFile {
|
477 |
+
min-height: 220px;
|
478 |
+
}
|
479 |
+
|
480 |
+
.filePreviewImage {
|
481 |
+
max-height: 12rem;
|
482 |
+
}
|
483 |
+
|
484 |
+
.filePreviewImage img {
|
485 |
+
max-height: 10rem;
|
486 |
+
}
|
487 |
+
}
|
488 |
+
|
489 |
+
.metadataSectionCard {
|
490 |
+
background-color: var(--go-ui-color-white);
|
491 |
+
border: var(--go-ui-width-separator-thin) solid var(--go-ui-color-separator);
|
492 |
+
border-radius: var(--go-ui-border-radius-lg);
|
493 |
+
padding: var(--go-ui-spacing-lg);
|
494 |
+
box-shadow: var(--go-ui-box-shadow-xs);
|
495 |
+
}
|
frontend/src/pages/{UploadPage.tsx → UploadPage/UploadPage.tsx}
RENAMED
@@ -10,48 +10,47 @@ import {
|
|
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 |
|
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 [
|
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}[]>([]);
|
35 |
const [types, setTypes] = useState<{t_code: string, label: string}[]>([]);
|
36 |
const [spatialReferences, setSpatialReferences] = useState<{epsg: string, srid: string, proj4: string, wkt: string}[]>([]);
|
37 |
const [imageTypes, setImageTypes] = useState<{image_type: string, label: string}[]>([]);
|
38 |
const [countriesOptions, setCountriesOptions] = useState<{c_code: string, label: string, r_code: 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;
|
45 |
uploadedImageIdRef.current = uploadedImageId;
|
46 |
|
47 |
-
// Wrapper functions to handle OptionKey to string conversion
|
48 |
const handleSourceChange = (value: any) => setSource(String(value));
|
49 |
-
const
|
50 |
const handleEpsgChange = (value: any) => setEpsg(String(value));
|
51 |
const handleImageTypeChange = (value: any) => setImageType(String(value));
|
52 |
const handleCountriesChange = (value: any) => setCountries(Array.isArray(value) ? value.map(String) : []);
|
53 |
|
54 |
-
|
|
|
|
|
|
|
55 |
useEffect(() => {
|
56 |
Promise.all([
|
57 |
fetch('/api/sources').then(r => r.json()),
|
@@ -67,9 +66,8 @@ export default function UploadPage() {
|
|
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)
|
73 |
if (spatialData.length > 0) setEpsg(spatialData[0].epsg);
|
74 |
if (imageTypesData.length > 0) setImageType(imageTypesData[0].image_type);
|
75 |
});
|
@@ -95,44 +93,50 @@ export default function UploadPage() {
|
|
95 |
const [imageUrl, setImageUrl] = useState<string|null>(null);
|
96 |
const [draft, setDraft] = useState('');
|
97 |
|
98 |
-
// Handle URL parameters for direct step 2 navigation
|
99 |
useEffect(() => {
|
100 |
const mapId = searchParams.get('mapId');
|
101 |
const stepParam = searchParams.get('step');
|
|
|
102 |
|
103 |
if (mapId && stepParam === '2') {
|
104 |
-
// Load the map data and start at step 2
|
105 |
fetch(`/api/images/${mapId}`)
|
106 |
.then(response => response.json())
|
107 |
.then(mapData => {
|
108 |
setImageUrl(mapData.image_url);
|
109 |
setSource(mapData.source);
|
110 |
-
|
111 |
setEpsg(mapData.epsg);
|
112 |
setImageType(mapData.image_type);
|
113 |
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
})
|
134 |
.catch(err => {
|
135 |
-
console.error('Failed to load map data:', err);
|
136 |
alert('Failed to load map data. Please try again.');
|
137 |
});
|
138 |
}
|
@@ -155,9 +159,9 @@ export default function UploadPage() {
|
|
155 |
usability: 50,
|
156 |
});
|
157 |
|
|
|
158 |
|
159 |
|
160 |
-
/* ---- drag-and-drop + file-picker handlers -------------------------- */
|
161 |
const onDrop = useCallback((e: DragEvent<HTMLDivElement>) => {
|
162 |
e.preventDefault();
|
163 |
const dropped = e.dataTransfer.files?.[0];
|
@@ -168,7 +172,6 @@ export default function UploadPage() {
|
|
168 |
if (file) setFile(file);
|
169 |
}, []);
|
170 |
|
171 |
-
// blob URL / preview
|
172 |
useEffect(() => {
|
173 |
if (!file) {
|
174 |
setPreview(null);
|
@@ -181,21 +184,20 @@ export default function UploadPage() {
|
|
181 |
|
182 |
|
183 |
async function readJsonSafely(res: Response): Promise<any> {
|
184 |
-
const text = await res.text();
|
185 |
try {
|
186 |
-
return text ? JSON.parse(text) : {};
|
187 |
} catch {
|
188 |
-
return { error: text };
|
189 |
}
|
190 |
}
|
191 |
|
192 |
function handleApiError(err: any, operation: string) {
|
193 |
-
|
194 |
const message = err.message || `Failed to ${operation.toLowerCase()}`;
|
195 |
alert(message);
|
196 |
}
|
197 |
|
198 |
-
/* ---- generate handler --------------------------------------------- */
|
199 |
async function handleGenerate() {
|
200 |
if (!file) return;
|
201 |
|
@@ -204,7 +206,7 @@ export default function UploadPage() {
|
|
204 |
const fd = new FormData();
|
205 |
fd.append('file', file);
|
206 |
fd.append('source', source);
|
207 |
-
fd.append('
|
208 |
fd.append('epsg', epsg);
|
209 |
fd.append('image_type', imageType);
|
210 |
countries.forEach((c) => fd.append('countries', c));
|
@@ -215,7 +217,6 @@ export default function UploadPage() {
|
|
215 |
}
|
216 |
|
217 |
try {
|
218 |
-
/* 1) upload */
|
219 |
const mapRes = await fetch('/api/images/', { method: 'POST', body: fd });
|
220 |
const mapJson = await readJsonSafely(mapRes);
|
221 |
if (!mapRes.ok) throw new Error(mapJson.error || 'Upload failed');
|
@@ -225,7 +226,6 @@ export default function UploadPage() {
|
|
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 |
{
|
@@ -243,26 +243,20 @@ export default function UploadPage() {
|
|
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)
|
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 |
-
|
266 |
} catch (err) {
|
267 |
handleApiError(err, 'Upload');
|
268 |
} finally {
|
@@ -270,15 +264,13 @@ export default function UploadPage() {
|
|
270 |
}
|
271 |
}
|
272 |
|
273 |
-
/* ---- submit handler --------------------------------------------- */
|
274 |
async function handleSubmit() {
|
275 |
if (!captionId) return alert("No caption to submit");
|
276 |
|
277 |
try {
|
278 |
-
// 1. Update image metadata
|
279 |
const metadataBody = {
|
280 |
source: source,
|
281 |
-
|
282 |
epsg: epsg,
|
283 |
image_type: imageType,
|
284 |
countries: countries,
|
@@ -291,10 +283,9 @@ export default function UploadPage() {
|
|
291 |
const metadataJson = await readJsonSafely(metadataRes);
|
292 |
if (!metadataRes.ok) throw new Error(metadataJson.error || "Metadata update failed");
|
293 |
|
294 |
-
// 2. Update caption
|
295 |
const captionBody = {
|
296 |
title: title,
|
297 |
-
edited: draft || '',
|
298 |
accuracy: scores.accuracy,
|
299 |
context: scores.context,
|
300 |
usability: scores.usability,
|
@@ -307,42 +298,114 @@ export default function UploadPage() {
|
|
307 |
const captionJson = await readJsonSafely(captionRes);
|
308 |
if (!captionRes.ok) throw new Error(captionJson.error || "Caption update failed");
|
309 |
|
310 |
-
// Clear uploaded IDs since submission was successful
|
311 |
setUploadedImageId(null);
|
312 |
-
|
313 |
} catch (err) {
|
314 |
handleApiError(err, 'Submit');
|
315 |
}
|
316 |
}
|
317 |
|
318 |
-
/* ---- delete handler --------------------------------------------- */
|
319 |
async function handleDelete() {
|
320 |
-
if (!uploadedImageId)
|
|
|
|
|
|
|
|
|
321 |
|
322 |
-
if (confirm("Are you sure you want to delete this
|
323 |
try {
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
|
|
|
|
|
|
328 |
|
329 |
-
if (
|
330 |
-
|
331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
332 |
}
|
333 |
|
334 |
-
// Reset to step 1
|
335 |
resetToStep1();
|
336 |
} catch (err) {
|
|
|
337 |
handleApiError(err, 'Delete');
|
338 |
}
|
339 |
}
|
340 |
}
|
341 |
|
342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
343 |
return (
|
344 |
<PageContainer>
|
345 |
-
<div className=
|
346 |
{/* Drop-zone */}
|
347 |
{step === 1 && (
|
348 |
<Container
|
@@ -360,40 +423,40 @@ export default function UploadPage() {
|
|
360 |
</p>
|
361 |
|
362 |
{/* "More »" link */}
|
363 |
-
<div className=
|
364 |
<Link
|
365 |
to="/help"
|
366 |
-
className=
|
367 |
>
|
368 |
More <ArrowRightLineIcon className="w-3 h-3" />
|
369 |
</Link>
|
370 |
</div>
|
371 |
|
372 |
<div
|
373 |
-
className={
|
374 |
-
file ? 'bg-white' : ''
|
375 |
-
}`}
|
376 |
onDragOver={(e) => e.preventDefault()}
|
377 |
onDrop={onDrop}
|
378 |
>
|
379 |
{file && preview ? (
|
380 |
-
<div className=
|
381 |
-
<div className=
|
382 |
<img
|
383 |
src={preview}
|
384 |
alt="File preview"
|
385 |
-
className="w-full h-full object-contain"
|
386 |
/>
|
387 |
</div>
|
388 |
-
<p className=
|
389 |
{file.name}
|
390 |
</p>
|
|
|
|
|
|
|
391 |
</div>
|
392 |
) : (
|
393 |
<>
|
394 |
-
<UploadCloudLineIcon className=
|
395 |
-
<p className=
|
396 |
-
<p className=
|
397 |
</>
|
398 |
)}
|
399 |
|
@@ -421,15 +484,15 @@ export default function UploadPage() {
|
|
421 |
|
422 |
{/* Loading state */}
|
423 |
{isLoading && (
|
424 |
-
<div className=
|
425 |
<Spinner className="text-ifrcRed" />
|
426 |
-
<p className=
|
427 |
</div>
|
428 |
)}
|
429 |
|
430 |
{/* Generate button */}
|
431 |
{step === 1 && !isLoading && (
|
432 |
-
<div className=
|
433 |
<Button
|
434 |
name="generate"
|
435 |
disabled={!file}
|
@@ -440,171 +503,248 @@ export default function UploadPage() {
|
|
440 |
</div>
|
441 |
)}
|
442 |
|
443 |
-
{step ===
|
444 |
-
<
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
className=
|
456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
457 |
</div>
|
458 |
</div>
|
459 |
-
</
|
460 |
)}
|
461 |
|
462 |
-
{step ===
|
463 |
-
<div className=
|
464 |
-
{/*
|
465 |
-
<
|
466 |
-
heading="
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
479 |
-
|
480 |
-
|
481 |
-
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
|
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 |
-
|
534 |
-
|
535 |
-
{
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
|
541 |
-
|
542 |
-
|
543 |
-
|
544 |
-
|
545 |
-
|
546 |
-
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
551 |
-
|
552 |
-
|
553 |
-
|
554 |
-
|
555 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
<
|
572 |
-
name="
|
573 |
-
|
574 |
-
|
575 |
-
|
576 |
-
|
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=
|
605 |
-
<Heading level={2}>Saved!</Heading>
|
606 |
-
<p className=
|
607 |
-
<div className=
|
608 |
<Button
|
609 |
name="upload-another"
|
610 |
onClick={resetToStep1}
|
@@ -614,6 +754,30 @@ export default function UploadPage() {
|
|
614 |
</div>
|
615 |
</div>
|
616 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
617 |
</div>
|
618 |
</PageContainer>
|
619 |
);
|
|
|
10 |
DeleteBinLineIcon,
|
11 |
} from '@ifrc-go/icons';
|
12 |
import { Link, useSearchParams } from 'react-router-dom';
|
13 |
+
import styles from './UploadPage.module.css';
|
14 |
|
15 |
const SELECTED_MODEL_KEY = 'selectedVlmModel';
|
16 |
|
17 |
export default function UploadPage() {
|
18 |
const [searchParams] = useSearchParams();
|
19 |
+
const [step, setStep] = useState<1 | '2a' | '2b' | 3>(1);
|
20 |
const [isLoading, setIsLoading] = useState(false);
|
21 |
const stepRef = useRef(step);
|
22 |
const uploadedImageIdRef = useRef<string | null>(null);
|
23 |
const [preview, setPreview] = useState<string | null>(null);
|
|
|
24 |
|
25 |
const [file, setFile] = useState<File | null>(null);
|
26 |
const [source, setSource] = useState('');
|
27 |
+
const [eventType, setEventType] = useState('');
|
28 |
const [epsg, setEpsg] = useState('');
|
29 |
const [imageType, setImageType] = useState('');
|
30 |
const [countries, setCountries] = useState<string[]>([]);
|
31 |
const [title, setTitle] = useState('');
|
32 |
|
|
|
33 |
const [sources, setSources] = useState<{s_code: string, label: string}[]>([]);
|
34 |
const [types, setTypes] = useState<{t_code: string, label: string}[]>([]);
|
35 |
const [spatialReferences, setSpatialReferences] = useState<{epsg: string, srid: string, proj4: string, wkt: string}[]>([]);
|
36 |
const [imageTypes, setImageTypes] = useState<{image_type: string, label: string}[]>([]);
|
37 |
const [countriesOptions, setCountriesOptions] = useState<{c_code: string, label: string, r_code: string}[]>([]);
|
38 |
|
|
|
39 |
const [uploadedImageId, setUploadedImageId] = useState<string | null>(null);
|
40 |
|
|
|
41 |
stepRef.current = step;
|
42 |
uploadedImageIdRef.current = uploadedImageId;
|
43 |
|
|
|
44 |
const handleSourceChange = (value: any) => setSource(String(value));
|
45 |
+
const handleEventTypeChange = (value: any) => setEventType(String(value));
|
46 |
const handleEpsgChange = (value: any) => setEpsg(String(value));
|
47 |
const handleImageTypeChange = (value: any) => setImageType(String(value));
|
48 |
const handleCountriesChange = (value: any) => setCountries(Array.isArray(value) ? value.map(String) : []);
|
49 |
|
50 |
+
const handleStepChange = (newStep: 1 | '2a' | '2b' | 3) => {
|
51 |
+
setStep(newStep);
|
52 |
+
};
|
53 |
+
|
54 |
useEffect(() => {
|
55 |
Promise.all([
|
56 |
fetch('/api/sources').then(r => r.json()),
|
|
|
66 |
setImageTypes(imageTypesData);
|
67 |
setCountriesOptions(countriesData);
|
68 |
|
|
|
69 |
if (sourcesData.length > 0) setSource(sourcesData[0].s_code);
|
70 |
+
if (typesData.length > 0) setEventType(typesData[0].t_code);
|
71 |
if (spatialData.length > 0) setEpsg(spatialData[0].epsg);
|
72 |
if (imageTypesData.length > 0) setImageType(imageTypesData[0].image_type);
|
73 |
});
|
|
|
93 |
const [imageUrl, setImageUrl] = useState<string|null>(null);
|
94 |
const [draft, setDraft] = useState('');
|
95 |
|
|
|
96 |
useEffect(() => {
|
97 |
const mapId = searchParams.get('mapId');
|
98 |
const stepParam = searchParams.get('step');
|
99 |
+
const captionIdParam = searchParams.get('captionId');
|
100 |
|
101 |
if (mapId && stepParam === '2') {
|
|
|
102 |
fetch(`/api/images/${mapId}`)
|
103 |
.then(response => response.json())
|
104 |
.then(mapData => {
|
105 |
setImageUrl(mapData.image_url);
|
106 |
setSource(mapData.source);
|
107 |
+
setEventType(mapData.event_type);
|
108 |
setEpsg(mapData.epsg);
|
109 |
setImageType(mapData.image_type);
|
110 |
|
111 |
+
setUploadedImageId(mapId);
|
112 |
+
|
113 |
+
if (captionIdParam) {
|
114 |
+
setCaptionId(captionIdParam);
|
115 |
+
const existingCaption = mapData.captions?.find((c: any) => c.cap_id === captionIdParam);
|
116 |
+
if (existingCaption) {
|
117 |
+
setDraft(existingCaption.edited || existingCaption.generated);
|
118 |
+
setTitle(existingCaption.title || 'Generated Caption');
|
119 |
+
}
|
120 |
+
|
121 |
+
if (mapData.countries && Array.isArray(mapData.countries)) {
|
122 |
+
setCountries(mapData.countries.map((c: any) => c.c_code));
|
123 |
+
}
|
124 |
+
|
125 |
+
handleStepChange('2a');
|
126 |
+
} else {
|
127 |
+
setCaptionId(null);
|
128 |
+
setDraft('');
|
129 |
+
setTitle('');
|
130 |
+
|
131 |
+
|
132 |
+
if (mapData.countries && Array.isArray(mapData.countries)) {
|
133 |
+
setCountries(mapData.countries.map((c: any) => c.c_code));
|
134 |
+
}
|
135 |
+
|
136 |
+
handleStepChange('2a');
|
137 |
+
}
|
138 |
})
|
139 |
.catch(err => {
|
|
|
140 |
alert('Failed to load map data. Please try again.');
|
141 |
});
|
142 |
}
|
|
|
159 |
usability: 50,
|
160 |
});
|
161 |
|
162 |
+
const [isFullSizeModalOpen, setIsFullSizeModalOpen] = useState(false);
|
163 |
|
164 |
|
|
|
165 |
const onDrop = useCallback((e: DragEvent<HTMLDivElement>) => {
|
166 |
e.preventDefault();
|
167 |
const dropped = e.dataTransfer.files?.[0];
|
|
|
172 |
if (file) setFile(file);
|
173 |
}, []);
|
174 |
|
|
|
175 |
useEffect(() => {
|
176 |
if (!file) {
|
177 |
setPreview(null);
|
|
|
184 |
|
185 |
|
186 |
async function readJsonSafely(res: Response): Promise<any> {
|
187 |
+
const text = await res.text();
|
188 |
try {
|
189 |
+
return text ? JSON.parse(text) : {};
|
190 |
} catch {
|
191 |
+
return { error: text };
|
192 |
}
|
193 |
}
|
194 |
|
195 |
function handleApiError(err: any, operation: string) {
|
196 |
+
|
197 |
const message = err.message || `Failed to ${operation.toLowerCase()}`;
|
198 |
alert(message);
|
199 |
}
|
200 |
|
|
|
201 |
async function handleGenerate() {
|
202 |
if (!file) return;
|
203 |
|
|
|
206 |
const fd = new FormData();
|
207 |
fd.append('file', file);
|
208 |
fd.append('source', source);
|
209 |
+
fd.append('event_type', eventType);
|
210 |
fd.append('epsg', epsg);
|
211 |
fd.append('image_type', imageType);
|
212 |
countries.forEach((c) => fd.append('countries', c));
|
|
|
217 |
}
|
218 |
|
219 |
try {
|
|
|
220 |
const mapRes = await fetch('/api/images/', { method: 'POST', body: fd });
|
221 |
const mapJson = await readJsonSafely(mapRes);
|
222 |
if (!mapRes.ok) throw new Error(mapJson.error || 'Upload failed');
|
|
|
226 |
if (!mapIdVal) throw new Error('Upload failed: image_id not found');
|
227 |
setUploadedImageId(mapIdVal);
|
228 |
|
|
|
229 |
const capRes = await fetch(
|
230 |
`/api/images/${mapIdVal}/caption`,
|
231 |
{
|
|
|
243 |
const capJson = await readJsonSafely(capRes);
|
244 |
if (!capRes.ok) throw new Error(capJson.error || 'Caption failed');
|
245 |
setCaptionId(capJson.cap_id);
|
|
|
246 |
|
|
|
247 |
const extractedMetadata = capJson.raw_json?.extracted_metadata;
|
248 |
if (extractedMetadata) {
|
|
|
|
|
|
|
249 |
if (extractedMetadata.title) setTitle(extractedMetadata.title);
|
250 |
if (extractedMetadata.source) setSource(extractedMetadata.source);
|
251 |
+
if (extractedMetadata.type) setEventType(extractedMetadata.type);
|
252 |
if (extractedMetadata.epsg) setEpsg(extractedMetadata.epsg);
|
253 |
if (extractedMetadata.countries && Array.isArray(extractedMetadata.countries)) {
|
254 |
setCountries(extractedMetadata.countries);
|
255 |
}
|
256 |
}
|
257 |
|
|
|
258 |
setDraft(capJson.generated);
|
259 |
+
handleStepChange('2a');
|
260 |
} catch (err) {
|
261 |
handleApiError(err, 'Upload');
|
262 |
} finally {
|
|
|
264 |
}
|
265 |
}
|
266 |
|
|
|
267 |
async function handleSubmit() {
|
268 |
if (!captionId) return alert("No caption to submit");
|
269 |
|
270 |
try {
|
|
|
271 |
const metadataBody = {
|
272 |
source: source,
|
273 |
+
event_type: eventType,
|
274 |
epsg: epsg,
|
275 |
image_type: imageType,
|
276 |
countries: countries,
|
|
|
283 |
const metadataJson = await readJsonSafely(metadataRes);
|
284 |
if (!metadataRes.ok) throw new Error(metadataJson.error || "Metadata update failed");
|
285 |
|
|
|
286 |
const captionBody = {
|
287 |
title: title,
|
288 |
+
edited: draft || '',
|
289 |
accuracy: scores.accuracy,
|
290 |
context: scores.context,
|
291 |
usability: scores.usability,
|
|
|
298 |
const captionJson = await readJsonSafely(captionRes);
|
299 |
if (!captionRes.ok) throw new Error(captionJson.error || "Caption update failed");
|
300 |
|
|
|
301 |
setUploadedImageId(null);
|
302 |
+
handleStepChange(3);
|
303 |
} catch (err) {
|
304 |
handleApiError(err, 'Submit');
|
305 |
}
|
306 |
}
|
307 |
|
|
|
308 |
async function handleDelete() {
|
309 |
+
if (!uploadedImageId) {
|
310 |
+
|
311 |
+
alert('No caption to delete. Please try refreshing the page.');
|
312 |
+
return;
|
313 |
+
}
|
314 |
|
315 |
+
if (confirm("Are you sure you want to delete this caption? This action cannot be undone.")) {
|
316 |
try {
|
317 |
+
const captionsResponse = await fetch(`/api/images/${uploadedImageId}/captions`);
|
318 |
+
let hasOtherCaptions = false;
|
319 |
+
|
320 |
+
if (captionsResponse.ok) {
|
321 |
+
const captions = await captionsResponse.json();
|
322 |
+
hasOtherCaptions = captions.some((cap: any) => cap.cap_id !== captionId);
|
323 |
+
}
|
324 |
|
325 |
+
if (hasOtherCaptions) {
|
326 |
+
if (captionId) {
|
327 |
+
const capRes = await fetch(`/api/captions/${captionId}`, {
|
328 |
+
method: "DELETE",
|
329 |
+
});
|
330 |
+
if (!capRes.ok) {
|
331 |
+
throw new Error('Failed to delete caption');
|
332 |
+
}
|
333 |
+
}
|
334 |
+
} else {
|
335 |
+
const res = await fetch(`/api/images/${uploadedImageId}`, {
|
336 |
+
method: "DELETE",
|
337 |
+
});
|
338 |
+
|
339 |
+
if (!res.ok) {
|
340 |
+
const json = await readJsonSafely(res);
|
341 |
+
|
342 |
+
throw new Error(json.error || `Delete failed with status ${res.status}`);
|
343 |
+
}
|
344 |
}
|
345 |
|
|
|
346 |
resetToStep1();
|
347 |
} catch (err) {
|
348 |
+
|
349 |
handleApiError(err, 'Delete');
|
350 |
}
|
351 |
}
|
352 |
}
|
353 |
|
354 |
+
const handleProcessCaption = useCallback(async () => {
|
355 |
+
if (!uploadedImageId) {
|
356 |
+
alert('No image ID available to create a new caption.');
|
357 |
+
return;
|
358 |
+
}
|
359 |
+
|
360 |
+
setIsLoading(true);
|
361 |
+
|
362 |
+
try {
|
363 |
+
if (captionId) {
|
364 |
+
const captionBody = {
|
365 |
+
title: title,
|
366 |
+
edited: draft || '',
|
367 |
+
};
|
368 |
+
const captionRes = await fetch(`/api/captions/${captionId}`, {
|
369 |
+
method: "PUT",
|
370 |
+
headers: { "Content-Type": "application/json" },
|
371 |
+
body: JSON.stringify(captionBody),
|
372 |
+
});
|
373 |
+
if (!captionRes.ok) throw new Error('Failed to update caption');
|
374 |
+
} else {
|
375 |
+
const capRes = await fetch(
|
376 |
+
`/api/images/${uploadedImageId}/caption`,
|
377 |
+
{
|
378 |
+
method: 'POST',
|
379 |
+
headers: {
|
380 |
+
'Content-Type': 'application/x-www-form-urlencoded',
|
381 |
+
},
|
382 |
+
body: new URLSearchParams({
|
383 |
+
title: 'New Contribution Caption',
|
384 |
+
prompt: 'Describe this crisis map in detail',
|
385 |
+
...(localStorage.getItem(SELECTED_MODEL_KEY) && {
|
386 |
+
model_name: localStorage.getItem(SELECTED_MODEL_KEY)!
|
387 |
+
})
|
388 |
+
})
|
389 |
+
}
|
390 |
+
);
|
391 |
+
const capJson = await readJsonSafely(capRes);
|
392 |
+
if (!capRes.ok) throw new Error(capJson.error || 'Caption failed');
|
393 |
+
|
394 |
+
setCaptionId(capJson.cap_id);
|
395 |
+
setDraft(capJson.generated);
|
396 |
+
}
|
397 |
+
|
398 |
+
handleStepChange('2b');
|
399 |
+
} catch (err) {
|
400 |
+
handleApiError(err, 'Create New Caption');
|
401 |
+
} finally {
|
402 |
+
setIsLoading(false);
|
403 |
+
}
|
404 |
+
}, [uploadedImageId, title, captionId, draft]);
|
405 |
+
|
406 |
return (
|
407 |
<PageContainer>
|
408 |
+
<div className={styles.uploadContainer} data-step={step}>
|
409 |
{/* Drop-zone */}
|
410 |
{step === 1 && (
|
411 |
<Container
|
|
|
423 |
</p>
|
424 |
|
425 |
{/* "More »" link */}
|
426 |
+
<div className={styles.helpLink}>
|
427 |
<Link
|
428 |
to="/help"
|
429 |
+
className={styles.helpLink}
|
430 |
>
|
431 |
More <ArrowRightLineIcon className="w-3 h-3" />
|
432 |
</Link>
|
433 |
</div>
|
434 |
|
435 |
<div
|
436 |
+
className={`${styles.dropZone} ${file ? styles.hasFile : ''}`}
|
|
|
|
|
437 |
onDragOver={(e) => e.preventDefault()}
|
438 |
onDrop={onDrop}
|
439 |
>
|
440 |
{file && preview ? (
|
441 |
+
<div className={styles.filePreview}>
|
442 |
+
<div className={styles.filePreviewImage}>
|
443 |
<img
|
444 |
src={preview}
|
445 |
alt="File preview"
|
|
|
446 |
/>
|
447 |
</div>
|
448 |
+
<p className={styles.fileName}>
|
449 |
{file.name}
|
450 |
</p>
|
451 |
+
<p className={styles.fileInfo}>
|
452 |
+
{(file.size / 1024 / 1024).toFixed(2)} MB
|
453 |
+
</p>
|
454 |
</div>
|
455 |
) : (
|
456 |
<>
|
457 |
+
<UploadCloudLineIcon className={styles.dropZoneIcon} />
|
458 |
+
<p className={styles.dropZoneText}>Drag & Drop a file here</p>
|
459 |
+
<p className={styles.dropZoneSubtext}>or</p>
|
460 |
</>
|
461 |
)}
|
462 |
|
|
|
484 |
|
485 |
{/* Loading state */}
|
486 |
{isLoading && (
|
487 |
+
<div className={styles.loadingContainer}>
|
488 |
<Spinner className="text-ifrcRed" />
|
489 |
+
<p className={styles.loadingText}>Generating caption...</p>
|
490 |
</div>
|
491 |
)}
|
492 |
|
493 |
{/* Generate button */}
|
494 |
{step === 1 && !isLoading && (
|
495 |
+
<div className={styles.generateButtonContainer}>
|
496 |
<Button
|
497 |
name="generate"
|
498 |
disabled={!file}
|
|
|
503 |
</div>
|
504 |
)}
|
505 |
|
506 |
+
{step === '2a' && (
|
507 |
+
<div className={styles.step2Layout}>
|
508 |
+
{/* Left Column - Map */}
|
509 |
+
<div className={styles.mapColumn}>
|
510 |
+
<Container heading="Uploaded Image" headingLevel={3} withHeaderBorder withInternalPadding>
|
511 |
+
<div className={styles.uploadedMapContainer}>
|
512 |
+
<div className={styles.uploadedMapImage}>
|
513 |
+
<img
|
514 |
+
src={imageUrl || preview || undefined}
|
515 |
+
alt="Uploaded image preview"
|
516 |
+
/>
|
517 |
+
</div>
|
518 |
+
<div className={styles.viewFullSizeButton}>
|
519 |
+
<Button
|
520 |
+
name="view-full-size"
|
521 |
+
variant="secondary"
|
522 |
+
size={1}
|
523 |
+
onClick={() => setIsFullSizeModalOpen(true)}
|
524 |
+
>
|
525 |
+
View Image
|
526 |
+
</Button>
|
527 |
+
</div>
|
528 |
+
</div>
|
529 |
+
</Container>
|
530 |
+
</div>
|
531 |
+
|
532 |
+
{/* Right Column - Metadata Form */}
|
533 |
+
<div className={styles.contentColumn}>
|
534 |
+
<div className={styles.metadataSectionCard}>
|
535 |
+
<Container
|
536 |
+
heading="Confirm image details"
|
537 |
+
headingLevel={3}
|
538 |
+
withHeaderBorder
|
539 |
+
withInternalPadding
|
540 |
+
>
|
541 |
+
<div className={styles.formGrid}>
|
542 |
+
<div className={styles.titleField}>
|
543 |
+
<TextInput
|
544 |
+
label="Title"
|
545 |
+
name="title"
|
546 |
+
value={title}
|
547 |
+
onChange={(value) => setTitle(value || '')}
|
548 |
+
placeholder="Enter a title for this map..."
|
549 |
+
required
|
550 |
+
/>
|
551 |
+
</div>
|
552 |
+
<SelectInput
|
553 |
+
label="Source"
|
554 |
+
name="source"
|
555 |
+
value={source}
|
556 |
+
onChange={handleSourceChange}
|
557 |
+
options={sources}
|
558 |
+
keySelector={(o) => o.s_code}
|
559 |
+
labelSelector={(o) => o.label}
|
560 |
+
required
|
561 |
+
/>
|
562 |
+
<SelectInput
|
563 |
+
label="Event Type"
|
564 |
+
name="event_type"
|
565 |
+
value={eventType}
|
566 |
+
onChange={handleEventTypeChange}
|
567 |
+
options={types}
|
568 |
+
keySelector={(o) => o.t_code}
|
569 |
+
labelSelector={(o) => o.label}
|
570 |
+
required
|
571 |
+
/>
|
572 |
+
<SelectInput
|
573 |
+
label="EPSG"
|
574 |
+
name="epsg"
|
575 |
+
value={epsg}
|
576 |
+
onChange={handleEpsgChange}
|
577 |
+
options={spatialReferences}
|
578 |
+
keySelector={(o) => o.epsg}
|
579 |
+
labelSelector={(o) => `${o.srid} (EPSG:${o.epsg})`}
|
580 |
+
required
|
581 |
+
/>
|
582 |
+
<SelectInput
|
583 |
+
label="Image Type"
|
584 |
+
name="image_type"
|
585 |
+
value={imageType}
|
586 |
+
onChange={handleImageTypeChange}
|
587 |
+
options={imageTypes}
|
588 |
+
keySelector={(o) => o.image_type}
|
589 |
+
labelSelector={(o) => o.label}
|
590 |
+
required
|
591 |
+
/>
|
592 |
+
<MultiSelectInput
|
593 |
+
label="Countries (optional)"
|
594 |
+
name="countries"
|
595 |
+
value={countries}
|
596 |
+
onChange={handleCountriesChange}
|
597 |
+
options={countriesOptions}
|
598 |
+
keySelector={(o) => o.c_code}
|
599 |
+
labelSelector={(o) => o.label}
|
600 |
+
placeholder="Select one or more"
|
601 |
+
/>
|
602 |
+
</div>
|
603 |
+
<div className={styles.confirmSection}>
|
604 |
+
<IconButton
|
605 |
+
name="delete"
|
606 |
+
variant="tertiary"
|
607 |
+
onClick={handleDelete}
|
608 |
+
title="Delete"
|
609 |
+
ariaLabel="Delete uploaded image"
|
610 |
+
>
|
611 |
+
<DeleteBinLineIcon />
|
612 |
+
</IconButton>
|
613 |
+
<Button
|
614 |
+
name="confirm-metadata"
|
615 |
+
onClick={() => {
|
616 |
+
if (imageUrl && !file) {
|
617 |
+
handleProcessCaption();
|
618 |
+
} else {
|
619 |
+
handleStepChange('2b');
|
620 |
+
}
|
621 |
+
}}
|
622 |
+
>
|
623 |
+
{imageUrl && !file ?
|
624 |
+
(captionId ? 'Edit Caption' : 'Create New Caption') :
|
625 |
+
'Next'
|
626 |
+
}
|
627 |
+
</Button>
|
628 |
+
</div>
|
629 |
+
</Container>
|
630 |
</div>
|
631 |
</div>
|
632 |
+
</div>
|
633 |
)}
|
634 |
|
635 |
+
{step === '2b' && (
|
636 |
+
<div className={styles.step2Layout}>
|
637 |
+
{/* Left Column - Map */}
|
638 |
+
<div className={styles.mapColumn}>
|
639 |
+
<Container heading="Uploaded Image" headingLevel={3} withHeaderBorder withInternalPadding>
|
640 |
+
<div className={styles.uploadedMapContainer}>
|
641 |
+
<div className={styles.uploadedMapImage}>
|
642 |
+
<img
|
643 |
+
src={imageUrl || preview || undefined}
|
644 |
+
alt="Uploaded image preview"
|
645 |
+
/>
|
646 |
+
</div>
|
647 |
+
<div className={styles.viewFullSizeButton}>
|
648 |
+
<Button
|
649 |
+
name="view-full-size"
|
650 |
+
variant="secondary"
|
651 |
+
size={1}
|
652 |
+
onClick={() => setIsFullSizeModalOpen(true)}
|
653 |
+
>
|
654 |
+
View Image
|
655 |
+
</Button>
|
656 |
+
</div>
|
657 |
+
</div>
|
658 |
+
</Container>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
659 |
</div>
|
660 |
+
|
661 |
+
{/* Right Column - Content */}
|
662 |
+
<div className={styles.contentColumn}>
|
663 |
+
{/* ────── RATING SLIDERS ────── */}
|
664 |
+
<div className={styles.metadataSectionCard}>
|
665 |
+
<Container
|
666 |
+
heading="AI Performance Rating"
|
667 |
+
headingLevel={3}
|
668 |
+
withHeaderBorder
|
669 |
+
withInternalPadding
|
670 |
+
>
|
671 |
+
<div className={styles.ratingSection}>
|
672 |
+
<p className={styles.ratingDescription}>How well did the AI perform on the task?</p>
|
673 |
+
{(['accuracy', 'context', 'usability'] as const).map((k) => (
|
674 |
+
<div key={k} className={styles.ratingSlider}>
|
675 |
+
<label className={styles.ratingLabel}>{k}</label>
|
676 |
+
<input
|
677 |
+
type="range"
|
678 |
+
min={0}
|
679 |
+
max={100}
|
680 |
+
value={scores[k]}
|
681 |
+
onChange={(e) =>
|
682 |
+
setScores((s) => ({ ...s, [k]: Number(e.target.value) }))
|
683 |
+
}
|
684 |
+
className={styles.ratingInput}
|
685 |
+
/>
|
686 |
+
<span className={styles.ratingValue}>{scores[k]}</span>
|
687 |
+
</div>
|
688 |
+
))}
|
689 |
+
</div>
|
690 |
+
</Container>
|
691 |
+
</div>
|
692 |
+
|
693 |
+
{/* ────── AI‑GENERATED CAPTION ────── */}
|
694 |
+
<div className={styles.metadataSectionCard}>
|
695 |
+
<Container
|
696 |
+
heading="AI‑Generated Caption"
|
697 |
+
headingLevel={3}
|
698 |
+
withHeaderBorder
|
699 |
+
withInternalPadding
|
700 |
+
>
|
701 |
+
<div className="text-left">
|
702 |
+
<TextArea
|
703 |
+
name="caption"
|
704 |
+
value={draft}
|
705 |
+
onChange={(value) => setDraft(value || '')}
|
706 |
+
rows={5}
|
707 |
+
placeholder="AI-generated caption will appear here..."
|
708 |
/>
|
|
|
709 |
</div>
|
710 |
+
</Container>
|
711 |
</div>
|
712 |
+
|
713 |
+
{/* ────── SUBMIT BUTTON ────── */}
|
714 |
+
<div className={styles.submitSection}>
|
715 |
+
<Button
|
716 |
+
name="back"
|
717 |
+
variant="secondary"
|
718 |
+
onClick={() => handleStepChange('2a')}
|
719 |
+
>
|
720 |
+
← Back to Metadata
|
721 |
+
</Button>
|
722 |
+
<IconButton
|
723 |
+
name="delete"
|
724 |
+
variant="tertiary"
|
725 |
+
onClick={handleDelete}
|
726 |
+
title="Delete"
|
727 |
+
ariaLabel="Delete uploaded image"
|
728 |
+
>
|
729 |
+
<DeleteBinLineIcon />
|
730 |
+
</IconButton>
|
731 |
+
<Button
|
732 |
+
name="submit"
|
733 |
+
onClick={handleSubmit}
|
734 |
+
>
|
735 |
+
Submit
|
736 |
+
</Button>
|
737 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
738 |
</div>
|
739 |
</div>
|
740 |
)}
|
741 |
|
742 |
{/* Success page */}
|
743 |
{step === 3 && (
|
744 |
+
<div className={styles.successContainer}>
|
745 |
+
<Heading level={2} className={styles.successHeading}>Saved!</Heading>
|
746 |
+
<p className={styles.successText}>Your caption has been successfully saved.</p>
|
747 |
+
<div className={styles.successButton}>
|
748 |
<Button
|
749 |
name="upload-another"
|
750 |
onClick={resetToStep1}
|
|
|
754 |
</div>
|
755 |
</div>
|
756 |
)}
|
757 |
+
|
758 |
+
{/* Full Size Image Modal */}
|
759 |
+
{isFullSizeModalOpen && (
|
760 |
+
<div className={styles.fullSizeModalOverlay} onClick={() => setIsFullSizeModalOpen(false)}>
|
761 |
+
<div className={styles.fullSizeModalContent} onClick={(e) => e.stopPropagation()}>
|
762 |
+
<div className={styles.fullSizeModalHeader}>
|
763 |
+
<Button
|
764 |
+
name="close-modal"
|
765 |
+
variant="tertiary"
|
766 |
+
size={1}
|
767 |
+
onClick={() => setIsFullSizeModalOpen(false)}
|
768 |
+
>
|
769 |
+
✕
|
770 |
+
</Button>
|
771 |
+
</div>
|
772 |
+
<div className={styles.fullSizeModalImage}>
|
773 |
+
<img
|
774 |
+
src={imageUrl || preview || undefined}
|
775 |
+
alt="Full size map"
|
776 |
+
/>
|
777 |
+
</div>
|
778 |
+
</div>
|
779 |
+
</div>
|
780 |
+
)}
|
781 |
</div>
|
782 |
</PageContainer>
|
783 |
);
|
frontend/src/pages/UploadPage/index.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export { default } from './UploadPage';
|
frontend/src/types.ts
CHANGED
@@ -1,13 +1,13 @@
|
|
1 |
export interface MapOut {
|
2 |
-
map_id: string;
|
3 |
file_key: string;
|
4 |
sha256: string;
|
5 |
source: string;
|
6 |
region: string;
|
7 |
category: string;
|
8 |
caption?: {
|
9 |
-
cap_id: string;
|
10 |
-
map_id: string;
|
11 |
generated: string;
|
12 |
edited?: string;
|
13 |
accuracy?: number;
|
|
|
1 |
export interface MapOut {
|
2 |
+
map_id: string;
|
3 |
file_key: string;
|
4 |
sha256: string;
|
5 |
source: string;
|
6 |
region: string;
|
7 |
category: string;
|
8 |
caption?: {
|
9 |
+
cap_id: string;
|
10 |
+
map_id: string;
|
11 |
generated: string;
|
12 |
edited?: string;
|
13 |
accuracy?: number;
|
frontend/tailwind.config.js
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
-
/** @type {import('tailwindcss').Config} */
|
2 |
export default {
|
3 |
content: [
|
4 |
"./index.html",
|
|
|
|
|
1 |
export default {
|
2 |
content: [
|
3 |
"./index.html",
|
frontend/tsconfig.app.json
CHANGED
@@ -6,16 +6,12 @@
|
|
6 |
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
7 |
"module": "ESNext",
|
8 |
"skipLibCheck": true,
|
9 |
-
|
10 |
-
/* Bundler mode */
|
11 |
"moduleResolution": "bundler",
|
12 |
"allowImportingTsExtensions": true,
|
13 |
"verbatimModuleSyntax": true,
|
14 |
"moduleDetection": "force",
|
15 |
"noEmit": true,
|
16 |
"jsx": "react-jsx",
|
17 |
-
|
18 |
-
/* Linting */
|
19 |
"strict": true,
|
20 |
"noUnusedLocals": true,
|
21 |
"noUnusedParameters": true,
|
|
|
6 |
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
7 |
"module": "ESNext",
|
8 |
"skipLibCheck": true,
|
|
|
|
|
9 |
"moduleResolution": "bundler",
|
10 |
"allowImportingTsExtensions": true,
|
11 |
"verbatimModuleSyntax": true,
|
12 |
"moduleDetection": "force",
|
13 |
"noEmit": true,
|
14 |
"jsx": "react-jsx",
|
|
|
|
|
15 |
"strict": true,
|
16 |
"noUnusedLocals": true,
|
17 |
"noUnusedParameters": true,
|
frontend/tsconfig.node.json
CHANGED
@@ -5,15 +5,11 @@
|
|
5 |
"lib": ["ES2023"],
|
6 |
"module": "ESNext",
|
7 |
"skipLibCheck": true,
|
8 |
-
|
9 |
-
/* Bundler mode */
|
10 |
"moduleResolution": "bundler",
|
11 |
"allowImportingTsExtensions": true,
|
12 |
"verbatimModuleSyntax": true,
|
13 |
"moduleDetection": "force",
|
14 |
"noEmit": true,
|
15 |
-
|
16 |
-
/* Linting */
|
17 |
"strict": true,
|
18 |
"noUnusedLocals": true,
|
19 |
"noUnusedParameters": true,
|
|
|
5 |
"lib": ["ES2023"],
|
6 |
"module": "ESNext",
|
7 |
"skipLibCheck": true,
|
|
|
|
|
8 |
"moduleResolution": "bundler",
|
9 |
"allowImportingTsExtensions": true,
|
10 |
"verbatimModuleSyntax": true,
|
11 |
"moduleDetection": "force",
|
12 |
"noEmit": true,
|
|
|
|
|
13 |
"strict": true,
|
14 |
"noUnusedLocals": true,
|
15 |
"noUnusedParameters": true,
|
frontend/vite.config.ts
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
-
// vite.config.ts
|
2 |
import { defineConfig } from 'vite'
|
3 |
import react from '@vitejs/plugin-react'
|
4 |
|
@@ -6,12 +5,10 @@ export default defineConfig({
|
|
6 |
plugins: [react()],
|
7 |
server: {
|
8 |
proxy: {
|
9 |
-
// proxy any /api/* request to localhost:8080
|
10 |
'/api': {
|
11 |
target: 'http://localhost:8080',
|
12 |
changeOrigin: true,
|
13 |
secure: false,
|
14 |
-
// rewrite: (path) => path.replace(/^\/api/, '/api'), // not needed if same prefix
|
15 |
},
|
16 |
},
|
17 |
},
|
|
|
|
|
1 |
import { defineConfig } from 'vite'
|
2 |
import react from '@vitejs/plugin-react'
|
3 |
|
|
|
5 |
plugins: [react()],
|
6 |
server: {
|
7 |
proxy: {
|
|
|
8 |
'/api': {
|
9 |
target: 'http://localhost:8080',
|
10 |
changeOrigin: true,
|
11 |
secure: false,
|
|
|
12 |
},
|
13 |
},
|
14 |
},
|
go-web-app-develop/.changeset/README.md
DELETED
@@ -1,8 +0,0 @@
|
|
1 |
-
# Changesets
|
2 |
-
|
3 |
-
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
4 |
-
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
5 |
-
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
6 |
-
|
7 |
-
We have a quick list of common questions to get you started engaging with this project in
|
8 |
-
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.changeset/config.json
DELETED
@@ -1,15 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
|
3 |
-
"changelog": "@changesets/cli/changelog",
|
4 |
-
"commit": false,
|
5 |
-
"fixed": [],
|
6 |
-
"linked": [],
|
7 |
-
"access": "public",
|
8 |
-
"baseBranch": "develop",
|
9 |
-
"updateInternalDependencies": "patch",
|
10 |
-
"ignore": [],
|
11 |
-
"privatePackages": {
|
12 |
-
"version": true,
|
13 |
-
"tag": true
|
14 |
-
}
|
15 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.changeset/lovely-kids-boil.md
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
---
|
2 |
-
"go-web-app": patch
|
3 |
-
---
|
4 |
-
|
5 |
-
Fix use of operational timeframe date in imminent final report form
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.changeset/pre.json
DELETED
@@ -1,15 +0,0 @@
|
|
1 |
-
{
|
2 |
-
"mode": "pre",
|
3 |
-
"tag": "beta",
|
4 |
-
"initialVersions": {
|
5 |
-
"go-web-app": "7.20.2",
|
6 |
-
"go-ui-storybook": "1.0.7",
|
7 |
-
"@ifrc-go/ui": "1.5.1"
|
8 |
-
},
|
9 |
-
"changesets": [
|
10 |
-
"lovely-kids-boil",
|
11 |
-
"solid-clubs-care",
|
12 |
-
"sweet-gifts-cheer",
|
13 |
-
"whole-lions-guess"
|
14 |
-
]
|
15 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.changeset/solid-clubs-care.md
DELETED
@@ -1,8 +0,0 @@
|
|
1 |
-
---
|
2 |
-
"go-web-app": minor
|
3 |
-
---
|
4 |
-
|
5 |
-
Add Crisis categorization update date
|
6 |
-
|
7 |
-
- Add updated date for crisis categorization in emergency page.
|
8 |
-
- Add consent checkbox over situational overview in field report form.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.changeset/sweet-gifts-cheer.md
DELETED
@@ -1,9 +0,0 @@
|
|
1 |
-
---
|
2 |
-
"go-web-app": minor
|
3 |
-
---
|
4 |
-
|
5 |
-
Add support for DREF imminent v2 in final report
|
6 |
-
|
7 |
-
- Add a separate route for the old dref final report form
|
8 |
-
- Update dref final report to accomodate imminent v2 changes
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.changeset/whole-lions-guess.md
DELETED
@@ -1,7 +0,0 @@
|
|
1 |
-
---
|
2 |
-
"go-web-app": patch
|
3 |
-
---
|
4 |
-
|
5 |
-
- Fix calculation of Operation End date in Final report form
|
6 |
-
- Fix icon position issue in the implementation table of DREF PDF export
|
7 |
-
- Update the label for last update date in the crisis categorization pop-up
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.dockerignore
DELETED
@@ -1,148 +0,0 @@
|
|
1 |
-
# Swap files
|
2 |
-
*.swp
|
3 |
-
|
4 |
-
# Byte-compiled / optimized / DLL files
|
5 |
-
__pycache__
|
6 |
-
*.py[cod]
|
7 |
-
*$py.class
|
8 |
-
|
9 |
-
# C extensions
|
10 |
-
*.so
|
11 |
-
|
12 |
-
# Distribution / packaging
|
13 |
-
.Python
|
14 |
-
env
|
15 |
-
build
|
16 |
-
develop-eggs
|
17 |
-
dist
|
18 |
-
downloads
|
19 |
-
eggs
|
20 |
-
.eggs
|
21 |
-
lib
|
22 |
-
lib64
|
23 |
-
parts
|
24 |
-
sdist
|
25 |
-
var
|
26 |
-
*.egg-info
|
27 |
-
.installed.cfg
|
28 |
-
*.egg
|
29 |
-
|
30 |
-
# PyInstaller
|
31 |
-
# Usually these files are written by a python script from a template
|
32 |
-
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
33 |
-
*.manifest
|
34 |
-
*.spec
|
35 |
-
|
36 |
-
# Installer logs
|
37 |
-
pip-log.txt
|
38 |
-
pip-delete-this-directory.txt
|
39 |
-
|
40 |
-
# Unit test / coverage reports
|
41 |
-
htmlcov
|
42 |
-
.tox
|
43 |
-
.coverage
|
44 |
-
.coverage.*
|
45 |
-
.cache
|
46 |
-
nosetests.xml
|
47 |
-
coverage.xml
|
48 |
-
*,cover
|
49 |
-
.hypothesis
|
50 |
-
|
51 |
-
# Translations
|
52 |
-
*.mo
|
53 |
-
*.pot
|
54 |
-
|
55 |
-
# Django stuff:
|
56 |
-
*.log
|
57 |
-
|
58 |
-
# Sphinx documentation
|
59 |
-
docs/_build
|
60 |
-
|
61 |
-
# PyBuilder
|
62 |
-
target
|
63 |
-
|
64 |
-
#Ipython Notebook
|
65 |
-
.ipynb_checkpoints
|
66 |
-
|
67 |
-
# SASS cache
|
68 |
-
.sass-cache
|
69 |
-
media_test
|
70 |
-
|
71 |
-
# Rope project settings
|
72 |
-
.ropeproject
|
73 |
-
|
74 |
-
# Logs
|
75 |
-
logs
|
76 |
-
*.log
|
77 |
-
npm-debug.log*
|
78 |
-
yarn-debug.log*
|
79 |
-
yarn-error.log*
|
80 |
-
|
81 |
-
# Runtime data
|
82 |
-
pids
|
83 |
-
*.pid
|
84 |
-
*.seed
|
85 |
-
*.pid.lock
|
86 |
-
|
87 |
-
# Directory for instrumented libs generated by jscoverage/JSCover
|
88 |
-
lib-cov
|
89 |
-
|
90 |
-
# Coverage directory used by tools like istanbul
|
91 |
-
coverage
|
92 |
-
|
93 |
-
# nyc test coverage
|
94 |
-
.nyc_output
|
95 |
-
|
96 |
-
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
97 |
-
.grunt
|
98 |
-
|
99 |
-
# Bower dependency directory (https://bower.io/)
|
100 |
-
bower_components
|
101 |
-
|
102 |
-
# node-waf configuration
|
103 |
-
.lock-wscript
|
104 |
-
|
105 |
-
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
106 |
-
build/Release
|
107 |
-
|
108 |
-
# Dependency directories
|
109 |
-
node_modules
|
110 |
-
jspm_packages
|
111 |
-
|
112 |
-
# Typescript v1 declaration files
|
113 |
-
typings
|
114 |
-
|
115 |
-
# Optional npm cache directory
|
116 |
-
.npm
|
117 |
-
|
118 |
-
# Optional eslint cache
|
119 |
-
.eslintcache
|
120 |
-
|
121 |
-
# Optional REPL history
|
122 |
-
.node_repl_history
|
123 |
-
|
124 |
-
# Output of 'npm pack'
|
125 |
-
*.tgz
|
126 |
-
|
127 |
-
# Yarn Integrity file
|
128 |
-
.yarn-integrity
|
129 |
-
|
130 |
-
# dotenv environment variables file
|
131 |
-
.env
|
132 |
-
.env*
|
133 |
-
|
134 |
-
# Sensitive Deploy Files
|
135 |
-
deploy/eb/
|
136 |
-
|
137 |
-
# tox
|
138 |
-
./.tox
|
139 |
-
|
140 |
-
# Helm
|
141 |
-
.helm-charts/
|
142 |
-
|
143 |
-
# Docker
|
144 |
-
Dockerfile
|
145 |
-
.dockerignore
|
146 |
-
|
147 |
-
# git
|
148 |
-
.gitignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.github/ISSUE_TEMPLATE/01_bug_report.yml
DELETED
@@ -1,92 +0,0 @@
|
|
1 |
-
name: "Bug Report"
|
2 |
-
description: "Report a technical or visual issue."
|
3 |
-
labels: ["type: bug"]
|
4 |
-
type: "Bug"
|
5 |
-
body:
|
6 |
-
- type: markdown
|
7 |
-
attributes:
|
8 |
-
value: |
|
9 |
-
**Bug Report**
|
10 |
-
Please fill out the form below with as much detail as possible.
|
11 |
-
If the issue is visual, screenshots or videos are greatly appreciated.
|
12 |
-
**Please review [our guide on reporting bugs](https://github.com/IFRCGo/go-web-app/blob/develop/CONTRIBUTING.md#reporting-bugs) before opening a new issue.**
|
13 |
-
|
14 |
-
- type: input
|
15 |
-
attributes:
|
16 |
-
label: "Page URL"
|
17 |
-
description: "The URL of the page where you encountered the issue."
|
18 |
-
placeholder: "https://go.ifrc.org/"
|
19 |
-
validations:
|
20 |
-
required: true
|
21 |
-
|
22 |
-
- type: dropdown
|
23 |
-
attributes:
|
24 |
-
label: "Environment"
|
25 |
-
description: "Please select the environment where the bug occurred."
|
26 |
-
options:
|
27 |
-
- "Alpha"
|
28 |
-
- "Staging"
|
29 |
-
- "Production"
|
30 |
-
validations:
|
31 |
-
required: true
|
32 |
-
|
33 |
-
- type: input
|
34 |
-
attributes:
|
35 |
-
label: "Browser"
|
36 |
-
description: "Which browser are you using? (e.g., Chrome, Firefox, Safari)"
|
37 |
-
placeholder: "Chrome"
|
38 |
-
validations:
|
39 |
-
required: true
|
40 |
-
|
41 |
-
- type: textarea
|
42 |
-
attributes:
|
43 |
-
label: "Steps to Reproduce the Issue"
|
44 |
-
description: |
|
45 |
-
Please describe the issue in detail, including:
|
46 |
-
1. What actions led to the issue?
|
47 |
-
2. If possible, attach screenshots or videos demonstrating the problem.
|
48 |
-
placeholder: |
|
49 |
-
1. I clicked on...
|
50 |
-
2. [Attach screenshots/videos if available]
|
51 |
-
validations:
|
52 |
-
required: true
|
53 |
-
|
54 |
-
- type: textarea
|
55 |
-
attributes:
|
56 |
-
label: "Expected Behavior"
|
57 |
-
description: "Describe what you expected to happen."
|
58 |
-
placeholder: "I expected the page to..."
|
59 |
-
validations:
|
60 |
-
required: true
|
61 |
-
|
62 |
-
- type: textarea
|
63 |
-
attributes:
|
64 |
-
label: "Actual Behavior"
|
65 |
-
description: "Describe what actually happened, including any error messages."
|
66 |
-
placeholder: "Instead, I saw..."
|
67 |
-
validations:
|
68 |
-
required: true
|
69 |
-
|
70 |
-
- type: dropdown
|
71 |
-
attributes:
|
72 |
-
label: "Priority"
|
73 |
-
description: "How urgent is this issue?"
|
74 |
-
options:
|
75 |
-
- "Low (Minor inconvenience)"
|
76 |
-
- "Medium (Affects functionality, but there is a workaround)"
|
77 |
-
- "High (Major functionality is broken)"
|
78 |
-
- "Critical (Site is unusable)"
|
79 |
-
validations:
|
80 |
-
required: false
|
81 |
-
|
82 |
-
- type: textarea
|
83 |
-
attributes:
|
84 |
-
label: "Additional Context (Optional)"
|
85 |
-
description: |
|
86 |
-
Provide any extra details, such as:
|
87 |
-
- Related links.
|
88 |
-
- Previous occurrences of this issue.
|
89 |
-
- Workarounds you have tried.
|
90 |
-
placeholder: "This issue also happened on [link]."
|
91 |
-
validations:
|
92 |
-
required: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.github/ISSUE_TEMPLATE/02_feature_request.yml
DELETED
@@ -1,39 +0,0 @@
|
|
1 |
-
name: "Feature Request"
|
2 |
-
description: "Suggest a new idea or enhancement."
|
3 |
-
labels: ["type: feature-request"]
|
4 |
-
type: "Feature"
|
5 |
-
body:
|
6 |
-
- type: markdown
|
7 |
-
attributes:
|
8 |
-
value: |
|
9 |
-
**Feature Request**
|
10 |
-
Thank you for suggesting a new feature!
|
11 |
-
Please provide as much detail as possible to help us understand and evaluate your idea.
|
12 |
-
**Please review [our guide on suggesting enhancements](https://github.com/IFRCGo/go-web-app/blob/develop/CONTRIBUTING.md#suggesting-enhancements).**
|
13 |
-
|
14 |
-
- type: textarea
|
15 |
-
attributes:
|
16 |
-
label: "Feature Description"
|
17 |
-
description: |
|
18 |
-
Describe your feature request in detail, including:
|
19 |
-
- What the feature is.
|
20 |
-
- Why it is needed and how it will improve the project.
|
21 |
-
- How it will benefit users (e.g., As a user, I want to [do something] so that [desired outcome].).
|
22 |
-
placeholder: "As a user, I want to filter search results by date so that I can quickly find recent information."
|
23 |
-
validations:
|
24 |
-
required: true
|
25 |
-
|
26 |
-
- type: textarea
|
27 |
-
attributes:
|
28 |
-
label: "Additional Context"
|
29 |
-
description: |
|
30 |
-
Provide any extra details or supporting information, such as:
|
31 |
-
- Links to references or related resources.
|
32 |
-
- Examples from other projects or systems.
|
33 |
-
- Screenshots, mockups, or diagrams.
|
34 |
-
*Tip: You can attach files by clicking here and dragging them in.*
|
35 |
-
placeholder: |
|
36 |
-
Here's a link to a similar feature in another project: [link].
|
37 |
-
I've also attached a mockup of what this could look like.
|
38 |
-
validations:
|
39 |
-
required: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.github/ISSUE_TEMPLATE/03_epic_request.yml
DELETED
@@ -1,37 +0,0 @@
|
|
1 |
-
name: "Epic"
|
2 |
-
description: "Track a larger initiative with multiple related tasks and deliverables."
|
3 |
-
labels: ["type: epic"]
|
4 |
-
type: "Feature"
|
5 |
-
body:
|
6 |
-
- type: markdown
|
7 |
-
attributes:
|
8 |
-
value: |
|
9 |
-
**Epic**
|
10 |
-
Use this to define a large, overarching initiative.
|
11 |
-
**Please review [our guide on suggesting enhancements](https://github.com/IFRCGo/go-web-app/blob/develop/CONTRIBUTING.md#suggesting-enhancements).**
|
12 |
-
|
13 |
-
- type: textarea
|
14 |
-
attributes:
|
15 |
-
label: "Epic Summary"
|
16 |
-
description: |
|
17 |
-
Provide a clear and concise summary of the epic.
|
18 |
-
- What is this epic about?
|
19 |
-
- What problem does it solve or what goal does it achieve?
|
20 |
-
- How does it align with the project’s objectives?
|
21 |
-
placeholder: |
|
22 |
-
Example:
|
23 |
-
This epic focuses on implementing a new feature.
|
24 |
-
validations:
|
25 |
-
required: true
|
26 |
-
|
27 |
-
- type: textarea
|
28 |
-
attributes:
|
29 |
-
label: "Additional Context or Resources"
|
30 |
-
description: "Provide any additional information, links, or resources that will help the team understand and execute this epic."
|
31 |
-
placeholder: |
|
32 |
-
Examples:
|
33 |
-
- Link to design mockups: [link]
|
34 |
-
- Technical specs document: [link]
|
35 |
-
- Reference to similar features: [link]
|
36 |
-
validations:
|
37 |
-
required: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.github/ISSUE_TEMPLATE/config.yml
DELETED
@@ -1,5 +0,0 @@
|
|
1 |
-
blank_issues_enabled: true
|
2 |
-
contact_links:
|
3 |
-
- name: Documentation
|
4 |
-
url: https://go-wiki.ifrc.org/en/home
|
5 |
-
about: Please consult the wiki to know more about IFRC GO.
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.github/dependabot.yml
DELETED
@@ -1,27 +0,0 @@
|
|
1 |
-
version: 2
|
2 |
-
updates:
|
3 |
-
- package-ecosystem: npm
|
4 |
-
directory: /
|
5 |
-
schedule:
|
6 |
-
interval: weekly
|
7 |
-
groups:
|
8 |
-
eslint:
|
9 |
-
patterns:
|
10 |
-
- "*eslint*"
|
11 |
-
vite:
|
12 |
-
patterns:
|
13 |
-
- "*vite*"
|
14 |
-
postcss:
|
15 |
-
patterns:
|
16 |
-
- "*postcss*"
|
17 |
-
stylelint:
|
18 |
-
patterns:
|
19 |
-
- "*stylelint*"
|
20 |
-
all-other-dependencies:
|
21 |
-
patterns:
|
22 |
-
- "*"
|
23 |
-
exclude-patterns:
|
24 |
-
- "*eslint*"
|
25 |
-
- "*vite*"
|
26 |
-
- "*postcss*"
|
27 |
-
- "*stylelint*"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.github/pull_request_template.md
DELETED
@@ -1,30 +0,0 @@
|
|
1 |
-
## Summary
|
2 |
-
|
3 |
-
Provide a brief description of what this PR addresses and its purpose.
|
4 |
-
|
5 |
-
## Addresses
|
6 |
-
|
7 |
-
* Issue(s): *List related issues or tickets.*
|
8 |
-
|
9 |
-
## Depends On
|
10 |
-
|
11 |
-
* Other PRs or Dependencies: *List PRs or dependencies this PR relies on.*
|
12 |
-
|
13 |
-
## Changes
|
14 |
-
|
15 |
-
* Detailed list or prose of changes
|
16 |
-
* Breaking changes
|
17 |
-
* Changes to configurations
|
18 |
-
|
19 |
-
## This PR Ensures:
|
20 |
-
|
21 |
-
* \[ ] No typos or grammatical errors
|
22 |
-
* \[ ] No conflict markers left in the code
|
23 |
-
* \[ ] No unwanted comments, temporary files, or auto-generated files
|
24 |
-
* \[ ] No inclusion of secret keys or sensitive data
|
25 |
-
* \[ ] No `console.log` statements meant for debugging
|
26 |
-
* \[ ] All CI checks have passed
|
27 |
-
|
28 |
-
## Additional Notes
|
29 |
-
|
30 |
-
*Optional: Add any other relevant context, screenshots, or details here.*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.github/workflows/add-issue-to-backlog.yml
DELETED
@@ -1,16 +0,0 @@
|
|
1 |
-
name: Add issues to Backlog
|
2 |
-
|
3 |
-
on:
|
4 |
-
issues:
|
5 |
-
types:
|
6 |
-
- opened
|
7 |
-
|
8 |
-
jobs:
|
9 |
-
add-to-project:
|
10 |
-
name: Add issue to project
|
11 |
-
runs-on: ubuntu-latest
|
12 |
-
steps:
|
13 |
-
- uses: actions/[email protected]
|
14 |
-
with:
|
15 |
-
project-url: https://github.com/orgs/IFRCGo/projects/12
|
16 |
-
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.github/workflows/chromatic.yml
DELETED
@@ -1,127 +0,0 @@
|
|
1 |
-
name: 'Chromatic'
|
2 |
-
|
3 |
-
on:
|
4 |
-
pull_request:
|
5 |
-
push:
|
6 |
-
branches:
|
7 |
-
- develop
|
8 |
-
|
9 |
-
concurrency:
|
10 |
-
group: ${{ github.workflow }}-${{ github.ref }}-chromatic
|
11 |
-
cancel-in-progress: true
|
12 |
-
|
13 |
-
permissions:
|
14 |
-
actions: write
|
15 |
-
contents: read
|
16 |
-
pages: write
|
17 |
-
id-token: write
|
18 |
-
|
19 |
-
jobs:
|
20 |
-
changed-files:
|
21 |
-
name: Check for changed files
|
22 |
-
runs-on: ubuntu-latest
|
23 |
-
outputs:
|
24 |
-
all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }}
|
25 |
-
any_changed: ${{ steps.changed-files.outputs.any_changed }}
|
26 |
-
steps:
|
27 |
-
- uses: actions/checkout@v4
|
28 |
-
with:
|
29 |
-
fetch-depth: 0
|
30 |
-
- name: Get changed files
|
31 |
-
id: changed-files
|
32 |
-
uses: tj-actions/changed-files@v44
|
33 |
-
with:
|
34 |
-
files: |
|
35 |
-
packages/ui/**
|
36 |
-
packages/go-ui-storybook/**
|
37 |
-
ui:
|
38 |
-
name: Build UI Library
|
39 |
-
environment: 'test'
|
40 |
-
runs-on: ubuntu-latest
|
41 |
-
needs: [changed-files]
|
42 |
-
if: ${{ needs.changed-files.outputs.any_changed == 'true' }}
|
43 |
-
defaults:
|
44 |
-
run:
|
45 |
-
working-directory: packages/ui
|
46 |
-
steps:
|
47 |
-
- uses: actions/checkout@v4
|
48 |
-
with:
|
49 |
-
fetch-depth: 0
|
50 |
-
- name: Install pnpm
|
51 |
-
uses: pnpm/action-setup@v4
|
52 |
-
- name: Install Node.js
|
53 |
-
uses: actions/setup-node@v4
|
54 |
-
with:
|
55 |
-
node-version: 20
|
56 |
-
cache: 'pnpm'
|
57 |
-
- name: Install dependencies
|
58 |
-
run: pnpm install
|
59 |
-
- name: Typecheck
|
60 |
-
run: pnpm typecheck
|
61 |
-
- name: Lint CSS
|
62 |
-
run: pnpm lint:css
|
63 |
-
- name: Lint JS
|
64 |
-
run: pnpm lint:js
|
65 |
-
- name: build UI library
|
66 |
-
run: pnpm build
|
67 |
-
- uses: actions/upload-artifact@v4
|
68 |
-
with:
|
69 |
-
name: ui-build
|
70 |
-
path: packages/ui/dist
|
71 |
-
chromatic:
|
72 |
-
name: Chromatic Deploy
|
73 |
-
runs-on: ubuntu-latest
|
74 |
-
needs: [ui]
|
75 |
-
steps:
|
76 |
-
- uses: actions/checkout@v4
|
77 |
-
with:
|
78 |
-
fetch-depth: 0
|
79 |
-
- name: Install pnpm
|
80 |
-
uses: pnpm/action-setup@v4
|
81 |
-
- name: Install Node.js
|
82 |
-
uses: actions/setup-node@v4
|
83 |
-
with:
|
84 |
-
node-version: 20
|
85 |
-
cache: 'pnpm'
|
86 |
-
- name: Install dependencies
|
87 |
-
run: pnpm install
|
88 |
-
- uses: actions/download-artifact@v4
|
89 |
-
with:
|
90 |
-
name: ui-build
|
91 |
-
path: packages/ui/dist
|
92 |
-
- name: Run Chromatic
|
93 |
-
uses: chromaui/action@v1
|
94 |
-
with:
|
95 |
-
exitZeroOnChanges: true
|
96 |
-
exitOnceUploaded: true
|
97 |
-
onlyChanged: true
|
98 |
-
skip: "@(renovate/**|dependabot/**)"
|
99 |
-
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
100 |
-
token: ${{ secrets.GITHUB_TOKEN }}
|
101 |
-
autoAcceptChanges: "develop"
|
102 |
-
workingDir: packages/go-ui-storybook
|
103 |
-
github-pages:
|
104 |
-
name: Deploy to Github Pages
|
105 |
-
runs-on: ubuntu-latest
|
106 |
-
needs: [ui]
|
107 |
-
steps:
|
108 |
-
- uses: actions/checkout@v4
|
109 |
-
with:
|
110 |
-
fetch-depth: 0
|
111 |
-
- name: Install pnpm
|
112 |
-
uses: pnpm/action-setup@v4
|
113 |
-
- name: Install Node.js
|
114 |
-
uses: actions/setup-node@v4
|
115 |
-
with:
|
116 |
-
node-version: 20
|
117 |
-
cache: 'pnpm'
|
118 |
-
- uses: actions/download-artifact@v4
|
119 |
-
with:
|
120 |
-
name: ui-build
|
121 |
-
path: packages/ui/dist
|
122 |
-
- uses: bitovi/[email protected]
|
123 |
-
with:
|
124 |
-
install_command: pnpm install
|
125 |
-
build_command: pnpm build-storybook
|
126 |
-
path: packages/go-ui-storybook/storybook-static
|
127 |
-
checkout: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.github/workflows/ci.yml
DELETED
@@ -1,304 +0,0 @@
|
|
1 |
-
name: CI
|
2 |
-
|
3 |
-
on:
|
4 |
-
pull_request:
|
5 |
-
push:
|
6 |
-
branches:
|
7 |
-
- 'develop'
|
8 |
-
|
9 |
-
env:
|
10 |
-
APP_ADMIN_URL: ${{ vars.APP_ADMIN_URL }}
|
11 |
-
APP_API_ENDPOINT: ${{ vars.APP_API_ENDPOINT }}
|
12 |
-
APP_ENVIRONMENT: ${{ vars.APP_ENVIRONMENT }}
|
13 |
-
APP_MAPBOX_ACCESS_TOKEN: ${{ vars.APP_MAPBOX_ACCESS_TOKEN }}
|
14 |
-
APP_RISK_ADMIN_URL: ${{ vars.APP_RISK_ADMIN_URL }}
|
15 |
-
APP_RISK_API_ENDPOINT: ${{ vars.APP_RISK_API_ENDPOINT }}
|
16 |
-
APP_SENTRY_DSN: ${{ vars.APP_SENTRY_DSN }}
|
17 |
-
APP_SENTRY_NORMALIZE_DEPTH: ${{ vars.APP_SENTRY_NORMALIZE_DEPTH }}
|
18 |
-
APP_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.APP_SENTRY_TRACES_SAMPLE_RATE }}
|
19 |
-
APP_SHOW_ENV_BANNER: ${{ vars.APP_SHOW_ENV_BANNER }}
|
20 |
-
APP_TINY_API_KEY: ${{ vars.APP_TINY_API_KEY }}
|
21 |
-
APP_TITLE: ${{ vars.APP_TITLE }}
|
22 |
-
GITHUB_WORKFLOW: true
|
23 |
-
|
24 |
-
concurrency:
|
25 |
-
group: ${{ github.workflow }}-${{ github.ref }}
|
26 |
-
cancel-in-progress: true
|
27 |
-
|
28 |
-
jobs:
|
29 |
-
ui:
|
30 |
-
name: Build UI Library
|
31 |
-
environment: 'test'
|
32 |
-
runs-on: ubuntu-latest
|
33 |
-
defaults:
|
34 |
-
run:
|
35 |
-
working-directory: packages/ui
|
36 |
-
steps:
|
37 |
-
- uses: actions/checkout@v4
|
38 |
-
- name: Install pnpm
|
39 |
-
uses: pnpm/action-setup@v4
|
40 |
-
- name: Install Node.js
|
41 |
-
uses: actions/setup-node@v4
|
42 |
-
with:
|
43 |
-
node-version: 20
|
44 |
-
cache: 'pnpm'
|
45 |
-
- name: Install dependencies
|
46 |
-
run: pnpm install
|
47 |
-
|
48 |
-
- name: Typecheck
|
49 |
-
run: pnpm typecheck
|
50 |
-
|
51 |
-
- name: Lint CSS
|
52 |
-
run: pnpm lint:css
|
53 |
-
|
54 |
-
- name: Lint JS
|
55 |
-
run: pnpm lint:js
|
56 |
-
|
57 |
-
- name: Build
|
58 |
-
run: pnpm build
|
59 |
-
|
60 |
-
- uses: actions/upload-artifact@v4
|
61 |
-
with:
|
62 |
-
name: ui-build
|
63 |
-
path: packages/ui/dist
|
64 |
-
|
65 |
-
test:
|
66 |
-
name: Run tests
|
67 |
-
environment: 'test'
|
68 |
-
runs-on: ubuntu-latest
|
69 |
-
defaults:
|
70 |
-
run:
|
71 |
-
working-directory: app
|
72 |
-
needs: [ui]
|
73 |
-
steps:
|
74 |
-
- uses: actions/checkout@v4
|
75 |
-
- name: Install pnpm
|
76 |
-
uses: pnpm/action-setup@v4
|
77 |
-
- name: Install Node.js
|
78 |
-
uses: actions/setup-node@v4
|
79 |
-
with:
|
80 |
-
node-version: 20
|
81 |
-
cache: 'pnpm'
|
82 |
-
- name: Install dependencies
|
83 |
-
run: pnpm install
|
84 |
-
|
85 |
-
- uses: actions/download-artifact@v4
|
86 |
-
with:
|
87 |
-
name: ui-build
|
88 |
-
path: packages/ui/dist
|
89 |
-
|
90 |
-
- name: Run test
|
91 |
-
run: pnpm test
|
92 |
-
|
93 |
-
translation:
|
94 |
-
continue-on-error: true
|
95 |
-
name: Identify error with translation files
|
96 |
-
runs-on: ubuntu-latest
|
97 |
-
defaults:
|
98 |
-
run:
|
99 |
-
working-directory: app
|
100 |
-
needs: [ui]
|
101 |
-
steps:
|
102 |
-
- uses: actions/checkout@v4
|
103 |
-
- name: Install pnpm
|
104 |
-
uses: pnpm/action-setup@v4
|
105 |
-
- name: Install Node.js
|
106 |
-
uses: actions/setup-node@v4
|
107 |
-
with:
|
108 |
-
node-version: 20
|
109 |
-
cache: 'pnpm'
|
110 |
-
- name: Install dependencies
|
111 |
-
run: pnpm install
|
112 |
-
|
113 |
-
- uses: actions/download-artifact@v4
|
114 |
-
with:
|
115 |
-
name: ui-build
|
116 |
-
path: packages/ui/dist
|
117 |
-
|
118 |
-
- name: Identify error with translation files
|
119 |
-
run: pnpm lint:translation
|
120 |
-
|
121 |
-
translation-migrations:
|
122 |
-
if: |
|
123 |
-
(github.event_name == 'pull_request' && github.base_ref == 'develop') ||
|
124 |
-
(github.event_name == 'push' && github.ref == 'refs/heads/develop')
|
125 |
-
continue-on-error: true
|
126 |
-
name: Identify if translation migrations need to be generated
|
127 |
-
runs-on: ubuntu-latest
|
128 |
-
defaults:
|
129 |
-
run:
|
130 |
-
working-directory: app
|
131 |
-
needs: [ui]
|
132 |
-
steps:
|
133 |
-
- uses: actions/checkout@v4
|
134 |
-
- name: Install pnpm
|
135 |
-
uses: pnpm/action-setup@v4
|
136 |
-
- name: Install Node.js
|
137 |
-
uses: actions/setup-node@v4
|
138 |
-
with:
|
139 |
-
node-version: 20
|
140 |
-
cache: 'pnpm'
|
141 |
-
- name: Install dependencies
|
142 |
-
run: pnpm install
|
143 |
-
|
144 |
-
- uses: actions/download-artifact@v4
|
145 |
-
with:
|
146 |
-
name: ui-build
|
147 |
-
path: packages/ui/dist
|
148 |
-
|
149 |
-
- name: Identify if translation migrations need to be generated
|
150 |
-
run: |
|
151 |
-
if pnpm translatte:generate; then
|
152 |
-
# The step should fail if generation is possible
|
153 |
-
exit 1
|
154 |
-
fi
|
155 |
-
|
156 |
-
unused:
|
157 |
-
name: Identify unused files
|
158 |
-
runs-on: ubuntu-latest
|
159 |
-
needs: [ui]
|
160 |
-
steps:
|
161 |
-
- uses: actions/checkout@v4
|
162 |
-
- name: Install pnpm
|
163 |
-
uses: pnpm/action-setup@v4
|
164 |
-
- name: Install Node.js
|
165 |
-
uses: actions/setup-node@v4
|
166 |
-
with:
|
167 |
-
node-version: 20
|
168 |
-
cache: 'pnpm'
|
169 |
-
- name: Install dependencies
|
170 |
-
run: pnpm install
|
171 |
-
|
172 |
-
- name: Initialize types
|
173 |
-
run: pnpm initialize:type
|
174 |
-
working-directory: app
|
175 |
-
|
176 |
-
- name: Identify unused files
|
177 |
-
run: pnpm lint:unused
|
178 |
-
|
179 |
-
lint:
|
180 |
-
name: Lint JS
|
181 |
-
runs-on: ubuntu-latest
|
182 |
-
defaults:
|
183 |
-
run:
|
184 |
-
working-directory: app
|
185 |
-
needs: [ui]
|
186 |
-
steps:
|
187 |
-
- uses: actions/checkout@v4
|
188 |
-
- name: Install pnpm
|
189 |
-
uses: pnpm/action-setup@v4
|
190 |
-
- name: Install Node.js
|
191 |
-
uses: actions/setup-node@v4
|
192 |
-
with:
|
193 |
-
node-version: 20
|
194 |
-
cache: 'pnpm'
|
195 |
-
- name: Install dependencies
|
196 |
-
run: pnpm install
|
197 |
-
|
198 |
-
- uses: actions/download-artifact@v4
|
199 |
-
with:
|
200 |
-
name: ui-build
|
201 |
-
path: packages/ui/dist
|
202 |
-
|
203 |
-
- name: Lint JS
|
204 |
-
run: pnpm lint:js
|
205 |
-
|
206 |
-
lint-css:
|
207 |
-
name: Lint CSS
|
208 |
-
runs-on: ubuntu-latest
|
209 |
-
defaults:
|
210 |
-
run:
|
211 |
-
working-directory: app
|
212 |
-
needs: [ui]
|
213 |
-
steps:
|
214 |
-
- uses: actions/checkout@v4
|
215 |
-
- name: Install pnpm
|
216 |
-
uses: pnpm/action-setup@v4
|
217 |
-
- name: Install Node.js
|
218 |
-
uses: actions/setup-node@v4
|
219 |
-
with:
|
220 |
-
node-version: 20
|
221 |
-
cache: 'pnpm'
|
222 |
-
- name: Install dependencies
|
223 |
-
run: pnpm install
|
224 |
-
|
225 |
-
- uses: actions/download-artifact@v4
|
226 |
-
with:
|
227 |
-
name: ui-build
|
228 |
-
path: packages/ui/dist
|
229 |
-
|
230 |
-
- name: Lint CSS
|
231 |
-
run: pnpm lint:css
|
232 |
-
|
233 |
-
# FIXME: Identify a way to generate schema before we run typecheck
|
234 |
-
# typecheck:
|
235 |
-
# name: Typecheck
|
236 |
-
# runs-on: ubuntu-latest
|
237 |
-
# steps:
|
238 |
-
# - uses: actions/checkout@v4
|
239 |
-
# - name: Install pnpm
|
240 |
-
# uses: pnpm/action-setup@v4
|
241 |
-
# - name: Install Node.js
|
242 |
-
# uses: actions/setup-node@v4
|
243 |
-
# with:
|
244 |
-
# node-version: 20
|
245 |
-
# cache: 'pnpm'
|
246 |
-
# - name: Install dependencies
|
247 |
-
# run: pnpm install
|
248 |
-
#
|
249 |
-
# - name: Typecheck
|
250 |
-
# run: pnpm typecheck
|
251 |
-
|
252 |
-
typos:
|
253 |
-
name: Spell Check with Typos
|
254 |
-
runs-on: ubuntu-latest
|
255 |
-
steps:
|
256 |
-
- name: Checkout Actions Repository
|
257 |
-
uses: actions/checkout@v4
|
258 |
-
|
259 |
-
- name: Check spelling
|
260 |
-
uses: crate-ci/[email protected]
|
261 |
-
|
262 |
-
build:
|
263 |
-
name: Build GO Web App
|
264 |
-
environment: 'test'
|
265 |
-
runs-on: ubuntu-latest
|
266 |
-
defaults:
|
267 |
-
run:
|
268 |
-
working-directory: app
|
269 |
-
needs: [lint, lint-css, test, ui]
|
270 |
-
steps:
|
271 |
-
- uses: actions/checkout@v4
|
272 |
-
- name: Install pnpm
|
273 |
-
uses: pnpm/action-setup@v4
|
274 |
-
- name: Install Node.js
|
275 |
-
uses: actions/setup-node@v4
|
276 |
-
with:
|
277 |
-
node-version: 20
|
278 |
-
cache: 'pnpm'
|
279 |
-
- name: Install dependencies
|
280 |
-
run: pnpm install
|
281 |
-
|
282 |
-
- uses: actions/download-artifact@v4
|
283 |
-
with:
|
284 |
-
name: ui-build
|
285 |
-
path: packages/ui/dist
|
286 |
-
|
287 |
-
- name: Build
|
288 |
-
run: pnpm build
|
289 |
-
|
290 |
-
validate_helm:
|
291 |
-
name: Validate Helm
|
292 |
-
runs-on: ubuntu-latest
|
293 |
-
|
294 |
-
steps:
|
295 |
-
- uses: actions/checkout@main
|
296 |
-
|
297 |
-
- name: Install Helm
|
298 |
-
uses: azure/setup-helm@v4
|
299 |
-
|
300 |
-
- name: Helm lint
|
301 |
-
run: helm lint ./nginx-serve/helm --values ./nginx-serve/helm/values-test.yaml
|
302 |
-
|
303 |
-
- name: Helm template
|
304 |
-
run: helm template ./nginx-serve/helm --values ./nginx-serve/helm/values-test.yaml
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.github/workflows/publish-nginx-serve.yml
DELETED
@@ -1,147 +0,0 @@
|
|
1 |
-
name: Publish Helm
|
2 |
-
|
3 |
-
on:
|
4 |
-
workflow_dispatch:
|
5 |
-
push:
|
6 |
-
branches:
|
7 |
-
- develop
|
8 |
-
- project/*
|
9 |
-
|
10 |
-
permissions:
|
11 |
-
packages: write
|
12 |
-
|
13 |
-
|
14 |
-
jobs:
|
15 |
-
publish_image:
|
16 |
-
name: Publish Docker Image
|
17 |
-
runs-on: ubuntu-latest
|
18 |
-
|
19 |
-
outputs:
|
20 |
-
docker_image_name: ${{ steps.prep.outputs.tagged_image_name }}
|
21 |
-
docker_image_tag: ${{ steps.prep.outputs.tag }}
|
22 |
-
docker_image: ${{ steps.prep.outputs.tagged_image }}
|
23 |
-
|
24 |
-
steps:
|
25 |
-
- uses: actions/checkout@main
|
26 |
-
|
27 |
-
- name: Login to GitHub Container Registry
|
28 |
-
uses: docker/login-action@v3
|
29 |
-
with:
|
30 |
-
registry: ghcr.io
|
31 |
-
username: ${{ github.actor }}
|
32 |
-
password: ${{ secrets.GITHUB_TOKEN }}
|
33 |
-
|
34 |
-
- name: 🐳 Prepare Docker
|
35 |
-
id: prep
|
36 |
-
env:
|
37 |
-
IMAGE_NAME: ghcr.io/${{ github.repository }}
|
38 |
-
run: |
|
39 |
-
BRANCH_NAME=$(echo $GITHUB_REF_NAME | sed 's|[/:]|-|' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g' | cut -c1-100 | sed 's/-*$//')
|
40 |
-
|
41 |
-
# XXX: Check if there is a slash in the BRANCH_NAME eg: project/add-docker
|
42 |
-
if [[ "$BRANCH_NAME" == *"/"* ]]; then
|
43 |
-
# XXX: Change the docker image package to -alpha
|
44 |
-
IMAGE_NAME="$IMAGE_NAME-alpha"
|
45 |
-
TAG="$(echo "$BRANCH_NAME" | sed 's|/|-|g').$(echo $GITHUB_SHA | head -c7)"
|
46 |
-
else
|
47 |
-
TAG="$BRANCH_NAME.$(echo $GITHUB_SHA | head -c7)"
|
48 |
-
fi
|
49 |
-
|
50 |
-
IMAGE_NAME=$(echo $IMAGE_NAME | tr '[:upper:]' '[:lower:]')
|
51 |
-
echo "tagged_image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT
|
52 |
-
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
53 |
-
echo "tagged_image=${IMAGE_NAME}:${TAG}" >> $GITHUB_OUTPUT
|
54 |
-
echo "::notice::Tagged docker image: ${IMAGE_NAME}:${TAG}"
|
55 |
-
|
56 |
-
- name: 🐳 Set up Docker Buildx
|
57 |
-
id: buildx
|
58 |
-
uses: docker/setup-buildx-action@v3
|
59 |
-
|
60 |
-
- name: 🐳 Cache Docker layers
|
61 |
-
uses: actions/cache@v4
|
62 |
-
with:
|
63 |
-
path: /tmp/.buildx-cache
|
64 |
-
key: ${{ runner.os }}-buildx-${{ github.ref }}
|
65 |
-
restore-keys: |
|
66 |
-
${{ runner.os }}-buildx-refs/develop
|
67 |
-
${{ runner.os }}-buildx-
|
68 |
-
|
69 |
-
- name: 🐳 Docker build
|
70 |
-
uses: docker/build-push-action@v6
|
71 |
-
with:
|
72 |
-
context: .
|
73 |
-
builder: ${{ steps.buildx.outputs.name }}
|
74 |
-
file: nginx-serve/Dockerfile
|
75 |
-
target: nginx-serve
|
76 |
-
load: true
|
77 |
-
push: true
|
78 |
-
tags: ${{ steps.prep.outputs.tagged_image }}
|
79 |
-
cache-from: type=local,src=/tmp/.buildx-cache
|
80 |
-
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
81 |
-
build-args: |
|
82 |
-
"APP_SENTRY_TRACES_SAMPLE_RATE=0.8"
|
83 |
-
"APP_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0.8"
|
84 |
-
"APP_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=0.8"
|
85 |
-
|
86 |
-
- name: 🐳 Move docker cache
|
87 |
-
run: |
|
88 |
-
rm -rf /tmp/.buildx-cache
|
89 |
-
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
90 |
-
|
91 |
-
publish_helm:
|
92 |
-
name: Publish Helm
|
93 |
-
needs: publish_image
|
94 |
-
runs-on: ubuntu-latest
|
95 |
-
|
96 |
-
steps:
|
97 |
-
- name: Checkout code
|
98 |
-
uses: actions/checkout@v4
|
99 |
-
|
100 |
-
- name: Login to GitHub Container Registry
|
101 |
-
uses: docker/login-action@v3
|
102 |
-
with:
|
103 |
-
registry: ghcr.io
|
104 |
-
username: ${{ github.actor }}
|
105 |
-
password: ${{ secrets.GITHUB_TOKEN }}
|
106 |
-
|
107 |
-
- name: Install Helm
|
108 |
-
uses: azure/setup-helm@v3
|
109 |
-
|
110 |
-
- name: Tag docker image in Helm Chart values.yaml
|
111 |
-
env:
|
112 |
-
IMAGE_NAME: ${{ needs.publish_image.outputs.docker_image_name }}
|
113 |
-
IMAGE_TAG: ${{ needs.publish_image.outputs.docker_image_tag }}
|
114 |
-
run: |
|
115 |
-
# Update values.yaml with latest docker image
|
116 |
-
sed -i "s|SET-BY-CICD-IMAGE|$IMAGE_NAME|" nginx-serve/helm/values.yaml
|
117 |
-
sed -i "s/SET-BY-CICD-TAG/$IMAGE_TAG/" nginx-serve/helm/values.yaml
|
118 |
-
|
119 |
-
- name: Package Helm Chart
|
120 |
-
id: set-variables
|
121 |
-
run: |
|
122 |
-
# XXX: Check if there is a slash in the BRANCH_NAME eg: project/add-docker
|
123 |
-
if [[ "$GITHUB_REF_NAME" == *"/"* ]]; then
|
124 |
-
# XXX: Change the helm chart to <chart-name>-alpha
|
125 |
-
sed -i 's/^name: \(.*\)/name: \1-alpha/' nginx-serve/helm/Chart.yaml
|
126 |
-
fi
|
127 |
-
|
128 |
-
SHA_SHORT=$(git rev-parse --short HEAD)
|
129 |
-
sed -i "s/SET-BY-CICD/$SHA_SHORT/g" nginx-serve/helm/Chart.yaml
|
130 |
-
helm package ./nginx-serve/helm -d .helm-charts
|
131 |
-
|
132 |
-
- name: Push Helm Chart
|
133 |
-
env:
|
134 |
-
IMAGE: ${{ needs.publish_image.outputs.docker_image }}
|
135 |
-
OCI_REPO: oci://ghcr.io/${{ github.repository }}
|
136 |
-
run: |
|
137 |
-
OCI_REPO=$(echo $OCI_REPO | tr '[:upper:]' '[:lower:]')
|
138 |
-
PACKAGE_FILE=$(ls .helm-charts/*.tgz | head -n 1)
|
139 |
-
echo "# Helm Chart" >> $GITHUB_STEP_SUMMARY
|
140 |
-
echo "" >> $GITHUB_STEP_SUMMARY
|
141 |
-
echo "Tagged Image: **$IMAGE**" >> $GITHUB_STEP_SUMMARY
|
142 |
-
echo "" >> $GITHUB_STEP_SUMMARY
|
143 |
-
echo "Helm push output" >> $GITHUB_STEP_SUMMARY
|
144 |
-
echo "" >> $GITHUB_STEP_SUMMARY
|
145 |
-
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
146 |
-
helm push "$PACKAGE_FILE" $OCI_REPO >> $GITHUB_STEP_SUMMARY
|
147 |
-
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.github/workflows/publish-storybook-nginx-serve.yml
DELETED
@@ -1,127 +0,0 @@
|
|
1 |
-
name: Publish Storybook Helm
|
2 |
-
|
3 |
-
on:
|
4 |
-
workflow_dispatch:
|
5 |
-
push:
|
6 |
-
branches:
|
7 |
-
- develop
|
8 |
-
|
9 |
-
permissions:
|
10 |
-
packages: write
|
11 |
-
|
12 |
-
|
13 |
-
jobs:
|
14 |
-
publish_image:
|
15 |
-
name: 🐳 Publish Docker Image
|
16 |
-
runs-on: ubuntu-latest
|
17 |
-
|
18 |
-
outputs:
|
19 |
-
docker_image_name: ${{ steps.prep.outputs.tagged_image_name }}
|
20 |
-
docker_image_tag: ${{ steps.prep.outputs.tag }}
|
21 |
-
docker_image: ${{ steps.prep.outputs.tagged_image }}
|
22 |
-
|
23 |
-
steps:
|
24 |
-
- uses: actions/checkout@main
|
25 |
-
|
26 |
-
- name: Login to GitHub Container Registry
|
27 |
-
uses: docker/login-action@v3
|
28 |
-
with:
|
29 |
-
registry: ghcr.io
|
30 |
-
username: ${{ github.actor }}
|
31 |
-
password: ${{ secrets.GITHUB_TOKEN }}
|
32 |
-
|
33 |
-
- name: 🐳 Prepare Docker
|
34 |
-
id: prep
|
35 |
-
env:
|
36 |
-
IMAGE_NAME: ghcr.io/${{ github.repository }}/go-ui-storybook
|
37 |
-
run: |
|
38 |
-
BRANCH_NAME=$(echo $GITHUB_REF_NAME | sed 's|[/:]|-|' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g' | cut -c1-100 | sed 's/-*$//')
|
39 |
-
TAG="$BRANCH_NAME.$(echo $GITHUB_SHA | head -c7)"
|
40 |
-
IMAGE_NAME=$(echo $IMAGE_NAME | tr '[:upper:]' '[:lower:]')
|
41 |
-
echo "tagged_image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT
|
42 |
-
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
43 |
-
echo "tagged_image=${IMAGE_NAME}:${TAG}" >> $GITHUB_OUTPUT
|
44 |
-
echo "::notice::Tagged docker image: ${IMAGE_NAME}:${TAG}"
|
45 |
-
|
46 |
-
- name: 🐳 Set up Docker Buildx
|
47 |
-
id: buildx
|
48 |
-
uses: docker/setup-buildx-action@v3
|
49 |
-
|
50 |
-
- name: 🐳 Cache Docker layers
|
51 |
-
uses: actions/cache@v4
|
52 |
-
with:
|
53 |
-
path: /tmp/.buildx-cache
|
54 |
-
key: ${{ runner.os }}-buildx-${{ github.ref }}
|
55 |
-
restore-keys: |
|
56 |
-
${{ runner.os }}-buildx-refs/develop
|
57 |
-
${{ runner.os }}-buildx-
|
58 |
-
|
59 |
-
- name: 🐳 Docker build
|
60 |
-
uses: docker/build-push-action@v6
|
61 |
-
with:
|
62 |
-
context: .
|
63 |
-
builder: ${{ steps.buildx.outputs.name }}
|
64 |
-
file: packages/go-ui-storybook/nginx-serve/Dockerfile
|
65 |
-
target: nginx-serve
|
66 |
-
load: true
|
67 |
-
push: true
|
68 |
-
tags: ${{ steps.prep.outputs.tagged_image }}
|
69 |
-
cache-from: type=local,src=/tmp/.buildx-cache
|
70 |
-
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
71 |
-
|
72 |
-
- name: 🐳 Move docker cache
|
73 |
-
run: |
|
74 |
-
rm -rf /tmp/.buildx-cache
|
75 |
-
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
76 |
-
|
77 |
-
publish_helm:
|
78 |
-
name: ⎈ Publish Helm
|
79 |
-
needs: publish_image
|
80 |
-
runs-on: ubuntu-latest
|
81 |
-
|
82 |
-
steps:
|
83 |
-
- name: Checkout code
|
84 |
-
uses: actions/checkout@v4
|
85 |
-
|
86 |
-
- name: Login to GitHub Container Registry
|
87 |
-
uses: docker/login-action@v3
|
88 |
-
with:
|
89 |
-
registry: ghcr.io
|
90 |
-
username: ${{ github.actor }}
|
91 |
-
password: ${{ secrets.GITHUB_TOKEN }}
|
92 |
-
|
93 |
-
- name: ⎈ Install Helm
|
94 |
-
uses: azure/setup-helm@v3
|
95 |
-
|
96 |
-
- name: ⎈ Tag docker image in Helm Chart values.yaml
|
97 |
-
env:
|
98 |
-
IMAGE_NAME: ${{ needs.publish_image.outputs.docker_image_name }}
|
99 |
-
IMAGE_TAG: ${{ needs.publish_image.outputs.docker_image_tag }}
|
100 |
-
run: |
|
101 |
-
# Update values.yaml with latest docker image
|
102 |
-
sed -i "s|SET-BY-CICD-IMAGE|$IMAGE_NAME|" packages/go-ui-storybook/nginx-serve/helm/values.yaml
|
103 |
-
sed -i "s/SET-BY-CICD-TAG/$IMAGE_TAG/" packages/go-ui-storybook/nginx-serve/helm/values.yaml
|
104 |
-
|
105 |
-
- name: ⎈ Package Helm Chart
|
106 |
-
id: set-variables
|
107 |
-
run: |
|
108 |
-
SHA_SHORT=$(git rev-parse --short HEAD)
|
109 |
-
sed -i "s/SET-BY-CICD/$SHA_SHORT/g" packages/go-ui-storybook/nginx-serve/helm/Chart.yaml
|
110 |
-
helm package ./packages/go-ui-storybook/nginx-serve/helm -d .helm-charts
|
111 |
-
|
112 |
-
- name: ⎈ Push Helm Chart
|
113 |
-
env:
|
114 |
-
IMAGE: ${{ needs.publish_image.outputs.docker_image }}
|
115 |
-
OCI_REPO: oci://ghcr.io/${{ github.repository }}
|
116 |
-
run: |
|
117 |
-
OCI_REPO=$(echo $OCI_REPO | tr '[:upper:]' '[:lower:]')
|
118 |
-
PACKAGE_FILE=$(ls .helm-charts/*.tgz | head -n 1)
|
119 |
-
echo "## 🚀 IFRC GO UI Helm Chart 🚀" >> $GITHUB_STEP_SUMMARY
|
120 |
-
echo "" >> $GITHUB_STEP_SUMMARY
|
121 |
-
echo "🐳 Tagged Image: **$IMAGE**" >> $GITHUB_STEP_SUMMARY
|
122 |
-
echo "" >> $GITHUB_STEP_SUMMARY
|
123 |
-
echo "⎈ Helm push output" >> $GITHUB_STEP_SUMMARY
|
124 |
-
echo "" >> $GITHUB_STEP_SUMMARY
|
125 |
-
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
126 |
-
helm push "$PACKAGE_FILE" $OCI_REPO >> $GITHUB_STEP_SUMMARY
|
127 |
-
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.gitignore
DELETED
@@ -1,43 +0,0 @@
|
|
1 |
-
# Logs
|
2 |
-
logs
|
3 |
-
*.log
|
4 |
-
npm-debug.log*
|
5 |
-
yarn-debug.log*
|
6 |
-
yarn-error.log*
|
7 |
-
pnpm-debug.log*
|
8 |
-
lerna-debug.log*
|
9 |
-
|
10 |
-
node_modules
|
11 |
-
dist
|
12 |
-
dist-ssr
|
13 |
-
build
|
14 |
-
build-ssr
|
15 |
-
*.local
|
16 |
-
|
17 |
-
# Editor directories and files
|
18 |
-
.vscode/*
|
19 |
-
!.vscode/extensions.json
|
20 |
-
.idea
|
21 |
-
.DS_Store
|
22 |
-
*.suo
|
23 |
-
*.ntvs*
|
24 |
-
*.njsproj
|
25 |
-
*.sln
|
26 |
-
*.sw?
|
27 |
-
|
28 |
-
.env*
|
29 |
-
!.env.example
|
30 |
-
.eslintcache
|
31 |
-
tsconfig.tsbuildinfo
|
32 |
-
|
33 |
-
# Custom ignores
|
34 |
-
|
35 |
-
stats.html
|
36 |
-
generated/
|
37 |
-
coverage/
|
38 |
-
|
39 |
-
# storybook build
|
40 |
-
storybook-static/
|
41 |
-
|
42 |
-
# Helm
|
43 |
-
.helm-charts/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/.npmrc
DELETED
@@ -1 +0,0 @@
|
|
1 |
-
enable-pre-post-scripts=true
|
|
|
|
go-web-app-develop/COLLABORATING.md
DELETED
@@ -1,18 +0,0 @@
|
|
1 |
-
# IFRC GO Collaboration Guide
|
2 |
-
|
3 |
-
This document offers guidelines for collaborators on codebase maintenance, testing, building and deployment, and issue management.
|
4 |
-
|
5 |
-
## Repository
|
6 |
-
|
7 |
-
* [Issues and Pull Requests](./collaborating/issues-and-pull-requests.md)
|
8 |
-
* [Structure](./collaborating/repository-structure.md)
|
9 |
-
* [Linting](./collaborating/linting.md)
|
10 |
-
* [Technology Used](./collaborating/technology.md)
|
11 |
-
|
12 |
-
## Development
|
13 |
-
|
14 |
-
* [Developing](./collaborating/developing.md)
|
15 |
-
* [Translation](./collaborating/translation.md)
|
16 |
-
* [Building](./collaborating/building.md)
|
17 |
-
* [Testing](./collaborating/testing.md)
|
18 |
-
* [Release](./collaborating/release.md)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
go-web-app-develop/CONTRIBUTING.md
DELETED
@@ -1,81 +0,0 @@
|
|
1 |
-
# IFRC GO Web Application Contributing Guide
|
2 |
-
|
3 |
-
First off, thanks for taking the time to contribute! ❤️
|
4 |
-
|
5 |
-
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution.
|
6 |
-
|
7 |
-
## Table of Contents
|
8 |
-
|
9 |
-
* [I Have a Question](#i-have-a-question)
|
10 |
-
* [I Want To Contribute](#i-want-to-contribute)
|
11 |
-
* [What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
12 |
-
* [Reporting Bugs](#reporting-bugs)
|
13 |
-
* [Suggesting Enhancements](#suggesting-enhancements)
|
14 |
-
* [Becoming a Collaborator](#becoming-a-collaborator)
|
15 |
-
|
16 |
-
## I Have a Question
|
17 |
-
|
18 |
-
> If you want to ask a question, we assume that you have read the available [documentation](https://go-wiki.ifrc.org/en/home).
|
19 |
-
|
20 |
-
Before you ask a question, it is best to search for existing [issues](https://github.com/IFRCGo/go-web-app/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue.
|
21 |
-
|
22 |
-
If you then still feel the need to ask a question and need clarification, we recommend the following:
|
23 |
-
|
24 |
-
* Open a [discussion](https://github.com/IFRCGo/go-web-app/discussions).
|
25 |
-
* Open an [issue](https://github.com/IFRCGo/go-web-app/issues/new/choose).
|
26 |
-
* Provide as much context as you can about what you're running into.
|
27 |
-
|
28 |
-
## I Want To Contribute
|
29 |
-
|
30 |
-
Any individual is welcome to contribute to IFRC GO. The repository currently has two kinds of contribution personas:
|
31 |
-
|
32 |
-
* A **Contributor** is any individual who creates an issue/PR, comments on an issue/PR, or contributes in some other way.
|
33 |
-
* A **Collaborator** is a contributor with write access to the repository.
|
34 |
-
|
35 |
-
### What should I know before I get started?
|
36 |
-
|
37 |
-
### IFRC GO and Packages
|
38 |
-
|
39 |
-
The project is hosted at <https://go.ifrc.org/>.
|
40 |
-
|
41 |
-
The project comprises several [repositories](https://github.com/orgs/IFRCGo/repositories), with notable ones including:
|
42 |
-
|
43 |
-
* [go-web-app](https://github.com/IFRCGo/go-web-app/) - The frontend repository for the IFRC GO project.
|
44 |
-
* [go-api](https://github.com/IFRCGo/go-api) - The backed repository for the IFRC GO project.
|
45 |
-
|
46 |
-
### Reporting Bugs
|
47 |
-
|
48 |
-
#### Before Submitting a Bug Report
|
49 |
-
|
50 |
-
Ensure the issue is not a user error by reviewing the documentation. Check the [existing bug reports](https://github.com/IFRCGo/go-web-app/issues?q=is%3Aissue%20state%3Aopen%20type%3ABug) to confirm if the issue has already been reported.
|
51 |
-
|
52 |
-
#### Submitting the Bug Report
|
53 |
-
|
54 |
-
1. Open a new [Issue](https://github.com/IFRCGo/go-web-app/issues/new?q=is%3Aissue+state%3Aopen+type%3ABug\&template=01_bug_report.yml).
|
55 |
-
2. Provide all relevant details.
|
56 |
-
|
57 |
-
#### After Submitting the Issue
|
58 |
-
|
59 |
-
* The team will categorize and attempt to reproduce the issue.
|
60 |
-
* If reproducible, the team will work on resolving the bug.
|
61 |
-
|
62 |
-
### Suggesting Enhancements
|
63 |
-
|
64 |
-
#### Before Submitting an Enhancement
|
65 |
-
|
66 |
-
* Review the [documentation](https://go-wiki.ifrc.org/en/home) to ensure the functionality isn't already covered.
|
67 |
-
* Perform a [search](https://github.com/IFRCGo/go-web-app/issues) to check if the enhancement has been suggested. If so, comment on the existing issue.
|
68 |
-
* Confirm that your suggestion aligns with the project’s scope and objectives.
|
69 |
-
|
70 |
-
#### How to Submit an Enhancement Suggestion
|
71 |
-
|
72 |
-
Enhancements are tracked as [GitHub issues](https://github.com/IFRCGo/go-web-app/issues).
|
73 |
-
|
74 |
-
* Open a new [feature request](https://github.com/IFRCGo/go-web-app/issues/new?q=is%3Aissue+state%3Aopen+type%3ABug\&template=02_feature_request.yml) or [Epic ticket](https://github.com/IFRCGo/go-web-app/issues/new?q=is%3Aissue+state%3Aopen+type%3ABug\&template=03_epic_request.yml) depending on the scale of the enhancement.
|
75 |
-
* Provide a clear description and submit the ticket.
|
76 |
-
|
77 |
-
## Becoming a Collaborator
|
78 |
-
|
79 |
-
Collaborators are key members of the IFRC GO Web Application Team, responsible for its development. Members should have expertise in modern web technologies and standards.
|
80 |
-
|
81 |
-
For detailed guidelines, refer to the [Collaboration Guide](./COLLABORATING.md).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|