feat: complete UI overhaul (liquid glass theme, i18n, FontAwesome)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Clock, History, Trash2, X } from "lucide-react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faClock, faHistory, faTrashAlt, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
clearHistory,
|
||||
formatRelative,
|
||||
@@ -9,88 +10,72 @@ import {
|
||||
type HistoryEntry,
|
||||
} from "../lib/history";
|
||||
import type { InputValues } from "../lib/types";
|
||||
import { useI18n } from "../lib/i18n";
|
||||
|
||||
interface HistoryPanelProps {
|
||||
onLoad: (entry: HistoryEntry) => void;
|
||||
}
|
||||
|
||||
export function HistoryPanel({ onLoad }: HistoryPanelProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [entries, setEntries] = useState<HistoryEntry[]>([]);
|
||||
const { t } = useI18n();
|
||||
|
||||
// Refresh the list whenever the panel is opened.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setEntries(loadHistory());
|
||||
}, [open]);
|
||||
}, []);
|
||||
|
||||
// And whenever the history is updated from elsewhere in the app (after a
|
||||
// successful generation, or a delete/clear from a different panel).
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
if (!open) return;
|
||||
setEntries(loadHistory());
|
||||
};
|
||||
window.addEventListener(HISTORY_UPDATED_EVENT, handler);
|
||||
return () => window.removeEventListener(HISTORY_UPDATED_EVENT, handler);
|
||||
}, [open]);
|
||||
|
||||
if (entries.length === 0 && !open) {
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="w-full flex items-center justify-between gap-2 px-3 py-2 rounded-lg border border-border bg-bg-card/50 hover:bg-bg-hover transition-colors text-sm"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span className="flex items-center gap-2 text-fg">
|
||||
<History className="w-4 h-4" />
|
||||
Recent generations
|
||||
<div className="flex flex-col h-full bg-bg-card/40 backdrop-blur-md border border-border rounded-xl shadow-lg overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-border bg-bg/20">
|
||||
<span className="flex items-center gap-2 text-sm font-bold text-fg">
|
||||
<FontAwesomeIcon icon={faHistory} className="text-accent-secondary" />
|
||||
{t.history}
|
||||
{entries.length > 0 && (
|
||||
<span className="text-xs text-fg-muted">({entries.length})</span>
|
||||
<span className="text-[10px] bg-accent-primary/20 text-accent-primary px-1.5 py-0.5 rounded-full">
|
||||
{entries.length}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-xs text-fg-muted">{open ? "Hide" : "Show"}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<div className="mt-2 rounded-lg border border-border bg-bg-card/30 divide-y divide-border">
|
||||
{entries.length === 0 ? (
|
||||
<p className="px-3 py-4 text-xs text-fg-muted text-center">
|
||||
No recent generations yet.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{entries.map((e) => (
|
||||
<HistoryRow
|
||||
key={e.id}
|
||||
entry={e}
|
||||
onLoad={() => {
|
||||
onLoad(e);
|
||||
setOpen(false);
|
||||
}}
|
||||
onRemove={() => setEntries(removeFromHistory(e.id))}
|
||||
/>
|
||||
))}
|
||||
<div className="px-3 py-2 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
clearHistory();
|
||||
setEntries([]);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 text-xs text-fg-muted hover:text-rose-300 transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto scrollbar-thin p-2 space-y-1">
|
||||
{entries.length === 0 ? (
|
||||
<p className="px-3 py-6 text-xs text-fg-muted text-center italic">
|
||||
{t.noHistory}
|
||||
</p>
|
||||
) : (
|
||||
entries.map((e) => (
|
||||
<HistoryRow
|
||||
key={e.id}
|
||||
entry={e}
|
||||
onLoad={() => onLoad(e)}
|
||||
onRemove={() => setEntries(removeFromHistory(e.id))}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{entries.length > 0 && (
|
||||
<div className="p-2 border-t border-border bg-bg/20">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
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.history === 'History' ? 'Clear history' : 'Verlauf löschen'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -108,31 +93,27 @@ function HistoryRow({
|
||||
}) {
|
||||
const summary = summarizeInput(entry.input);
|
||||
return (
|
||||
<div className="px-3 py-2 flex items-center justify-between gap-2 text-sm hover:bg-bg-hover/40 transition-colors">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onLoad}
|
||||
className="flex-1 text-left min-w-0"
|
||||
>
|
||||
<div className="truncate text-fg">{summary}</div>
|
||||
<div className="flex items-center gap-2 text-[11px] text-fg-muted mt-0.5">
|
||||
<Clock className="w-3 h-3" />
|
||||
<div className="group px-3 py-2.5 flex items-center justify-between gap-2 text-sm hover:bg-bg-hover/80 rounded-lg transition-colors cursor-pointer" onClick={onLoad}>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="truncate text-fg font-medium text-[13px]">{summary}</div>
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-fg-muted mt-1 uppercase tracking-wider font-semibold">
|
||||
<FontAwesomeIcon icon={faClock} />
|
||||
<span>{formatRelative(entry.timestamp)}</span>
|
||||
{entry.input.mood && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span className="opacity-50">·</span>
|
||||
<span className="truncate">{entry.input.mood}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="text-fg-muted hover:text-rose-300 transition-colors p-1"
|
||||
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-md hover:bg-rose-400/10"
|
||||
aria-label="Remove from history"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
<FontAwesomeIcon icon={faTimes} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -140,7 +121,7 @@ function HistoryRow({
|
||||
|
||||
function summarizeInput(v: InputValues): string {
|
||||
const idea = v.idea.trim();
|
||||
const max = 90;
|
||||
const max = 60;
|
||||
if (idea.length <= max) return idea || "(no idea text)";
|
||||
return idea.slice(0, max - 1) + "…";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user