SCGR's picture
loading state hadnling
cb0b78c
import { useState } from 'react';
import { Button, SegmentInput, Checkbox, Spinner } from '@ifrc-go/ui';
import styles from './ExportModal.module.css';
interface ExportModalProps {
isOpen: boolean;
onClose: () => void;
onExport: (mode: 'standard' | 'fine-tuning', selectedTypes: string[]) => void;
filteredCount: number;
totalCount: number;
hasFilters: boolean;
crisisMapsCount: number;
droneImagesCount: number;
isLoading?: boolean;
exportSuccess?: boolean;
variant?: 'bulk' | 'single';
onNavigateToList?: () => void;
onNavigateAndExport?: () => void;
}
export default function ExportModal({
isOpen,
onClose,
onExport,
crisisMapsCount,
droneImagesCount,
isLoading = false,
exportSuccess = false,
variant = 'bulk',
onNavigateAndExport
}: ExportModalProps) {
const [exportMode, setExportMode] = useState<'standard' | 'fine-tuning'>('standard');
const [trainSplit, setTrainSplit] = useState(80);
const [testSplit, setTestSplit] = useState(10);
const [valSplit, setValSplit] = useState(10);
const [crisisMapsSelected, setCrisisMapsSelected] = useState(true);
const [droneImagesSelected, setDroneImagesSelected] = useState(true);
const handleExport = () => {
if (variant === 'single') {
// For single item export, just export immediately
onExport(exportMode, ['crisis_map', 'drone_image']); // Export regardless of type
return;
}
// For bulk export, check selections
if (!crisisMapsSelected && !droneImagesSelected) {
alert('Please select at least one image type to export.');
return;
}
const selectedTypes: string[] = [];
if (crisisMapsSelected) selectedTypes.push('crisis_map');
if (droneImagesSelected) selectedTypes.push('drone_image');
onExport(exportMode, selectedTypes);
};
const handleClose = () => {
onClose();
};
if (!isOpen) return null;
// Single item export UI
if (variant === 'single') {
return (
<div className={styles.fullSizeModalOverlay} onClick={handleClose}>
<div className={styles.fullSizeModalContent} onClick={(e) => e.stopPropagation()}>
{isLoading && (
<div className={styles.loadingOverlay}>
<div className="flex flex-col items-center gap-4">
<Spinner className="text-ifrcRed" />
<div className="text-lg font-medium">Exporting...</div>
<div className="text-sm text-gray-600">This might take a few seconds</div>
</div>
</div>
)}
{exportSuccess && (
<div className={styles.loadingOverlay}>
<div className="flex flex-col items-center gap-4">
<div className="text-lg font-medium">Export Successful!</div>
<div className="text-sm text-gray-600">Your dataset has been downloaded</div>
<Button
name="close-export-success"
onClick={handleClose}
className="mt-4"
>
Close
</Button>
</div>
</div>
)}
<div className={styles.ratingWarningContent}>
<h3 className={styles.ratingWarningTitle}>Export Single Item</h3>
<div className={styles.singleExportMessage}>
<p>This only exports the 1 item currently on display.</p>
<p>You may export the entire dataset from the "list view" here:</p>
</div>
<div className={styles.navigateButtonContainer}>
<Button
name="navigate-to-list"
variant="secondary"
onClick={onNavigateAndExport}
>
Navigate to List View
</Button>
</div>
<div className={styles.ratingWarningButtons}>
<Button
name="continue-export"
onClick={handleExport}
disabled={isLoading}
>
{isLoading ? (
<div className="flex items-center gap-2">
<Spinner className="text-white" />
Exporting...
</div>
) : (
'Continue'
)}
</Button>
<Button
name="cancel-export"
variant="tertiary"
onClick={handleClose}
disabled={isLoading}
>
Cancel
</Button>
</div>
</div>
</div>
</div>
);
}
// Bulk export UI (original functionality)
return (
<div className={styles.fullSizeModalOverlay} onClick={handleClose}>
<div className={styles.fullSizeModalContent} onClick={(e) => e.stopPropagation()}>
{isLoading && (
<div className={styles.loadingOverlay}>
<div className="flex flex-col items-center gap-4">
<Spinner className="text-ifrcRed" />
<div className="text-lg font-medium">Exporting...</div>
<div className="text-sm text-gray-600">This might take a few seconds</div>
</div>
</div>
)}
{exportSuccess && (
<div className={styles.loadingOverlay}>
<div className="flex flex-col items-center gap-4">
<div className="text-lg font-medium">Export Successful!</div>
<div className="text-sm text-gray-600">Your dataset has been downloaded</div>
<Button
name="close-export-success"
onClick={handleClose}
className="mt-4"
>
Close
</Button>
</div>
</div>
)}
<div className={styles.ratingWarningContent}>
<h3 className={styles.ratingWarningTitle}>Export Dataset</h3>
{/* Export Mode Switch */}
<div className={styles.exportModeSection}>
<SegmentInput
name="export-mode"
value={exportMode}
onChange={(value) => {
if (value === 'standard' || value === 'fine-tuning') {
setExportMode(value);
}
}}
options={[
{ key: 'standard' as const, label: 'Standard' },
{ key: 'fine-tuning' as const, label: 'Fine-tuning' }
]}
keySelector={(o) => o.key}
labelSelector={(o) => o.label}
disabled={isLoading}
/>
</div>
{/* Train/Test/Val Split Configuration - Only show for Fine-tuning mode */}
{exportMode === 'fine-tuning' && (
<div className={styles.splitConfigSection}>
<div className={styles.splitConfigTitle}>Dataset Split Configuration</div>
<div className={styles.splitInputsContainer}>
<div className={styles.splitInputGroup}>
<label htmlFor="train-split" className={styles.splitInputLabel}>Train (%)</label>
<input
id="train-split"
type="number"
min="0"
max="100"
value={trainSplit}
onChange={(e) => {
const newTrain = parseInt(e.target.value) || 0;
const remaining = 100 - newTrain;
if (remaining >= 0) {
setTrainSplit(newTrain);
if (testSplit + valSplit > remaining) {
setTestSplit(Math.floor(remaining / 2));
setValSplit(remaining - Math.floor(remaining / 2));
}
}
}}
className={styles.splitInput}
disabled={isLoading}
/>
</div>
<div className={styles.splitInputGroup}>
<label htmlFor="test-split" className={styles.splitInputLabel}>Test (%)</label>
<input
id="test-split"
type="number"
min="0"
max="100"
value={testSplit}
onChange={(e) => {
const newTest = parseInt(e.target.value) || 0;
const remaining = 100 - trainSplit - newTest;
if (remaining >= 0) {
setTestSplit(newTest);
setValSplit(remaining);
}
}}
className={styles.splitInput}
disabled={isLoading}
/>
</div>
<div className={styles.splitInputGroup}>
<label htmlFor="val-split" className={styles.splitInputLabel}>Val (%)</label>
<input
id="val-split"
type="number"
min="0"
max="100"
value={valSplit}
onChange={(e) => {
const newVal = parseInt(e.target.value) || 0;
const remaining = 100 - trainSplit - newVal;
if (remaining >= 0) {
setValSplit(newVal);
setTestSplit(remaining);
}
}}
className={styles.splitInput}
disabled={isLoading}
/>
</div>
</div>
{trainSplit + testSplit + valSplit !== 100 && (
<div className={styles.splitTotal}>
<span className={styles.splitTotalError}>Must equal 100%</span>
</div>
)}
</div>
)}
<div className={styles.checkboxesContainer}>
<div className="flex items-center gap-3">
<Checkbox
name="crisis-maps"
label={`Crisis Maps (${crisisMapsCount} images)`}
value={crisisMapsSelected}
onChange={(value) => setCrisisMapsSelected(value)}
disabled={isLoading}
/>
</div>
<div className="flex items-center gap-3">
<Checkbox
name="drone-images"
label={`Drone Images (${droneImagesCount} images)`}
value={droneImagesSelected}
onChange={(value) => setDroneImagesSelected(value)}
disabled={isLoading}
/>
</div>
</div>
<div className={styles.ratingWarningButtons}>
<Button
name="confirm-export"
onClick={handleExport}
disabled={isLoading}
>
{isLoading ? (
<div className="flex items-center gap-2">
<Spinner className="text-white" />
Exporting...
</div>
) : (
'Export Selected'
)}
</Button>
<Button
name="cancel-export"
variant="tertiary"
onClick={handleClose}
disabled={isLoading}
>
Cancel
</Button>
</div>
</div>
</div>
</div>
);
}