SCGR's picture
webmanifest
f7c5d4e
import { useState, useEffect} from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { PageContainer, Container, SegmentInput, Spinner, Button } from '@ifrc-go/ui';
import { DeleteBinLineIcon } from '@ifrc-go/icons';
import { useFilterContext } from '../../hooks/useFilterContext';
import { useAdmin } from '../../hooks/useAdmin';
import FilterBar from '../../components/FilterBar';
import Paginator from '../../components/Paginator';
import styles from './ExplorePage.module.css';
import ExportModal from '../../components/ExportModal';
interface ImageWithCaptionOut {
image_id: string;
title: string;
prompt: string;
model: string;
schema_id: string;
raw_json: Record<string, unknown>;
generated: string;
edited?: string;
accuracy?: number;
context?: number;
usability?: number;
starred: boolean;
created_at?: string;
updated_at?: string;
file_key: string;
image_url: string;
thumbnail_url?: string; // URL to smallest version (300x200px)
detail_url?: string; // URL to medium quality version (800x600px)
source: string;
event_type: string;
epsg: string;
image_type: string;
countries: {c_code: string, label: string, r_code: string}[];
// Multi-upload fields
all_image_ids?: string[];
image_count?: number;
}
export default function ExplorePage() {
const navigate = useNavigate();
const location = useLocation();
const { isAuthenticated } = useAdmin();
const [view, setView] = useState<'explore' | 'mapDetails'>('explore');
const [captions, setCaptions] = useState<ImageWithCaptionOut[]>([]);
const {
search,
srcFilter,
catFilter,
regionFilter,
countryFilter,
imageTypeFilter,
uploadTypeFilter,
showReferenceExamples,
setShowReferenceExamples
} = useFilterContext();
const [sources, setSources] = useState<{s_code: string, label: string}[]>([]);
const [types, setTypes] = useState<{t_code: string, label: string}[]>([]);
const [regions, setRegions] = useState<{r_code: string, label: string}[]>([]);
const [countries, setCountries] = useState<{c_code: string, label: string, r_code: string}[]>([]);
const [imageTypes, setImageTypes] = useState<{image_type: string, label: string}[]>([]);
const [isLoadingFilters, setIsLoadingFilters] = useState(true);
const [isLoadingContent, setIsLoadingContent] = useState(true);
const [showExportModal, setShowExportModal] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const [exportSuccess, setExportSuccess] = useState(false);
// Delete state management
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [imageToDelete, setImageToDelete] = useState<string>('');
const [isDeleting, setIsDeleting] = useState(false);
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(10);
const [totalItems, setTotalItems] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const viewOptions = [
{ key: 'explore' as const, label: 'List' },
{ key: 'mapDetails' as const, label: 'Carousel' }
];
const fetchCaptions = () => {
setIsLoadingContent(true);
// Build query parameters for server-side filtering and pagination
const params = new URLSearchParams({
page: currentPage.toString(),
limit: itemsPerPage.toString()
});
if (search) params.append('search', search);
if (srcFilter) params.append('source', srcFilter);
if (catFilter) params.append('event_type', catFilter);
if (regionFilter) params.append('region', regionFilter);
if (countryFilter) params.append('country', countryFilter);
if (imageTypeFilter) params.append('image_type', imageTypeFilter);
if (uploadTypeFilter) params.append('upload_type', uploadTypeFilter);
if (showReferenceExamples) params.append('starred_only', 'true');
fetch(`/api/images/grouped?${params.toString()}`)
.then(r => {
if (!r.ok) {
console.error('ExplorePage: Grouped endpoint failed, trying legacy endpoint');
// Fallback to legacy endpoint for backward compatibility
return fetch('/api/captions/legacy').then(r2 => {
if (!r2.ok) {
console.error('ExplorePage: Legacy endpoint failed, trying regular images endpoint');
return fetch('/api/images').then(r3 => {
if (!r3.ok) {
throw new Error(`HTTP ${r3.status}: ${r3.statusText}`);
}
return r3.json();
});
}
return r2.json();
});
}
return r.json();
})
.then(data => {
console.log('ExplorePage: Fetched captions:', data);
setCaptions(data);
})
.catch(error => {
console.error('ExplorePage: Error fetching captions:', error);
setCaptions([]);
})
.finally(() => {
setIsLoadingContent(false);
});
};
const fetchTotalCount = () => {
// Build query parameters for count endpoint
const params = new URLSearchParams();
if (search) params.append('search', search);
if (srcFilter) params.append('source', srcFilter);
if (catFilter) params.append('event_type', catFilter);
if (regionFilter) params.append('region', regionFilter);
if (countryFilter) params.append('country', countryFilter);
if (imageTypeFilter) params.append('image_type', imageTypeFilter);
if (uploadTypeFilter) params.append('upload_type', uploadTypeFilter);
if (showReferenceExamples) params.append('starred_only', 'true');
fetch(`/api/images/grouped/count?${params.toString()}`)
.then(r => {
if (!r.ok) {
console.error('ExplorePage: Count endpoint failed');
return { total_count: 0 };
}
return r.json();
})
.then(data => {
console.log('ExplorePage: Total count:', data.total_count);
setTotalItems(data.total_count);
setTotalPages(Math.ceil(data.total_count / itemsPerPage));
})
.catch(error => {
console.error('ExplorePage: Error fetching total count:', error);
setTotalItems(0);
setTotalPages(0);
});
};
// Fetch data when component mounts or filters change
useEffect(() => {
fetchCaptions();
fetchTotalCount();
}, [currentPage, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples]);
// Reset to first page when filters change (but not when currentPage changes)
useEffect(() => {
if (currentPage !== 1) {
setCurrentPage(1);
}
}, [search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, uploadTypeFilter, showReferenceExamples]);
useEffect(() => {
const handleVisibilityChange = () => {
if (!document.hidden) {
fetchCaptions();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
useEffect(() => {
const searchParams = new URLSearchParams(location.search);
const exportParam = searchParams.get('export');
if (exportParam === 'true') {
setShowExportModal(true);
if (search || srcFilter || catFilter || regionFilter || countryFilter || imageTypeFilter || showReferenceExamples) {
} else {
}
// Clean up the URL
navigate('/explore', { replace: true });
}
}, [location.search, navigate, search, srcFilter, catFilter, regionFilter, countryFilter, imageTypeFilter, showReferenceExamples]);
useEffect(() => {
setIsLoadingFilters(true);
Promise.all([
fetch('/api/sources').then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
return r.json();
}),
fetch('/api/types').then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
return r.json();
}),
fetch('/api/regions').then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
return r.json();
}),
fetch('/api/countries').then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
return r.json();
}),
fetch('/api/image-types').then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
return r.json();
}),
]).then(([sourcesData, typesData, regionsData, countriesData, imageTypesData]) => {
setSources(sourcesData);
setTypes(typesData);
setRegions(regionsData);
setCountries(countriesData);
setImageTypes(imageTypesData);
}).catch(() => {
}).finally(() => {
setIsLoadingFilters(false);
});
}, []);
// Server-side pagination - no client-side filtering needed
const paginatedResults = captions;
const exportDataset = async (images: ImageWithCaptionOut[], mode: 'standard' | 'fine-tuning' = 'fine-tuning') => {
if (images.length === 0) {
alert('No images to export');
return;
}
setIsExporting(true);
setExportSuccess(false);
try {
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
// Separate images by type
const crisisMaps = images.filter(img => img.image_type === 'crisis_map');
const droneImages = images.filter(img => img.image_type === 'drone_image');
if (crisisMaps.length > 0) {
const crisisFolder = zip.folder('crisis_maps_dataset');
const crisisImagesFolder = crisisFolder?.folder('images');
if (crisisImagesFolder) {
// Process each caption (which may contain multiple images)
let jsonIndex = 1;
for (const caption of crisisMaps) {
try {
// Get all image IDs for this caption
const imageIds = caption.image_count && caption.image_count > 1
? caption.all_image_ids || [caption.image_id]
: [caption.image_id];
// Fetch all images for this caption
const imagePromises = imageIds.map(async (imageId, imgIndex) => {
try {
const response = await fetch(`/api/images/${imageId}/file`);
if (!response.ok) throw new Error(`Failed to fetch image ${imageId}`);
const blob = await response.blob();
const fileExtension = caption.file_key.split('.').pop() || 'jpg';
const fileName = `${String(jsonIndex).padStart(4, '0')}_${String(imgIndex + 1).padStart(2, '0')}.${fileExtension}`;
crisisImagesFolder.file(fileName, blob);
return { success: true, fileName, imageId };
} catch (error) {
console.error(`Failed to process image ${imageId}:`, error);
return { success: false, fileName: '', imageId };
}
});
const imageResults = await Promise.all(imagePromises);
const successfulImages = imageResults.filter(result => result.success);
if (successfulImages.length > 0) {
if (mode === 'fine-tuning') {
// For fine-tuning, create one entry per caption with all images
const imageFiles = successfulImages.map(result => `images/${result.fileName}`);
const random = Math.random();
const entry = {
image: imageFiles.length === 1 ? imageFiles[0] : imageFiles,
caption: caption.edited || caption.generated || '',
metadata: {
image_id: imageIds,
title: caption.title,
source: caption.source,
event_type: caption.event_type,
image_type: caption.image_type,
countries: caption.countries,
starred: caption.starred,
image_count: caption.image_count || 1
}
};
// Store the entry for later processing
if (!crisisFolder) continue;
if (random < 0.8) {
// Add to train data
const trainFile = crisisFolder.file('train.jsonl');
if (trainFile) {
const existingData = await trainFile.async('string').then(data => JSON.parse(data || '[]')).catch(() => []);
existingData.push(entry);
crisisFolder.file('train.jsonl', JSON.stringify(existingData, null, 2));
} else {
crisisFolder.file('train.jsonl', JSON.stringify([entry], null, 2));
}
} else if (random < 0.9) {
// Add to test data
const testFile = crisisFolder.file('test.jsonl');
if (testFile) {
const existingData = await testFile.async('string').then(data => JSON.parse(data || '[]')).catch(() => []);
existingData.push(entry);
crisisFolder.file('test.jsonl', JSON.stringify(existingData, null, 2));
} else {
crisisFolder.file('test.jsonl', JSON.stringify([entry], null, 2));
}
} else {
// Add to validation data
const valFile = crisisFolder.file('val.jsonl');
if (valFile) {
const existingData = await valFile.async('string').then(data => JSON.parse(data || '[]')).catch(() => []);
existingData.push(entry);
crisisFolder.file('val.jsonl', JSON.stringify(existingData, null, 2));
} else {
crisisFolder.file('val.jsonl', JSON.stringify([entry], null, 2));
}
}
} else {
// For standard mode, create one JSON file per caption
const imageFiles = successfulImages.map(result => `images/${result.fileName}`);
const jsonData = {
image: imageFiles.length === 1 ? imageFiles[0] : imageFiles,
caption: caption.edited || caption.generated || '',
metadata: {
image_id: imageIds,
title: caption.title,
source: caption.source,
event_type: caption.event_type,
image_type: caption.image_type,
countries: caption.countries,
starred: caption.starred,
image_count: caption.image_count || 1
}
};
if (crisisFolder) {
crisisFolder.file(`${String(jsonIndex).padStart(4, '0')}.json`, JSON.stringify(jsonData, null, 2));
}
}
jsonIndex++;
}
} catch (error) {
console.error(`Failed to process caption ${caption.image_id}:`, error);
}
}
}
}
// Create drone_images dataset
if (droneImages.length > 0) {
const droneFolder = zip.folder('drone_images_dataset');
const droneImagesFolder = droneFolder?.folder('images');
if (droneImagesFolder) {
// Process each caption (which may contain multiple images)
let jsonIndex = 1;
for (const caption of droneImages) {
try {
// Get all image IDs for this caption
const imageIds = caption.image_count && caption.image_count > 1
? caption.all_image_ids || [caption.image_id]
: [caption.image_id];
// Fetch all images for this caption
const imagePromises = imageIds.map(async (imageId, imgIndex) => {
try {
const response = await fetch(`/api/images/${imageId}/file`);
if (!response.ok) throw new Error(`Failed to fetch image ${imageId}`);
const blob = await response.blob();
const fileExtension = caption.file_key.split('.').pop() || 'jpg';
const fileName = `${String(jsonIndex).padStart(4, '0')}_${String(imgIndex + 1).padStart(2, '0')}.${fileExtension}`;
droneImagesFolder.file(fileName, blob);
return { success: true, fileName, imageId };
} catch (error) {
console.error(`Failed to process image ${imageId}:`, error);
return { success: false, fileName: '', imageId };
}
});
const imageResults = await Promise.all(imagePromises);
const successfulImages = imageResults.filter(result => result.success);
if (successfulImages.length > 0) {
if (mode === 'fine-tuning') {
// For fine-tuning, create one entry per caption with all images
const imageFiles = successfulImages.map(result => `images/${result.fileName}`);
const random = Math.random();
const entry = {
image: imageFiles.length === 1 ? imageFiles[0] : imageFiles,
caption: caption.edited || caption.generated || '',
metadata: {
image_id: imageIds,
title: caption.title,
source: caption.source,
event_type: caption.event_type,
image_type: caption.image_type,
countries: caption.countries,
starred: caption.starred,
image_count: caption.image_count || 1
}
};
// Store the entry for later processing
if (!droneFolder) continue;
if (random < 0.8) {
// Add to train data
const trainFile = droneFolder.file('train.jsonl');
if (trainFile) {
const existingData = await trainFile.async('string').then(data => JSON.parse(data || '[]')).catch(() => []);
existingData.push(entry);
droneFolder.file('train.jsonl', JSON.stringify(existingData, null, 2));
} else {
droneFolder.file('train.jsonl', JSON.stringify([entry], null, 2));
}
} else if (random < 0.9) {
// Add to test data
const testFile = droneFolder.file('test.jsonl');
if (testFile) {
const existingData = await testFile.async('string').then(data => JSON.parse(data || '[]')).catch(() => []);
existingData.push(entry);
droneFolder.file('test.jsonl', JSON.stringify(existingData, null, 2));
} else {
droneFolder.file('test.jsonl', JSON.stringify([entry], null, 2));
}
} else {
// Add to validation data
const valFile = droneFolder.file('val.jsonl');
if (valFile) {
const existingData = await valFile.async('string').then(data => JSON.parse(data || '[]')).catch(() => []);
existingData.push(entry);
droneFolder.file('val.jsonl', JSON.stringify(existingData, null, 2));
} else {
droneFolder.file('val.jsonl', JSON.stringify([entry], null, 2));
}
}
} else {
// For standard mode, create one JSON file per caption
const imageFiles = successfulImages.map(result => `images/${result.fileName}`);
const jsonData = {
image: imageFiles.length === 1 ? imageFiles[0] : imageFiles,
caption: caption.edited || caption.generated || '',
metadata: {
image_id: imageIds,
title: caption.title,
source: caption.source,
event_type: caption.event_type,
image_type: caption.image_type,
countries: caption.countries,
starred: caption.starred,
image_count: caption.image_count || 1
}
};
if (droneFolder) {
droneFolder.file(`${String(jsonIndex).padStart(4, '0')}.json`, JSON.stringify(jsonData, null, 2));
}
}
jsonIndex++;
}
} catch (error) {
console.error(`Failed to process caption ${caption.image_id}:`, error);
}
}
}
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(zipBlob);
const link = document.createElement('a');
link.href = url;
link.download = `datasets_${mode}_${new Date().toISOString().split('T')[0]}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
const totalImages = (crisisMaps.length || 0) + (droneImages.length || 0);
console.log(`Exported ${mode} datasets with ${totalImages} total images:`);
if (crisisMaps.length > 0) console.log(`- Crisis maps: ${crisisMaps.length} images`);
if (droneImages.length > 0) console.log(`- Drone images: ${droneImages.length} images`);
setExportSuccess(true);
} catch (error) {
console.error('Export failed:', error);
alert('Failed to export dataset. Please try again.');
} finally {
setIsExporting(false);
}
};
// Delete functions
const handleDelete = (imageId: string) => {
setImageToDelete(imageId);
setShowDeleteConfirm(true);
};
const confirmDelete = async () => {
if (!imageToDelete) return;
setIsDeleting(true);
try {
console.log('Deleting image with ID:', imageToDelete);
const response = await fetch(`/api/images/${imageToDelete}`, {
method: 'DELETE',
});
if (response.ok) {
// Remove the deleted image from the captions list
setCaptions(prev => prev.filter(img => img.image_id !== imageToDelete));
setShowDeleteConfirm(false);
setImageToDelete('');
} else {
console.error('Delete failed');
alert('Failed to delete image. Please try again.');
}
} catch (error) {
console.error('Delete failed:', error);
alert('Failed to delete image. Please try again.');
} finally {
setIsDeleting(false);
}
};
return (
<PageContainer>
{isLoadingContent ? (
<div className="flex flex-col items-center justify-center min-h-[60vh]">
<div className="flex flex-col items-center gap-4">
<Spinner className="text-ifrcRed" />
<div>Loading examples...</div>
</div>
</div>
) : (
<div className="max-w-7xl mx-auto">
<div className={styles.tabSelector}>
<SegmentInput
name="explore-view"
value={view}
onChange={(value) => {
if (value === 'explore' || value === 'mapDetails') {
setView(value);
if (value === 'mapDetails' && captions.length > 0) {
if (captions[0]?.image_id && captions[0].image_id !== 'undefined' && captions[0].image_id !== 'null') {
navigate(`/map/${captions[0].image_id}`);
} else {
console.error('Invalid image_id for navigation:', captions[0]?.image_id);
}
}
}
}}
options={viewOptions}
keySelector={(o) => o.key}
labelSelector={(o) => o.label}
/>
{/* Action Buttons - Right Aligned */}
<div className="flex items-center gap-2 ml-auto">
{/* Reference Examples Filter - Available to all users */}
<Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
<Button
name="reference-examples"
variant={showReferenceExamples ? "primary" : "secondary"}
onClick={() => setShowReferenceExamples(!showReferenceExamples)}
className="whitespace-nowrap"
>
<span className="mr-2">
{showReferenceExamples ? (
<span className="text-yellow-400"></span>
) : (
<span className="text-yellow-400"></span>
)}
</span>
Reference Examples
</Button>
</Container>
{/* Export Dataset Button */}
<Button
name="export-dataset"
variant="secondary"
onClick={() => setShowExportModal(true)}
>
Export
</Button>
</div>
</div>
{view === 'explore' ? (
<div className="space-y-6">
{/* Search and Filters */}
<div className="mb-6 space-y-4">
{/* Layer 1: Search, Reference Examples, Clear Filters */}
<div className="flex flex-wrap items-center gap-4">
<Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2 flex-1 min-w-[300px]">
<FilterBar
sources={sources}
types={types}
regions={regions}
countries={countries}
imageTypes={imageTypes}
isLoadingFilters={isLoadingFilters}
/>
</Container>
</div>
</div>
{/* Results Section */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<p className="text-sm text-gray-600">
{paginatedResults.length} of {totalItems} examples
</p>
</div>
{/* Loading State */}
{isLoadingContent && (
<div className="text-center py-12">
<div className="flex flex-col items-center gap-4">
<Spinner className="text-ifrcRed" />
<div>Loading examples...</div>
</div>
</div>
)}
{/* Content */}
{!isLoadingContent && (
<div className="space-y-4">
{paginatedResults.map(c => (
<div key={c.image_id} className="flex items-center gap-4">
{/* Card Content */}
<div className={`${styles.mapItem} flex-1`} onClick={() => {
console.log('ExplorePage: Clicking on image with ID:', c.image_id);
console.log('ExplorePage: Image data:', c);
if (c.image_id && c.image_id !== 'undefined' && c.image_id !== 'null') {
console.log('ExplorePage: Navigating to:', `/map/${c.image_id}`);
console.log('ExplorePage: Full navigation URL:', `/#/map/${c.image_id}`);
navigate(`/map/${c.image_id}`);
} else {
console.error('Invalid image_id for navigation:', c.image_id);
console.error('Full item data:', JSON.stringify(c, null, 2));
// Show a visual error in production
alert(`Cannot navigate: Invalid image ID (${c.image_id})`);
}
}}>
<div className={styles.mapItemImage} style={{ width: '120px', height: '80px' }}>
{/* Explore Page: Prioritize thumbnails for faster loading */}
{c.thumbnail_url ? (
<>
{console.log('ExplorePage: Using thumbnail for fast loading:', c.thumbnail_url)}
<img
src={c.thumbnail_url}
alt={c.file_key}
onError={(e) => {
console.error('ExplorePage: Thumbnail failed to load, falling back to original:', c.thumbnail_url);
// Fallback to original image
const target = e.target as HTMLImageElement;
if (c.image_url) {
target.src = c.image_url;
} else {
target.style.display = 'none';
target.parentElement!.innerHTML = 'Img';
}
}}
onLoad={() => console.log('ExplorePage: Thumbnail loaded successfully:', c.thumbnail_url)}
/>
</>
) : c.image_url ? (
<>
{console.log('ExplorePage: No thumbnail available, using original image:', c.image_url)}
<img
src={c.image_url}
alt={c.file_key}
onError={(e) => {
console.error('ExplorePage: Original image failed to load:', c.image_url);
const target = e.target as HTMLImageElement;
target.style.display = 'none';
target.parentElement!.innerHTML = 'Img';
}}
onLoad={() => console.log('ExplorePage: Original image loaded successfully:', c.image_url)}
/>
</>
) : (
<>
{console.log('ExplorePage: No image_url or thumbnail provided for item:', c)}
'Img'
</>
)}
</div>
<div className={styles.mapItemContent}>
<h3 className={styles.mapItemTitle}>
<div className="flex items-center gap-2">
<span>{c.title || 'Untitled'}</span>
{c.starred && (
<span className="text-red-500 text-lg" title="Starred image"></span>
)}
</div>
</h3>
<div className={styles.mapItemMetadata}>
<div className={styles.metadataTags}>
{c.image_type !== 'drone_image' && (
<span className={styles.metadataTagSource}>
{c.source && c.source.includes(', ')
? c.source.split(', ').map(s => sources.find(src => src.s_code === s.trim())?.label || s.trim()).join(', ')
: sources.find(s => s.s_code === c.source)?.label || c.source
}
</span>
)}
<span className={styles.metadataTagType}>
{c.event_type && c.event_type.includes(', ')
? c.event_type.split(', ').map(e => types.find(t => t.t_code === e.trim())?.label || e.trim()).join(', ')
: types.find(t => t.t_code === c.event_type)?.label || c.event_type
}
</span>
<span className={styles.metadataTag}>
{imageTypes.find(it => it.image_type === c.image_type)?.label || c.image_type}
</span>
{c.image_count && c.image_count > 1 && (
<span className={styles.metadataTag} title={`Multi-upload with ${c.image_count} images`}>
📷 {c.image_count}
</span>
)}
{(!c.image_count || c.image_count <= 1) && (
<span className={styles.metadataTag} title="Single Upload">
Single
</span>
)}
{c.countries && c.countries.length > 0 && (
<>
<span className={styles.metadataTag}>
{regions.find(r => r.r_code === c.countries[0].r_code)?.label || 'Unknown Region'}
</span>
<span className={styles.metadataTag}>
{c.countries.map(country => country.label).join(', ')}
</span>
</>
)}
</div>
</div>
</div>
</div>
{/* Delete Button - Admin Only */}
{isAuthenticated && (
<Container withInternalPadding className="bg-white/20 backdrop-blur-sm rounded-md p-2">
<Button
name={`delete-${c.image_id}`}
variant="tertiary"
size={1}
className="bg-red-50 hover:bg-red-100 text-red-700 border border-red-200 hover:border-red-300"
onClick={() => handleDelete(c.image_id)}
title="Delete"
aria-label="Delete saved image"
>
<DeleteBinLineIcon className="w-4 h-4" />
</Button>
</Container>
)}
</div>
))}
{!paginatedResults.length && (
<div className="text-center py-12">
<p className="text-gray-500">No examples found.</p>
</div>
)}
{/* Enhanced Paginator Component */}
{!isLoadingContent && paginatedResults.length > 0 && (
<Paginator
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalItems}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
/>
)}
</div>
)}
</div>
</div>
) : (
<div className="space-y-6">
<div className="text-center py-12">
<p className="text-gray-500">Map Details view coming soon...</p>
<p className="text-sm text-gray-400 mt-2">This will show detailed information about individual maps</p>
</div>
</div>
)}
</div>
)}
{/* Delete Confirmation Modal */}
{showDeleteConfirm && (
<div className={styles.fullSizeModalOverlay} onClick={() => setShowDeleteConfirm(false)}>
<div className={styles.fullSizeModalContent} onClick={(e) => e.stopPropagation()}>
<div className={styles.ratingWarningContent}>
<h3 className={styles.ratingWarningTitle}>Delete Image?</h3>
<p className={styles.ratingWarningText}>
This action cannot be undone. Are you sure you want to delete this saved image and all related data?
</p>
<div className={styles.ratingWarningButtons}>
<Button
name="confirm-delete"
variant="secondary"
onClick={confirmDelete}
disabled={isDeleting}
>
{isDeleting ? 'Deleting...' : 'Delete'}
</Button>
<Button
name="cancel-delete"
variant="tertiary"
onClick={() => setShowDeleteConfirm(false)}
disabled={isDeleting}
>
Cancel
</Button>
</div>
</div>
</div>
</div>
)}
{/* Export Selection Modal */}
<ExportModal
isOpen={showExportModal}
onClose={() => {
setShowExportModal(false);
setExportSuccess(false);
setIsExporting(false);
}}
onExport={(mode, selectedTypes) => {
const filteredByType = paginatedResults.filter((img: ImageWithCaptionOut) => selectedTypes.includes(img.image_type));
exportDataset(filteredByType, mode);
}}
filteredCount={paginatedResults.length}
totalCount={totalItems}
hasFilters={!!(search || srcFilter || catFilter || regionFilter || countryFilter || imageTypeFilter || uploadTypeFilter || showReferenceExamples)}
crisisMapsCount={paginatedResults.filter((img: ImageWithCaptionOut) => img.image_type === 'crisis_map').length}
droneImagesCount={paginatedResults.filter((img: ImageWithCaptionOut) => img.image_type === 'drone_image').length}
isLoading={isExporting}
exportSuccess={exportSuccess}
/>
</PageContainer>
);
}