From 50caa88cd9c0af57ee92cf9ef06ffde1acb6c60d Mon Sep 17 00:00:00 2001 From: Syco21 Date: Fri, 22 May 2026 21:18:46 +0200 Subject: [PATCH] Rename Footer to ImportButton, add SSE progress bar on import --- src/components/Footer/Footer.jsx | 65 ---------- src/components/ImportButton/ImportButton.jsx | 125 +++++++++++++++++++ src/pages/HomePage.jsx | 4 +- src/services/api.js | 7 -- 4 files changed, 127 insertions(+), 74 deletions(-) delete mode 100644 src/components/Footer/Footer.jsx create mode 100644 src/components/ImportButton/ImportButton.jsx diff --git a/src/components/Footer/Footer.jsx b/src/components/Footer/Footer.jsx deleted file mode 100644 index 058b82a..0000000 --- a/src/components/Footer/Footer.jsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { fetchDatabaseVersion, triggerFullImport } from '../../services/api'; - -function Footer({ onImportComplete }) { - const [dbVersion, setDbVersion] = useState(null); - const [importing, setImporting] = useState(false); - const [modalMessage, setModalMessage] = useState(''); - const [showModal, setShowModal] = useState(false); - - useEffect(() => { - fetchDatabaseVersion() - .then(data => setDbVersion(data.database_version)) - .catch(() => setDbVersion('N/A')); - }, []); - - const handleImport = async () => { - setImporting(true); - try { - const result = await triggerFullImport(); - const data = await fetchDatabaseVersion(); - setDbVersion(data.database_version); - setModalMessage(result.message || 'Import completed'); - if (onImportComplete) await onImportComplete(); - } catch (err) { - setModalMessage(`Import failed: ${err.message}`); - } finally { - setImporting(false); - setShowModal(true); - } - }; - - return ( - <> -
- DB: {dbVersion ?? '…'} - -
- - {showModal && createPortal( - <> -
setShowModal(false)} /> -
-
{modalMessage}
- -
- , - document.body - )} - - ); -} - -export default Footer; diff --git a/src/components/ImportButton/ImportButton.jsx b/src/components/ImportButton/ImportButton.jsx new file mode 100644 index 0000000..b20d12e --- /dev/null +++ b/src/components/ImportButton/ImportButton.jsx @@ -0,0 +1,125 @@ +import React, { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { fetchDatabaseVersion } from '../../services/api'; + +function ImportButton({ onImportComplete }) { + const [dbVersion, setDbVersion] = useState(null); + const [importing, setImporting] = useState(false); + const [showModal, setShowModal] = useState(false); + const [progress, setProgress] = useState(0); + const [progressMessage, setProgressMessage] = useState(''); + const [done, setDone] = useState(false); + const [resultMessage, setResultMessage] = useState(''); + const [isError, setIsError] = useState(false); + + useEffect(() => { + fetchDatabaseVersion() + .then(data => setDbVersion(data.database_version)) + .catch(() => setDbVersion('N/A')); + }, []); + + const handleImport = async () => { + setImporting(true); + setDone(false); + setIsError(false); + setProgress(0); + setProgressMessage('Starting…'); + setResultMessage(''); + setShowModal(true); + + try { + const response = await fetch('/api/import/full-import', { method: 'POST' }); + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done: streamDone, value } = await reader.read(); + if (streamDone) break; + buffer += decoder.decode(value, { stream: true }); + const parts = buffer.split('\n\n'); + buffer = parts.pop(); + for (const part of parts) { + if (!part.startsWith('data: ')) continue; + const data = JSON.parse(part.slice(6)); + if (data.progress !== undefined) setProgress(data.progress); + if (data.message) setProgressMessage(data.message); + if (data.done) { + setDone(true); + setImporting(false); + setIsError(!!data.error); + if (data.version) setDbVersion(data.version); + if (data.result) { + const r = data.result; + setResultMessage( + `${r.cards_added} added · ${r.cards_removed ?? 0} removed · ${r.total_cards.toLocaleString()} total · ${r.duration_seconds}s` + ); + } + if (!data.error && onImportComplete) onImportComplete(); + } + } + } + } catch (err) { + setProgressMessage(`Error: ${err.message}`); + setDone(true); + setIsError(true); + setImporting(false); + } + }; + + return ( + <> +
+ DB: {dbVersion ?? '…'} + +
+ + {showModal && createPortal( + <> +
done && setShowModal(false)} /> +
+
+ {progressMessage} +
+
+
+
+ {!done && ( +
+ {Math.round(progress * 100)}% +
+ )} + {done && ( + <> + {resultMessage && ( +
{resultMessage}
+ )} + + + )} +
+ , + document.body + )} + + ); +} + +export default ImportButton; diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx index 896d4cb..e17ae1b 100644 --- a/src/pages/HomePage.jsx +++ b/src/pages/HomePage.jsx @@ -8,7 +8,7 @@ import { fuzzyMatch } from '../utils/search'; import CardRow from '../components/CardRow/CardRow'; import SearchBar from '../components/SearchBar/SearchBar'; import FilterBar from '../components/FilterBar/FilterBar'; -import Footer from '../components/Footer/Footer'; +import ImportButton from '../components/ImportButton/ImportButton'; import PrintingRow from '../components/PrintingRow/PrintingRow'; const BADGE = { @@ -234,7 +234,7 @@ function HomePage() { onClick={exportCollection} style={{ background: '#2a2a2a', color: '#ccc', border: '1px solid #3a3a3a', borderRadius: '5px', padding: '3px 10px', fontSize: '12px', cursor: 'pointer' }} >Export -
diff --git a/src/services/api.js b/src/services/api.js index 3309ecf..b39d66b 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -37,13 +37,6 @@ export async function fetchDatabaseVersion() { return await response.json(); } -export async function triggerFullImport() { - const response = await fetch(`${API_BASE}/import/full-import`, { - method: 'POST' - }); - if (!response.ok) throw new Error('Failed to trigger full import'); - return await response.json(); -} export async function fetchSets() { const response = await fetch(`${API_BASE}/sets`);