refactor: decompose monolithic App.tsx into per-component files
- Extract views to src/components/views/ (Overview, Logbook, Trends, Settings, Auth) - Extract standalone components (EntryModal, ImportPreviewModal, ConfirmDialog, Sidebar, TopBar, MobileNav, EntryLedger, FiltersPanel, QuickAddPanel, etc.) - Move shared helpers to src/lib (dashboard, filters, userDisplay) and static data to src/data (accents, quickAdds) - App.tsx now orchestration-only (2971 -> 777 lines); no behavior change
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { FileSpreadsheet, Loader2, X } from "lucide-react";
|
||||
import { humanDateTime } from "../lib/metrics";
|
||||
import { MiniMetric } from "./MetricTiles";
|
||||
import type { ImportPreview } from "../types";
|
||||
|
||||
type ImportPreviewModalProps = {
|
||||
busy: boolean;
|
||||
preview: ImportPreview | null;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
export function ImportPreviewModal({
|
||||
busy,
|
||||
preview,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: ImportPreviewModalProps) {
|
||||
const validRows = preview?.rows.filter((row) => row.entry && !row.errors.length && !row.duplicate) ?? [];
|
||||
const invalidRows = preview?.rows.filter((row) => row.errors.length) ?? [];
|
||||
const duplicateRows = preview?.rows.filter((row) => row.duplicate) ?? [];
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{preview && (
|
||||
<motion.div
|
||||
className="modal-backdrop fixed inset-0 z-50 flex justify-center bg-black/60 backdrop-blur-xl"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="import-preview-title"
|
||||
>
|
||||
<motion.div
|
||||
className="modal-panel max-w-5xl"
|
||||
initial={{ opacity: 0, y: 18, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 14, scale: 0.98 }}
|
||||
>
|
||||
<div className="mb-5 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium uppercase tracking-[0.18em] text-pink-100">Excel import</p>
|
||||
<h2 id="import-preview-title" className="mt-1 text-3xl font-semibold tracking-tight text-white">
|
||||
Preview rows
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-slate-400">{preview.fileName}</p>
|
||||
</div>
|
||||
<button className="icon-button" type="button" onClick={onClose} aria-label="Close import preview">
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 grid gap-3 sm:grid-cols-3">
|
||||
<MiniMetric label="Ready" value={`${validRows.length}`} accent="#39d5ff" />
|
||||
<MiniMetric label="Duplicates" value={`${duplicateRows.length}`} accent="#ffd84d" />
|
||||
<MiniMetric label="Invalid" value={`${invalidRows.length}`} accent="#ff3448" />
|
||||
</div>
|
||||
|
||||
<div className="max-h-[48vh] overflow-auto rounded-lg border border-white/10">
|
||||
<table className="w-full min-w-[760px] border-collapse text-left text-sm">
|
||||
<thead className="sticky top-0 bg-[#0d142c] text-xs uppercase tracking-[0.14em] text-slate-400">
|
||||
<tr>
|
||||
<th className="px-3 py-3">Row</th>
|
||||
<th className="px-3 py-3">Status</th>
|
||||
<th className="px-3 py-3">Date</th>
|
||||
<th className="px-3 py-3">Flavour</th>
|
||||
<th className="px-3 py-3">Size</th>
|
||||
<th className="px-3 py-3">Cans</th>
|
||||
<th className="px-3 py-3">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{preview.rows.map((row) => (
|
||||
<tr key={row.rowNumber} className="border-t border-white/10">
|
||||
<td className="px-3 py-3 text-slate-400">{row.rowNumber}</td>
|
||||
<td className="px-3 py-3">
|
||||
<span className={`rounded px-2 py-1 text-xs font-semibold ${row.errors.length ? "bg-red-500/15 text-red-100" : row.duplicate ? "bg-amber-300/15 text-amber-100" : "bg-cyan-300/15 text-cyan-100"}`}>
|
||||
{row.errors.length ? "Invalid" : row.duplicate ? "Duplicate" : "Ready"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-3 text-slate-300">{row.entry ? humanDateTime(row.entry.dateTime) : "-"}</td>
|
||||
<td className="px-3 py-3 text-white">{row.entry?.flavour ?? "-"}</td>
|
||||
<td className="px-3 py-3 text-slate-300">{row.entry ? `${row.entry.sizeMl}ml` : "-"}</td>
|
||||
<td className="px-3 py-3 text-slate-300">{row.entry?.cans ?? "-"}</td>
|
||||
<td className="px-3 py-3 text-slate-400">{row.errors.join(" ") || row.duplicateReason || "Looks good."}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||
<button className="secondary-button justify-center" type="button" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="excel-button justify-center" type="button" disabled={!validRows.length || busy} onClick={onConfirm}>
|
||||
{busy ? <Loader2 className="animate-spin" size={17} aria-hidden="true" /> : <FileSpreadsheet size={17} aria-hidden="true" />}
|
||||
Import {validRows.length} row{validRows.length === 1 ? "" : "s"}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user