SCGR commited on
Commit
26c355d
·
1 Parent(s): 65933cd

map details page

Browse files
frontend/src/components/HeaderNav.tsx CHANGED
@@ -52,6 +52,7 @@ export default function HeaderNav() {
52
  <nav className="flex items-center space-x-4 bg-gray-50/80 rounded-xl p-2 backdrop-blur-sm">
53
  {navItems.map(({ to, label, Icon }) => {
54
  const isActive = location.pathname === to ||
 
55
  (to === '/explore' && location.pathname.startsWith('/map/'));
56
  return (
57
  <div key={to} className="relative">
@@ -94,9 +95,13 @@ export default function HeaderNav() {
94
 
95
  <Button
96
  name="help"
97
- variant="tertiary"
98
  size={1}
99
- className="transition-all duration-200 hover:bg-blue-50 hover:text-blue-600 hover:shadow-md hover:scale-105"
 
 
 
 
100
  onClick={() => {
101
  if (location.pathname === "/upload") {
102
  if ((window as any).confirmNavigationIfNeeded) {
 
52
  <nav className="flex items-center space-x-4 bg-gray-50/80 rounded-xl p-2 backdrop-blur-sm">
53
  {navItems.map(({ to, label, Icon }) => {
54
  const isActive = location.pathname === to ||
55
+ (to === '/upload' && location.pathname === '/') ||
56
  (to === '/explore' && location.pathname.startsWith('/map/'));
57
  return (
58
  <div key={to} className="relative">
 
95
 
96
  <Button
97
  name="help"
98
+ variant={location.pathname === '/help' ? "primary" : "tertiary"}
99
  size={1}
100
+ className={`transition-all duration-200 ${
101
+ location.pathname === '/help'
102
+ ? 'shadow-lg shadow-ifrcRed/20 transform scale-105'
103
+ : 'hover:bg-blue-50 hover:text-blue-600 hover:shadow-md hover:scale-105'
104
+ }`}
105
  onClick={() => {
106
  if (location.pathname === "/upload") {
107
  if ((window as any).confirmNavigationIfNeeded) {
frontend/src/pages/ExplorePage/ExplorePage.tsx CHANGED
@@ -195,61 +195,71 @@ export default function ExplorePage() {
195
  {view === 'explore' ? (
196
  <div className="space-y-6">
197
  {/* Search and Filters */}
198
- <div className="space-y-4">
199
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
200
- <TextInput
201
- name="search"
202
- placeholder="Search examples..."
203
- value={search}
204
- onChange={(v) => setSearch(v || '')}
205
- />
 
 
206
 
207
- <SelectInput
208
- name="source"
209
- placeholder={isLoadingFilters ? "Loading..." : "All Sources"}
210
- options={sources}
211
- value={srcFilter || null}
212
- onChange={(v) => setSrcFilter(v as string || '')}
213
- keySelector={(o) => o.s_code}
214
- labelSelector={(o) => o.label}
215
- required={false}
216
- disabled={isLoadingFilters}
217
- />
 
 
218
 
219
- <SelectInput
220
- name="category"
221
- placeholder={isLoadingFilters ? "Loading..." : "All Categories"}
222
- options={types}
223
- value={catFilter || null}
224
- onChange={(v) => setCatFilter(v as string || '')}
225
- keySelector={(o) => o.t_code}
226
- labelSelector={(o) => o.label}
227
- required={false}
228
- disabled={isLoadingFilters}
229
- />
 
 
230
 
231
- <SelectInput
232
- name="region"
233
- placeholder={isLoadingFilters ? "Loading..." : "All Regions"}
234
- options={regions}
235
- value={regionFilter || null}
236
- onChange={(v) => setRegionFilter(v as string || '')}
237
- keySelector={(o) => o.r_code}
238
- labelSelector={(o) => o.label}
239
- required={false}
240
- disabled={isLoadingFilters}
241
- />
 
 
242
 
243
- <MultiSelectInput
244
- name="country"
245
- placeholder={isLoadingFilters ? "Loading..." : "All Countries"}
246
- options={countries}
247
- value={countryFilter ? [countryFilter] : []}
248
- onChange={(v) => setCountryFilter((v as string[])[0] || '')}
249
- keySelector={(o) => o.c_code}
250
- labelSelector={(o) => o.label}
251
- disabled={isLoadingFilters}
252
- />
 
 
253
  </div>
254
  </div>
255
 
 
195
  {view === 'explore' ? (
196
  <div className="space-y-6">
197
  {/* Search and Filters */}
198
+ <div className="mb-6">
199
+ <div className="flex flex-wrap items-center gap-4">
200
+ <Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2 flex-1 min-w-[300px]">
201
+ <TextInput
202
+ name="search"
203
+ placeholder="Search examples..."
204
+ value={search}
205
+ onChange={(v) => setSearch(v || '')}
206
+ />
207
+ </Container>
208
 
209
+ <Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
210
+ <SelectInput
211
+ name="source"
212
+ placeholder={isLoadingFilters ? "Loading..." : "All Sources"}
213
+ options={sources}
214
+ value={srcFilter || null}
215
+ onChange={(v) => setSrcFilter(v as string || '')}
216
+ keySelector={(o) => o.s_code}
217
+ labelSelector={(o) => o.label}
218
+ required={false}
219
+ disabled={isLoadingFilters}
220
+ />
221
+ </Container>
222
 
223
+ <Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
224
+ <SelectInput
225
+ name="category"
226
+ placeholder={isLoadingFilters ? "Loading..." : "All Categories"}
227
+ options={types}
228
+ value={catFilter || null}
229
+ onChange={(v) => setCatFilter(v as string || '')}
230
+ keySelector={(o) => o.t_code}
231
+ labelSelector={(o) => o.label}
232
+ required={false}
233
+ disabled={isLoadingFilters}
234
+ />
235
+ </Container>
236
 
237
+ <Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
238
+ <SelectInput
239
+ name="region"
240
+ placeholder={isLoadingFilters ? "Loading..." : "All Regions"}
241
+ options={regions}
242
+ value={regionFilter || null}
243
+ onChange={(v) => setRegionFilter(v as string || '')}
244
+ keySelector={(o) => o.r_code}
245
+ labelSelector={(o) => o.label}
246
+ required={false}
247
+ disabled={isLoadingFilters}
248
+ />
249
+ </Container>
250
 
251
+ <Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
252
+ <MultiSelectInput
253
+ name="country"
254
+ placeholder={isLoadingFilters ? "Loading..." : "All Countries"}
255
+ options={countries}
256
+ value={countryFilter ? [countryFilter] : []}
257
+ onChange={(v) => setCountryFilter((v as string[])[0] || '')}
258
+ keySelector={(o) => o.c_code}
259
+ labelSelector={(o) => o.label}
260
+ disabled={isLoadingFilters}
261
+ />
262
+ </Container>
263
  </div>
264
  </div>
265
 
frontend/src/pages/HelpPage.tsx CHANGED
@@ -1,9 +1,44 @@
1
- import { PageContainer, Heading } from '@ifrc-go/ui';
2
 
3
  export default function HelpPage() {
4
  return (
5
- <PageContainer className="py-10 text-center">
6
- <Heading level={2}>Help &amp; Support</Heading>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  </PageContainer>
8
  );
9
  }
 
1
+ import { PageContainer, Heading, Container } from '@ifrc-go/ui';
2
 
3
  export default function HelpPage() {
4
  return (
5
+ <PageContainer className="py-10">
6
+ <Container withInternalPadding className="max-w-4xl mx-auto">
7
+ <Heading level={2} className="text-center mb-12 text-gray-900">Help &amp; Support</Heading>
8
+
9
+ <div className="space-y-8">
10
+ <Container withInternalPadding className="p-8">
11
+ <Heading level={3} className="mb-4 text-ifrcRed font-semibold">Introduction</Heading>
12
+ <p className="text-gray-700 leading-relaxed text-base">
13
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore
14
+ et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
15
+ aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
16
+ cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
17
+ culpa qui officia deserunt mollit anim id est laborum.
18
+ </p>
19
+ </Container>
20
+
21
+ <Container withInternalPadding className="p-8">
22
+ <Heading level={3} className="mb-4 text-ifrcRed font-semibold">Guidelines</Heading>
23
+ <p className="text-gray-700 leading-relaxed text-base">
24
+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
25
+ totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae
26
+ dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit,
27
+ sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
28
+ </p>
29
+ </Container>
30
+
31
+ <Container withInternalPadding className="p-8">
32
+ <Heading level={3} className="mb-4 text-ifrcRed font-semibold">VLMs</Heading>
33
+ <p className="text-gray-700 leading-relaxed text-base">
34
+ At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum
35
+ deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non
36
+ provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum
37
+ fuga. Et harum quidem rerum facilis est et expedita distinctio.
38
+ </p>
39
+ </Container>
40
+ </div>
41
+ </Container>
42
  </PageContainer>
43
  );
44
  }
frontend/src/pages/MapDetailsPage/MapDetailPage.tsx CHANGED
@@ -1,6 +1,7 @@
1
- import { PageContainer, Container, Button, Spinner, SegmentInput } from '@ifrc-go/ui';
2
  import { useParams, useNavigate } from 'react-router-dom';
3
- import { useState, useEffect } from 'react';
 
4
  import styles from './MapDetailPage.module.css';
5
 
6
  interface MapOut {
@@ -43,6 +44,19 @@ export default function MapDetailPage() {
43
  const [types, setTypes] = useState<{t_code: string, label: string}[]>([]);
44
  const [imageTypes, setImageTypes] = useState<{image_type: string, label: string}[]>([]);
45
  const [regions, setRegions] = useState<{r_code: string, label: string}[]>([]);
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  const viewOptions = [
48
  { key: 'explore' as const, label: 'Explore' },
@@ -56,38 +70,113 @@ export default function MapDetailPage() {
56
  return;
57
  }
58
 
59
- fetch(`/api/images/${mapId}`)
60
- .then(response => {
61
- if (!response.ok) {
62
- throw new Error('Map not found');
63
- }
64
- return response.json();
65
- })
66
- .then(data => {
67
- setMap(data);
68
- setLoading(false);
69
- })
70
- .catch(err => {
71
- setError(err.message);
72
- setLoading(false);
73
- });
74
  }, [mapId]);
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  useEffect(() => {
77
  Promise.all([
78
  fetch('/api/sources').then(r => r.json()),
79
  fetch('/api/types').then(r => r.json()),
80
  fetch('/api/image-types').then(r => r.json()),
81
  fetch('/api/regions').then(r => r.json()),
82
- ]).then(([sourcesData, typesData, imageTypesData, regionsData]) => {
 
83
  setSources(sourcesData);
84
  setTypes(typesData);
85
  setImageTypes(imageTypesData);
86
  setRegions(regionsData);
 
87
  }).catch(console.error);
88
  }, []);
89
 
90
  const [isGenerating, setIsGenerating] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  const handleContribute = async () => {
93
  if (!map) return;
@@ -205,94 +294,236 @@ export default function MapDetailPage() {
205
  />
206
  </div>
207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  {view === 'mapDetails' ? (
209
- <>
210
- <div className={styles.gridLayout}>
211
- {/* Image Section */}
212
- <Container
213
- heading={map.title || "Map Image"}
214
- headingLevel={2}
215
- withHeaderBorder
216
- withInternalPadding
217
- spacing="comfortable"
218
- >
219
- <div className={styles.imageContainer}>
220
- {map.image_url ? (
221
- <img
222
- src={map.image_url}
223
- alt={map.file_key}
224
- />
225
- ) : (
226
- <div className={styles.imagePlaceholder}>
227
- No image available
 
 
 
 
228
  </div>
229
- )}
230
- </div>
231
- </Container>
232
 
233
- {/* Details Section */}
234
- <div className={styles.detailsSection}>
235
- <Container
236
- heading="Tags"
237
- headingLevel={3}
238
- withHeaderBorder
239
- withInternalPadding
240
- spacing="comfortable"
241
- >
242
- <div className={styles.metadataTags}>
243
- <span className={styles.metadataTag}>
244
- {sources.find(s => s.s_code === map.source)?.label || map.source}
245
- </span>
246
- <span className={styles.metadataTag}>
247
- {types.find(t => t.t_code === map.event_type)?.label || map.event_type}
248
- </span>
249
- <span className={styles.metadataTag}>
250
- {imageTypes.find(it => it.image_type === map.image_type)?.label || map.image_type}
251
- </span>
252
- {map.countries && map.countries.length > 0 && (
253
- <>
254
  <span className={styles.metadataTag}>
255
- {regions.find(r => r.r_code === map.countries[0].r_code)?.label || 'Unknown Region'}
256
  </span>
257
  <span className={styles.metadataTag}>
258
- {map.countries.map(country => country.label).join(', ')}
259
  </span>
260
- </>
261
- )}
262
- </div>
263
- </Container>
 
 
 
 
 
 
 
 
 
 
 
264
 
265
- <Container
266
- heading="Description"
267
- headingLevel={3}
268
- withHeaderBorder
269
- withInternalPadding
270
- spacing="comfortable"
271
- >
272
- <div className={styles.captionContainer}>
273
- {map.generated ? (
274
- <div className={styles.captionText}>
275
- <p>{map.edited || map.generated}</p>
 
 
 
 
276
  </div>
277
- ) : (
278
- <p>— no caption yet —</p>
279
- )}
280
  </div>
281
- </Container>
282
- </div>
283
- </div>
284
 
285
- {/* Contribute Section */}
286
- <div className="flex justify-center mt-8">
287
- <Button
288
- name="contribute"
289
- onClick={handleContribute}
290
- disabled={isGenerating}
291
- >
292
- {isGenerating ? 'Generating...' : 'Contribute'}
293
- </Button>
294
- </div>
295
- </>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  ) : null}
297
  </Container>
298
  </PageContainer>
 
1
+ import { PageContainer, Container, Button, Spinner, SegmentInput, IconButton, TextInput, SelectInput, MultiSelectInput } from '@ifrc-go/ui';
2
  import { useParams, useNavigate } from 'react-router-dom';
3
+ import { useState, useEffect, useMemo } from 'react';
4
+ import { ChevronLeftLineIcon, ChevronRightLineIcon } from '@ifrc-go/icons';
5
  import styles from './MapDetailPage.module.css';
6
 
7
  interface MapOut {
 
44
  const [types, setTypes] = useState<{t_code: string, label: string}[]>([]);
45
  const [imageTypes, setImageTypes] = useState<{image_type: string, label: string}[]>([]);
46
  const [regions, setRegions] = useState<{r_code: string, label: string}[]>([]);
47
+ const [countries, setCountries] = useState<{c_code: string, label: string, r_code: string}[]>([]);
48
+
49
+ // Carousel state
50
+ const [hasPrevious, setHasPrevious] = useState(false);
51
+ const [hasNext, setHasNext] = useState(false);
52
+ const [isNavigating, setIsNavigating] = useState(false);
53
+
54
+ // Search and filter state
55
+ const [search, setSearch] = useState('');
56
+ const [srcFilter, setSrcFilter] = useState('');
57
+ const [catFilter, setCatFilter] = useState('');
58
+ const [regionFilter, setRegionFilter] = useState('');
59
+ const [countryFilter, setCountryFilter] = useState('');
60
 
61
  const viewOptions = [
62
  { key: 'explore' as const, label: 'Explore' },
 
70
  return;
71
  }
72
 
73
+ fetchMapData(mapId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  }, [mapId]);
75
 
76
+ const fetchMapData = async (id: string) => {
77
+ setIsNavigating(true);
78
+ setLoading(true);
79
+
80
+ try {
81
+ const response = await fetch(`/api/images/${id}`);
82
+ if (!response.ok) {
83
+ throw new Error('Map not found');
84
+ }
85
+ const data = await response.json();
86
+ setMap(data);
87
+
88
+ // Check for previous/next items
89
+ await checkNavigationAvailability(id);
90
+ } catch (err: any) {
91
+ setError(err.message);
92
+ } finally {
93
+ setLoading(false);
94
+ setIsNavigating(false);
95
+ }
96
+ };
97
+
98
+ const checkNavigationAvailability = async (currentId: string) => {
99
+ try {
100
+ // Fetch all image IDs to determine navigation
101
+ const response = await fetch('/api/images');
102
+ if (response.ok) {
103
+ const images = await response.json();
104
+ const currentIndex = images.findIndex((img: any) => img.image_id === currentId);
105
+
106
+ // Always show navigation arrows since it's circular
107
+ setHasPrevious(true);
108
+ setHasNext(true);
109
+ }
110
+ } catch (error) {
111
+ console.error('Failed to check navigation availability:', error);
112
+ }
113
+ };
114
+
115
+ const navigateToItem = async (direction: 'previous' | 'next') => {
116
+ if (isNavigating) return;
117
+
118
+ try {
119
+ const response = await fetch('/api/images');
120
+ if (!response.ok) return;
121
+
122
+ const images = await response.json();
123
+ const currentIndex = images.findIndex((img: any) => img.image_id === mapId);
124
+
125
+ let targetIndex: number;
126
+ if (direction === 'previous') {
127
+ // Wrap around to the last item if at the beginning
128
+ targetIndex = currentIndex === 0 ? images.length - 1 : currentIndex - 1;
129
+ } else {
130
+ // Wrap around to the first item if at the end
131
+ targetIndex = currentIndex === images.length - 1 ? 0 : currentIndex + 1;
132
+ }
133
+
134
+ const targetId = images[targetIndex].image_id;
135
+ navigate(`/map/${targetId}`);
136
+ } catch (error) {
137
+ console.error('Navigation failed:', error);
138
+ }
139
+ };
140
+
141
  useEffect(() => {
142
  Promise.all([
143
  fetch('/api/sources').then(r => r.json()),
144
  fetch('/api/types').then(r => r.json()),
145
  fetch('/api/image-types').then(r => r.json()),
146
  fetch('/api/regions').then(r => r.json()),
147
+ fetch('/api/countries').then(r => r.json()),
148
+ ]).then(([sourcesData, typesData, imageTypesData, regionsData, countriesData]) => {
149
  setSources(sourcesData);
150
  setTypes(typesData);
151
  setImageTypes(imageTypesData);
152
  setRegions(regionsData);
153
+ setCountries(countriesData);
154
  }).catch(console.error);
155
  }, []);
156
 
157
  const [isGenerating, setIsGenerating] = useState(false);
158
+
159
+ // Filter the current map based on search and filter criteria
160
+ const filteredMap = useMemo(() => {
161
+ if (!map) return null;
162
+
163
+ // Check if map matches search criteria
164
+ const matchesSearch = !search ||
165
+ map.title?.toLowerCase().includes(search.toLowerCase()) ||
166
+ map.generated?.toLowerCase().includes(search.toLowerCase()) ||
167
+ map.source?.toLowerCase().includes(search.toLowerCase()) ||
168
+ map.event_type?.toLowerCase().includes(search.toLowerCase());
169
+
170
+ // Check if map matches filter criteria
171
+ const matchesSource = !srcFilter || map.source === srcFilter;
172
+ const matchesCategory = !catFilter || map.event_type === catFilter;
173
+ const matchesRegion = !regionFilter ||
174
+ map.countries.some(country => country.r_code === regionFilter);
175
+ const matchesCountry = !countryFilter ||
176
+ map.countries.some(country => country.c_code === countryFilter);
177
+
178
+ return matchesSearch && matchesSource && matchesCategory && matchesRegion && matchesCountry ? map : null;
179
+ }, [map, search, srcFilter, catFilter, regionFilter, countryFilter]);
180
 
181
  const handleContribute = async () => {
182
  if (!map) return;
 
294
  />
295
  </div>
296
 
297
+ {/* Search and Filters */}
298
+ <div className="mb-6">
299
+ <div className="flex flex-wrap items-center gap-4">
300
+ <Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2 flex-1 min-w-[300px]">
301
+ <TextInput
302
+ name="search"
303
+ placeholder="Search examples..."
304
+ value={search}
305
+ onChange={(v) => setSearch(v || '')}
306
+ />
307
+ </Container>
308
+
309
+ <Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
310
+ <SelectInput
311
+ name="source"
312
+ placeholder="All Sources"
313
+ options={sources}
314
+ value={srcFilter || null}
315
+ onChange={(v) => setSrcFilter(v as string || '')}
316
+ keySelector={(o) => o.s_code}
317
+ labelSelector={(o) => o.label}
318
+ required={false}
319
+ />
320
+ </Container>
321
+
322
+ <Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
323
+ <SelectInput
324
+ name="category"
325
+ placeholder="All Categories"
326
+ options={types}
327
+ value={catFilter || null}
328
+ onChange={(v) => setCatFilter(v as string || '')}
329
+ keySelector={(o) => o.t_code}
330
+ labelSelector={(o) => o.label}
331
+ required={false}
332
+ />
333
+ </Container>
334
+
335
+ <Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
336
+ <SelectInput
337
+ name="region"
338
+ placeholder="All Regions"
339
+ options={regions}
340
+ value={regionFilter || null}
341
+ onChange={(v) => setRegionFilter(v as string || '')}
342
+ keySelector={(o) => o.r_code}
343
+ labelSelector={(o) => o.label}
344
+ required={false}
345
+ />
346
+ </Container>
347
+
348
+ <Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
349
+ <MultiSelectInput
350
+ name="country"
351
+ placeholder="All Countries"
352
+ options={countries}
353
+ value={countryFilter ? [countryFilter] : []}
354
+ onChange={(v) => setCountryFilter((v as string[])[0] || '')}
355
+ keySelector={(o) => o.c_code}
356
+ labelSelector={(o) => o.label}
357
+ />
358
+ </Container>
359
+ </div>
360
+ </div>
361
+
362
  {view === 'mapDetails' ? (
363
+ <div className="relative">
364
+ {filteredMap ? (
365
+ <>
366
+ <div className={styles.gridLayout}>
367
+ {/* Image Section */}
368
+ <Container
369
+ heading={filteredMap.title || "Map Image"}
370
+ headingLevel={2}
371
+ withHeaderBorder
372
+ withInternalPadding
373
+ spacing="comfortable"
374
+ >
375
+ <div className={styles.imageContainer}>
376
+ {filteredMap.image_url ? (
377
+ <img
378
+ src={filteredMap.image_url}
379
+ alt={filteredMap.file_key}
380
+ />
381
+ ) : (
382
+ <div className={styles.imagePlaceholder}>
383
+ No image available
384
+ </div>
385
+ )}
386
  </div>
387
+ </Container>
 
 
388
 
389
+ {/* Details Section */}
390
+ <div className={styles.detailsSection}>
391
+ <Container
392
+ heading="Tags"
393
+ headingLevel={3}
394
+ withHeaderBorder
395
+ withInternalPadding
396
+ spacing="comfortable"
397
+ >
398
+ <div className={styles.metadataTags}>
 
 
 
 
 
 
 
 
 
 
 
399
  <span className={styles.metadataTag}>
400
+ {sources.find(s => s.s_code === filteredMap.source)?.label || filteredMap.source}
401
  </span>
402
  <span className={styles.metadataTag}>
403
+ {types.find(t => t.t_code === filteredMap.event_type)?.label || filteredMap.event_type}
404
  </span>
405
+ <span className={styles.metadataTag}>
406
+ {imageTypes.find(it => it.image_type === filteredMap.image_type)?.label || filteredMap.image_type}
407
+ </span>
408
+ {filteredMap.countries && filteredMap.countries.length > 0 && (
409
+ <>
410
+ <span className={styles.metadataTag}>
411
+ {regions.find(r => r.r_code === filteredMap.countries[0].r_code)?.label || 'Unknown Region'}
412
+ </span>
413
+ <span className={styles.metadataTag}>
414
+ {filteredMap.countries.map(country => country.label).join(', ')}
415
+ </span>
416
+ </>
417
+ )}
418
+ </div>
419
+ </Container>
420
 
421
+ <Container
422
+ heading="Description"
423
+ headingLevel={3}
424
+ withHeaderBorder
425
+ withInternalPadding
426
+ spacing="comfortable"
427
+ >
428
+ <div className={styles.captionContainer}>
429
+ {filteredMap.generated ? (
430
+ <div className={styles.captionText}>
431
+ <p>{filteredMap.edited || filteredMap.generated}</p>
432
+ </div>
433
+ ) : (
434
+ <p>— no caption yet —</p>
435
+ )}
436
  </div>
437
+ </Container>
 
 
438
  </div>
439
+ </div>
 
 
440
 
441
+ {/* Contribute Section with Navigation Arrows */}
442
+ <div className="flex items-center justify-center mt-8">
443
+ <Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-lg p-4">
444
+ <div className="flex items-center gap-4">
445
+ {hasPrevious && (
446
+ <Container withInternalPadding className="rounded-md p-2">
447
+ <Button
448
+ name="previous-item"
449
+ variant="tertiary"
450
+ size={1}
451
+ className={`bg-white/90 hover:bg-white shadow-lg border border-gray-200 ${
452
+ isNavigating ? 'opacity-50 cursor-not-allowed' : 'hover:scale-110'
453
+ }`}
454
+ onClick={() => navigateToItem('previous')}
455
+ disabled={isNavigating}
456
+ >
457
+ <div className="flex items-center gap-1">
458
+ <div className="flex -space-x-1">
459
+ <ChevronLeftLineIcon className="w-4 h-4" />
460
+ <ChevronLeftLineIcon className="w-4 h-4" />
461
+ </div>
462
+ <span className="font-semibold">Previous</span>
463
+ </div>
464
+ </Button>
465
+ </Container>
466
+ )}
467
+
468
+ <Container withInternalPadding className="rounded-md p-2">
469
+ <Button
470
+ name="contribute"
471
+ onClick={handleContribute}
472
+ disabled={isGenerating}
473
+ >
474
+ {isGenerating ? 'Generating...' : 'Contribute'}
475
+ </Button>
476
+ </Container>
477
+
478
+ {hasNext && (
479
+ <Container withInternalPadding className="rounded-md p-2">
480
+ <Button
481
+ name="next-item"
482
+ variant="tertiary"
483
+ size={1}
484
+ className={`bg-white/90 hover:bg-white shadow-lg border border-gray-200 ${
485
+ isNavigating ? 'opacity-50 cursor-not-allowed' : 'hover:scale-110'
486
+ }`}
487
+ onClick={() => navigateToItem('next')}
488
+ disabled={isNavigating}
489
+ >
490
+ <div className="flex items-center gap-1">
491
+ <span className="font-semibold">Next</span>
492
+ <div className="flex -space-x-1">
493
+ <ChevronRightLineIcon className="w-4 h-4" />
494
+ <ChevronRightLineIcon className="w-4 h-4" />
495
+ </div>
496
+ </div>
497
+ </Button>
498
+ </Container>
499
+ )}
500
+ </div>
501
+ </Container>
502
+ </div>
503
+ </>
504
+ ) : (
505
+ <div className="text-center py-12">
506
+ <div className="text-xl font-semibold text-gray-600 mb-4">
507
+ No matches found
508
+ </div>
509
+ <div className="mt-4">
510
+ <Button
511
+ name="clear-filters"
512
+ variant="secondary"
513
+ onClick={() => {
514
+ setSearch('');
515
+ setSrcFilter('');
516
+ setCatFilter('');
517
+ setRegionFilter('');
518
+ setCountryFilter('');
519
+ }}
520
+ >
521
+ Clear Filters
522
+ </Button>
523
+ </div>
524
+ </div>
525
+ )}
526
+ </div>
527
  ) : null}
528
  </Container>
529
  </PageContainer>