Rename Footer to ImportButton, add SSE progress bar on import
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -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;
|
||||
@@ -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</button>
|
||||
<Footer onImportComplete={loadCards} />
|
||||
<ImportButton onImportComplete={loadCards} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user