feat(web): wire routine export/import into history page UI
This commit is contained in:
parent
77254b751e
commit
141fcc2a38
@ -7,7 +7,8 @@ import { computeStreak } from '@/lib/stats';
|
|||||||
import { StatsView } from '@/components/StatsView';
|
import { StatsView } from '@/components/StatsView';
|
||||||
import { StreakCard } from '@/components/StreakCard';
|
import { StreakCard } from '@/components/StreakCard';
|
||||||
import { TimerCard } from '@/components/TimerCard';
|
import { TimerCard } from '@/components/TimerCard';
|
||||||
import { downloadExport, readFileAsText, parseImportData, importTimers } from '@/lib/export';
|
import { downloadExportAll, readFileAsText, parseImportData, importTimers, importRoutines } from '@/lib/export';
|
||||||
|
import { useRoutineStore } from '@/lib/routine-store';
|
||||||
import { importCalendar } from '@/lib/calendar-import';
|
import { importCalendar } from '@/lib/calendar-import';
|
||||||
import { ArrowLeft, Download, Upload, Calendar, Search, Filter, RotateCcw, FileSpreadsheet, Eye, Check, X } from 'lucide-react';
|
import { ArrowLeft, Download, Upload, Calendar, Search, Filter, RotateCcw, FileSpreadsheet, Eye, Check, X } from 'lucide-react';
|
||||||
import { getCategoryById, getAllCategories } from '@/lib/categories';
|
import { getCategoryById, getAllCategories } from '@/lib/categories';
|
||||||
@ -17,6 +18,7 @@ import type { Timer } from '@/lib/timer-engine';
|
|||||||
export default function HistoryPage() {
|
export default function HistoryPage() {
|
||||||
const timers = useTimerStore((s) => s.timers);
|
const timers = useTimerStore((s) => s.timers);
|
||||||
const now = useTimerStore((s) => s.now);
|
const now = useTimerStore((s) => s.now);
|
||||||
|
const routines = useRoutineStore((s) => s.routines);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [filterCategory, setFilterCategory] = useState<string | ''>('');
|
const [filterCategory, setFilterCategory] = useState<string | ''>('');
|
||||||
@ -50,7 +52,7 @@ export default function HistoryPage() {
|
|||||||
|
|
||||||
if (!mounted) return null;
|
if (!mounted) return null;
|
||||||
|
|
||||||
const handleExport = () => downloadExport(timers);
|
const handleExport = () => downloadExportAll(timers, routines);
|
||||||
|
|
||||||
const handleJsonImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleJsonImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
@ -62,14 +64,28 @@ export default function HistoryPage() {
|
|||||||
setImportStatus('Invalid file format. Expected a ChronoMind export JSON.');
|
setImportStatus('Invalid file format. Expected a ChronoMind export JSON.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = importTimers(data, timers);
|
const timerResult = importTimers(data, timers);
|
||||||
// Add the imported timers to the store
|
// Add the imported timers to the store
|
||||||
const existingIds = new Set(timers.map((t) => t.id));
|
const existingIds = new Set(timers.map((t) => t.id));
|
||||||
const newTimers = data.timers.filter((t) => !existingIds.has(t.id));
|
const newTimers = data.timers.filter((t) => !existingIds.has(t.id));
|
||||||
if (newTimers.length > 0) {
|
if (newTimers.length > 0) {
|
||||||
useTimerStore.setState((s) => ({ timers: [...s.timers, ...newTimers] }));
|
useTimerStore.setState((s) => ({ timers: [...s.timers, ...newTimers] }));
|
||||||
}
|
}
|
||||||
setImportStatus(`Imported ${result.imported} timers. ${result.skipped} skipped.${result.errors.length > 0 ? ` Errors: ${result.errors.join(', ')}` : ''}`);
|
// Import routines if present
|
||||||
|
const routineResult = importRoutines(data, routines);
|
||||||
|
if (data.routines && data.routines.length > 0) {
|
||||||
|
const existingRoutineIds = new Set(routines.map((r) => r.id));
|
||||||
|
const newRoutines = data.routines.filter((r) => !existingRoutineIds.has(r.id));
|
||||||
|
if (newRoutines.length > 0) {
|
||||||
|
useRoutineStore.setState((s) => ({ routines: [...s.routines, ...newRoutines] }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parts = [`Imported ${timerResult.imported} timers`];
|
||||||
|
if (routineResult.imported > 0) parts.push(`${routineResult.imported} routines`);
|
||||||
|
const totalSkipped = timerResult.skipped + routineResult.skipped;
|
||||||
|
if (totalSkipped > 0) parts.push(`${totalSkipped} skipped`);
|
||||||
|
const allErrors = [...timerResult.errors, ...routineResult.errors];
|
||||||
|
setImportStatus(`${parts.join(', ')}.${allErrors.length > 0 ? ` Errors: ${allErrors.join(', ')}` : ''}`);
|
||||||
} catch {
|
} catch {
|
||||||
setImportStatus('Failed to read file.');
|
setImportStatus('Failed to read file.');
|
||||||
}
|
}
|
||||||
@ -300,15 +316,15 @@ export default function HistoryPage() {
|
|||||||
style={{ backgroundColor: 'var(--cm-surface-card)', borderColor: 'var(--cm-border)' }}
|
style={{ backgroundColor: 'var(--cm-surface-card)', borderColor: 'var(--cm-border)' }}
|
||||||
>
|
>
|
||||||
<h3 className="text-sm font-semibold mb-2 flex items-center gap-2" style={{ color: 'var(--cm-text-primary)' }}>
|
<h3 className="text-sm font-semibold mb-2 flex items-center gap-2" style={{ color: 'var(--cm-text-primary)' }}>
|
||||||
<Download size={16} /> Export Timers
|
<Download size={16} /> Export Data
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs mb-3" style={{ color: 'var(--cm-text-tertiary)' }}>
|
<p className="text-xs mb-3" style={{ color: 'var(--cm-text-tertiary)' }}>
|
||||||
Download all {timers.length} timers as a JSON file.
|
Download all {timers.length} timers and {routines.length} routines as a JSON file.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
disabled={timers.length === 0}
|
disabled={timers.length === 0 && routines.length === 0}
|
||||||
className="flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:opacity-30"
|
className="flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium cursor-pointer disabled:opacity-30"
|
||||||
style={{ backgroundColor: 'var(--cm-accent)', color: '#fff' }}
|
style={{ backgroundColor: 'var(--cm-accent)', color: '#fff' }}
|
||||||
>
|
>
|
||||||
@ -331,10 +347,10 @@ export default function HistoryPage() {
|
|||||||
style={{ backgroundColor: 'var(--cm-surface-card)', borderColor: 'var(--cm-border)' }}
|
style={{ backgroundColor: 'var(--cm-surface-card)', borderColor: 'var(--cm-border)' }}
|
||||||
>
|
>
|
||||||
<h3 className="text-sm font-semibold mb-2 flex items-center gap-2" style={{ color: 'var(--cm-text-primary)' }}>
|
<h3 className="text-sm font-semibold mb-2 flex items-center gap-2" style={{ color: 'var(--cm-text-primary)' }}>
|
||||||
<Upload size={16} /> Import Timers (JSON)
|
<Upload size={16} /> Import Data (JSON)
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs mb-3" style={{ color: 'var(--cm-text-tertiary)' }}>
|
<p className="text-xs mb-3" style={{ color: 'var(--cm-text-tertiary)' }}>
|
||||||
Restore timers from a previously exported ChronoMind JSON file.
|
Restore timers and routines from a previously exported ChronoMind JSON file.
|
||||||
</p>
|
</p>
|
||||||
<label
|
<label
|
||||||
className="inline-block px-4 py-2 rounded-lg text-sm font-medium cursor-pointer"
|
className="inline-block px-4 py-2 rounded-lg text-sm font-medium cursor-pointer"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user