Rename Footer to ImportButton, add SSE progress bar on import
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-05-22 21:18:46 +02:00
parent 5d7121bec8
commit 50caa88cd9
4 changed files with 127 additions and 74 deletions
-65
View File
@@ -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 (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', fontSize: '12px', color: '#555' }}>
<span>DB: {dbVersion ?? '…'}</span>
<button
onClick={handleImport}
disabled={importing}
style={{
background: '#2a2a2a', color: '#ccc', border: '1px solid #3a3a3a',
borderRadius: '5px', padding: '3px 10px', fontSize: '12px',
cursor: importing ? 'not-allowed' : 'pointer',
opacity: importing ? 0.5 : 1,
}}
>
{importing ? 'Importing…' : 'Full Import'}
</button>
</div>
{showModal && createPortal(
<>
<div className="modal-backdrop" onClick={() => setShowModal(false)} />
<div className="import-modal">
<div>{modalMessage}</div>
<button onClick={() => setShowModal(false)}>Close</button>
</div>
</>,
document.body
)}
</>
);
}
export default Footer;
@@ -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 (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', fontSize: '12px', color: '#555' }}>
<span>DB: {dbVersion ?? '…'}</span>
<button
onClick={handleImport}
disabled={importing}
style={{
background: '#2a2a2a', color: '#ccc', border: '1px solid #3a3a3a',
borderRadius: '5px', padding: '3px 10px', fontSize: '12px',
cursor: importing ? 'not-allowed' : 'pointer',
opacity: importing ? 0.6 : 1,
}}
>
Full Import
</button>
</div>
{showModal && createPortal(
<>
<div className="modal-backdrop" onClick={() => done && setShowModal(false)} />
<div className="import-modal">
<div style={{ fontSize: '13px', color: isError ? '#c04040' : '#ccc', marginBottom: '12px' }}>
{progressMessage}
</div>
<div style={{ background: '#2a2a2a', borderRadius: '4px', height: '6px', overflow: 'hidden', marginBottom: '12px' }}>
<div style={{
height: '100%',
width: `${Math.round(progress * 100)}%`,
background: isError ? '#c04040' : '#4a90d9',
borderRadius: '4px',
transition: 'width 0.3s ease',
}} />
</div>
{!done && (
<div style={{ fontSize: '11px', color: '#444', textAlign: 'right' }}>
{Math.round(progress * 100)}%
</div>
)}
{done && (
<>
{resultMessage && (
<div style={{ fontSize: '12px', color: '#555', marginBottom: '12px' }}>{resultMessage}</div>
)}
<button onClick={() => setShowModal(false)}>Close</button>
</>
)}
</div>
</>,
document.body
)}
</>
);
}
export default ImportButton;
+2 -2
View File
@@ -8,7 +8,7 @@ import { fuzzyMatch } from '../utils/search';
import CardRow from '../components/CardRow/CardRow'; import CardRow from '../components/CardRow/CardRow';
import SearchBar from '../components/SearchBar/SearchBar'; import SearchBar from '../components/SearchBar/SearchBar';
import FilterBar from '../components/FilterBar/FilterBar'; import FilterBar from '../components/FilterBar/FilterBar';
import Footer from '../components/Footer/Footer'; import ImportButton from '../components/ImportButton/ImportButton';
import PrintingRow from '../components/PrintingRow/PrintingRow'; import PrintingRow from '../components/PrintingRow/PrintingRow';
const BADGE = { const BADGE = {
@@ -234,7 +234,7 @@ function HomePage() {
onClick={exportCollection} onClick={exportCollection}
style={{ background: '#2a2a2a', color: '#ccc', border: '1px solid #3a3a3a', borderRadius: '5px', padding: '3px 10px', fontSize: '12px', cursor: 'pointer' }} style={{ background: '#2a2a2a', color: '#ccc', border: '1px solid #3a3a3a', borderRadius: '5px', padding: '3px 10px', fontSize: '12px', cursor: 'pointer' }}
>Export</button> >Export</button>
<Footer onImportComplete={loadCards} /> <ImportButton onImportComplete={loadCards} />
</div> </div>
</div> </div>
-7
View File
@@ -37,13 +37,6 @@ export async function fetchDatabaseVersion() {
return await response.json(); 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() { export async function fetchSets() {
const response = await fetch(`${API_BASE}/sets`); const response = await fetch(`${API_BASE}/sets`);