186 lines
6.0 KiB
TypeScript
186 lines
6.0 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { faClock, faHistory, faTrashAlt, faTimes, faCircleNotch } from "@fortawesome/free-solid-svg-icons";
|
|
import {
|
|
clearHistory,
|
|
formatRelative,
|
|
HISTORY_UPDATED_EVENT,
|
|
loadHistory,
|
|
removeFromHistory,
|
|
type HistoryEntry,
|
|
} from "../lib/history";
|
|
import type { InputValues } from "../lib/types";
|
|
import { useI18n } from "../lib/i18n";
|
|
|
|
interface HistoryDrawerProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onLoad: (entry: HistoryEntry) => void;
|
|
}
|
|
|
|
export function HistoryDrawer({ open, onClose, onLoad }: HistoryDrawerProps) {
|
|
const [entries, setEntries] = useState<HistoryEntry[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const { t } = useI18n();
|
|
|
|
const fetchHistory = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await loadHistory();
|
|
setEntries(data);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (open) fetchHistory();
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
window.addEventListener(HISTORY_UPDATED_EVENT, fetchHistory);
|
|
return () => window.removeEventListener(HISTORY_UPDATED_EVENT, fetchHistory);
|
|
}, []);
|
|
|
|
// Close on Escape
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
|
window.addEventListener("keydown", handler);
|
|
return () => window.removeEventListener("keydown", handler);
|
|
}, [open, onClose]);
|
|
|
|
return (
|
|
<>
|
|
{/* Backdrop */}
|
|
{open && (
|
|
<div
|
|
className="fixed inset-0 z-40 bg-black/30 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
aria-hidden
|
|
/>
|
|
)}
|
|
|
|
{/* Drawer */}
|
|
<div
|
|
className={`fixed top-0 right-0 h-full z-50 w-80 flex flex-col bg-bg-card/90 backdrop-blur-2xl border-l border-border shadow-2xl transition-transform duration-300 ease-out ${
|
|
open ? "translate-x-0" : "translate-x-full"
|
|
}`}
|
|
role="dialog"
|
|
aria-modal
|
|
aria-label="History"
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
|
|
<span className="flex items-center gap-2 text-sm font-bold text-fg">
|
|
<FontAwesomeIcon icon={faHistory} className="text-accent-primary" />
|
|
{t.history}
|
|
{!loading && entries.length > 0 && (
|
|
<span className="text-[10px] bg-accent-primary/15 text-accent-primary px-1.5 py-0.5 rounded-full font-bold">
|
|
{entries.length}
|
|
</span>
|
|
)}
|
|
</span>
|
|
<div className="flex items-center gap-2">
|
|
{loading && <FontAwesomeIcon icon={faCircleNotch} spin className="text-fg-muted w-3 h-3" />}
|
|
<button
|
|
onClick={onClose}
|
|
className="w-7 h-7 flex items-center justify-center rounded-lg text-fg-muted hover:text-fg hover:bg-bg-hover transition-colors"
|
|
aria-label="Close history"
|
|
>
|
|
<FontAwesomeIcon icon={faTimes} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Entries */}
|
|
<div className="flex-1 overflow-y-auto scrollbar-thin p-2 space-y-1">
|
|
{entries.length === 0 && !loading ? (
|
|
<p className="px-3 py-10 text-xs text-fg-muted text-center italic">{t.noHistory}</p>
|
|
) : (
|
|
entries.map((e) => (
|
|
<HistoryRow
|
|
key={e.id}
|
|
entry={e}
|
|
onLoad={() => { onLoad(e); onClose(); }}
|
|
onRemove={async () => {
|
|
await removeFromHistory(e.id);
|
|
}}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
{entries.length > 0 && (
|
|
<div className="p-2 border-t border-border shrink-0">
|
|
<button
|
|
type="button"
|
|
onClick={async () => {
|
|
if (window.confirm(t.clearHistoryConfirm)) {
|
|
await clearHistory();
|
|
setEntries([]);
|
|
}
|
|
}}
|
|
className="w-full inline-flex items-center justify-center gap-2 text-xs font-semibold text-fg-muted hover:text-rose-400 hover:bg-rose-400/10 transition-colors py-2 rounded-lg"
|
|
>
|
|
<FontAwesomeIcon icon={faTrashAlt} />
|
|
{t.clearHistory}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Keep old named export for compatibility but point to the drawer
|
|
export { HistoryDrawer as HistoryPanel };
|
|
|
|
function HistoryRow({
|
|
entry,
|
|
onLoad,
|
|
onRemove,
|
|
}: {
|
|
entry: HistoryEntry;
|
|
onLoad: () => void;
|
|
onRemove: () => void;
|
|
}) {
|
|
const summary = summarizeInput(entry.input);
|
|
return (
|
|
<div
|
|
className="group px-3 py-2.5 flex items-center justify-between gap-2 text-sm hover:bg-bg-hover/80 rounded-xl transition-colors cursor-pointer"
|
|
onClick={onLoad}
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="truncate text-fg font-semibold text-[13px]">{summary}</div>
|
|
<div className="flex items-center gap-1.5 text-[10px] text-fg-muted mt-0.5 uppercase tracking-wider font-semibold">
|
|
<FontAwesomeIcon icon={faClock} />
|
|
<span>{formatRelative(entry.timestamp)}</span>
|
|
{entry.input.mood && (
|
|
<>
|
|
<span className="opacity-40">·</span>
|
|
<span className="truncate">{entry.input.mood}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={(e) => { e.stopPropagation(); onRemove(); }}
|
|
className="opacity-0 group-hover:opacity-100 text-fg-muted hover:text-rose-400 transition-all p-1.5 rounded-lg hover:bg-rose-400/10"
|
|
aria-label="Remove from history"
|
|
>
|
|
<FontAwesomeIcon icon={faTimes} />
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function summarizeInput(v: InputValues): string {
|
|
const idea = v.idea.trim();
|
|
const max = 55;
|
|
if (idea.length <= max) return idea || "(no idea text)";
|
|
return idea.slice(0, max - 1) + "…";
|
|
}
|