Files

186 lines
6.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { Download, Trash2 } from "lucide-react";
import PageShell from "@/components/page-shell";
import { Button } from "@/components/ui/button";
import { ZenCard } from "@/components/ui/zen-card";
import BaziChartDisplay from "@/components/modes/bazi-chart";
import Markdown from "react-markdown";
import {
buildHistoryMarkdown,
deleteHistoryEntry,
downloadMarkdown,
loadHistory,
} from "@/lib/history/storage";
import { MODE_LABELS, type CalcHistoryEntry } from "@/lib/history/types";
export default function HistoryPageClient() {
const [items, setItems] = useState<CalcHistoryEntry[]>([]);
const [activeId, setActiveId] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
let cancelled = false;
(async () => {
try {
const list = await loadHistory();
if (cancelled) {
return;
}
setItems(list);
setActiveId(list[0]?.id ?? null);
} catch (e) {
if (!cancelled) {
setError(e instanceof Error ? e.message : String(e));
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
})();
return () => {
cancelled = true;
};
}, []);
const active = items.find((e) => e.id === activeId) ?? null;
async function handleDelete(id: string) {
try {
await deleteHistoryEntry(id);
const next = items.filter((item) => item.id !== id);
setItems(next);
if (activeId === id) {
setActiveId(next[0]?.id ?? null);
}
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
}
return (
<PageShell className="py-8">
<div className="mb-6 text-center">
<h1 className="text-2xl font-bold tracking-wide"></h1>
<p className="mt-2 text-sm text-muted-foreground">
</p>
</div>
{loading ? (
<ZenCard className="text-center text-sm text-muted-foreground">
</ZenCard>
) : error ? (
<ZenCard className="text-center text-sm text-destructive">{error}</ZenCard>
) : items.length === 0 ? (
<ZenCard className="text-center text-sm text-muted-foreground">
<p></p>
<Link href="/liuyao" className="mt-3 inline-block text-primary underline">
</Link>
</ZenCard>
) : (
<div className="grid gap-6 lg:grid-cols-[minmax(0,280px)_1fr]">
<div className="space-y-2">
{items.map((item) => (
<button
key={item.id}
type="button"
onClick={() => setActiveId(item.id)}
className={`w-full rounded-xl border px-4 py-3 text-left text-sm transition ${
activeId === item.id
? "border-primary/40 bg-primary/5"
: "border-border/60 bg-card/80 hover:border-primary/20"
}`}
>
<p className="font-medium">{item.title}</p>
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
{item.question}
</p>
{item.baziChart && (
<p className="mt-1 font-mono text-xs text-muted-foreground">
{item.baziChart.pillars.year.ganZhi}{" "}
{item.baziChart.pillars.month.ganZhi}{" "}
{item.baziChart.pillars.day.ganZhi}{" "}
{item.baziChart.pillars.time.ganZhi}
</p>
)}
<p className="mt-1 text-xs text-muted-foreground">
{MODE_LABELS[item.mode]} ·{" "}
{new Date(item.createdAt).toLocaleString("zh-CN")}
</p>
</button>
))}
</div>
{active && (
<ZenCard
title={active.title}
subtitle={`${MODE_LABELS[active.mode]} · ${new Date(active.createdAt).toLocaleString("zh-CN")}`}
>
<p className="text-sm text-muted-foreground">{active.question}</p>
{active.summary && (
<p className="text-sm text-muted-foreground">{active.summary}</p>
)}
{active.baziInput && (
<div className="rounded-md border bg-background/50 p-3 text-sm text-muted-foreground">
<p>
{active.baziInput.birthPlaceName} ·{" "}
{active.baziInput.date}{" "}
{active.baziInput.unknownHour
? "时辰不详"
: active.baziInput.time}{" "}
· {active.baziInput.gender === "male" ? "男" : "女"}
</p>
</div>
)}
{active.baziChart && <BaziChartDisplay chart={active.baziChart} />}
{active.hexagram && (
<div className="rounded-md border bg-background/50 p-3 text-sm">
<p className="font-medium">{active.hexagram.guaTitle}</p>
<p className="mt-1 text-muted-foreground">
{active.hexagram.guaResult}
</p>
</div>
)}
<div className="flex flex-wrap gap-2">
<Button
size="sm"
variant="outline"
onClick={() =>
downloadMarkdown(
buildHistoryMarkdown(active),
`${active.title}.md`,
)
}
>
<Download size={14} className="mr-1" />
Markdown
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDelete(active.id)}
>
<Trash2 size={14} className="mr-1" />
</Button>
</div>
<div className="max-h-[60vh] overflow-y-auto rounded-md border bg-background/50 p-4">
<Markdown className="prose max-w-none text-sm dark:prose-invert">
{active.completion}
</Markdown>
</div>
</ZenCard>
)}
</div>
)}
</PageShell>
);
}