feat: show brief success state before closing export modal
Progress bar turns green at 100%, button shows "Done", then modal auto-closes after 600ms. Gives visual confirmation without fake delay.
This commit is contained in:
@@ -51,6 +51,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
const [filename, setFilename] = useState(`pulse_export_${formatDateISO(new Date())}`)
|
const [filename, setFilename] = useState(`pulse_export_${formatDateISO(new Date())}`)
|
||||||
const [includeHeader, setIncludeHeader] = useState(true)
|
const [includeHeader, setIncludeHeader] = useState(true)
|
||||||
const [isExporting, setIsExporting] = useState(false)
|
const [isExporting, setIsExporting] = useState(false)
|
||||||
|
const [exportDone, setExportDone] = useState(false)
|
||||||
const [exportProgress, setExportProgress] = useState({ step: 0, total: 1, label: '' })
|
const [exportProgress, setExportProgress] = useState({ step: 0, total: 1, label: '' })
|
||||||
const [selectedFields, setSelectedFields] = useState<Record<keyof DailyStat, boolean>>({
|
const [selectedFields, setSelectedFields] = useState<Record<keyof DailyStat, boolean>>({
|
||||||
date: true,
|
date: true,
|
||||||
@@ -64,6 +65,15 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
setSelectedFields((prev) => ({ ...prev, [field]: checked }))
|
setSelectedFields((prev) => ({ ...prev, [field]: checked }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const finishExport = useCallback(() => {
|
||||||
|
setExportDone(true)
|
||||||
|
setIsExporting(false)
|
||||||
|
setTimeout(() => {
|
||||||
|
setExportDone(false)
|
||||||
|
onClose()
|
||||||
|
}, 600)
|
||||||
|
}, [onClose])
|
||||||
|
|
||||||
// Yield to the UI thread so the browser can paint progress updates
|
// Yield to the UI thread so the browser can paint progress updates
|
||||||
const updateProgress = useCallback(async (step: number, total: number, label: string) => {
|
const updateProgress = useCallback(async (step: number, total: number, label: string) => {
|
||||||
setExportProgress({ step, total, label })
|
setExportProgress({ step, total, label })
|
||||||
@@ -134,7 +144,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
document.body.appendChild(link)
|
document.body.appendChild(link)
|
||||||
link.click()
|
link.click()
|
||||||
document.body.removeChild(link)
|
document.body.removeChild(link)
|
||||||
onClose()
|
finishExport()
|
||||||
return
|
return
|
||||||
} else if (format === 'pdf') {
|
} else if (format === 'pdf') {
|
||||||
const totalSteps = 3 + (topPages?.length ? 1 : 0) + (topReferrers?.length ? 1 : 0) + (campaigns?.length ? 1 : 0)
|
const totalSteps = 3 + (topPages?.length ? 1 : 0) + (topReferrers?.length ? 1 : 0) + (campaigns?.length ? 1 : 0)
|
||||||
@@ -359,7 +369,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
|
|
||||||
await updateProgress(totalSteps, totalSteps, 'Saving PDF...')
|
await updateProgress(totalSteps, totalSteps, 'Saving PDF...')
|
||||||
doc.save(`${filename || 'export'}.pdf`)
|
doc.save(`${filename || 'export'}.pdf`)
|
||||||
onClose()
|
finishExport()
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
content = JSON.stringify(exportData, null, 2)
|
content = JSON.stringify(exportData, null, 2)
|
||||||
@@ -376,7 +386,7 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
link.click()
|
link.click()
|
||||||
document.body.removeChild(link)
|
document.body.removeChild(link)
|
||||||
|
|
||||||
onClose()
|
finishExport()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Export failed:', e)
|
console.error('Export failed:', e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -468,16 +478,16 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Progress Bar */}
|
||||||
{isExporting && (
|
{(isExporting || exportDone) && (
|
||||||
<div className="space-y-2 pt-2">
|
<div className="space-y-2 pt-2">
|
||||||
<div className="flex items-center justify-between text-xs text-neutral-500 dark:text-neutral-400">
|
<div className="flex items-center justify-between text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
<span>{exportProgress.label}</span>
|
<span>{exportDone ? 'Export complete' : exportProgress.label}</span>
|
||||||
<span>{Math.round((exportProgress.step / exportProgress.total) * 100)}%</span>
|
<span>{exportDone ? '100%' : `${Math.round((exportProgress.step / exportProgress.total) * 100)}%`}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1.5 w-full rounded-full bg-neutral-100 dark:bg-neutral-800 overflow-hidden">
|
<div className="h-1.5 w-full rounded-full bg-neutral-100 dark:bg-neutral-800 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full bg-brand-orange transition-all duration-300 ease-out"
|
className={`h-full rounded-full transition-all duration-300 ease-out ${exportDone ? 'bg-green-500' : 'bg-brand-orange'}`}
|
||||||
style={{ width: `${(exportProgress.step / exportProgress.total) * 100}%` }}
|
style={{ width: exportDone ? '100%' : `${(exportProgress.step / exportProgress.total) * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -488,8 +498,8 @@ export default function ExportModal({ isOpen, onClose, data, stats, topPages, to
|
|||||||
<Button variant="secondary" onClick={onClose} disabled={isExporting}>
|
<Button variant="secondary" onClick={onClose} disabled={isExporting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" onClick={handleExport} disabled={isExporting}>
|
<Button variant="primary" onClick={handleExport} disabled={isExporting || exportDone}>
|
||||||
{isExporting ? 'Exporting...' : 'Export Data'}
|
{exportDone ? '✓ Done' : isExporting ? 'Exporting...' : 'Export Data'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user