import ActivityHeatmap from "@/components/ActivityHeatmap";
import { useI18n } from "@/i18n/I18nProvider";
import { dateFnsLocale } from "@/lib/dateFns";
import {
type ProjectMetrics,
fetchDailyActivity,
fetchProjectMetrics,
} from "@/lib/queries";
import type { DailyCount } from "@/lib/queries";
import {
differenceInDays,
formatDistanceToNow,
parseISO,
} from "date-fns";
import { useEffect, useState } from "react";
const HEATMAP_DAYS = 364; // 52 weeks (one year) — GitHub-style
export default function ProjectHistory({ project }: { project: string }) {
const { t, lang } = useI18n();
const [metrics, setMetrics] = useState<ProjectMetrics | null>(null);
const [heatmap, setHeatmap] = useState<DailyCount[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let alive = true;
setLoading(true);
setError(null);
Promise.all([
fetchProjectMetrics(project),
fetchDailyActivity(HEATMAP_DAYS, project),
])
.then(([m, hm]) => {
if (!alive) return;
setMetrics(m);
setHeatmap(hm);
})
.catch((e) => alive && setError(String(e?.message ?? e)))
.finally(() => alive && setLoading(false));
return () => {
alive = false;
};
}, [project]);
if (loading) {
return (
<div className="text-sm text-muted-foreground">{t("common.loading")}</div>
);
}
if (error) {
return (
<div className="rounded-md border border-red-300 bg-red-50 p-3 text-sm text-red-700">
{error}
</div>
);
}
const totalHeatmap = heatmap.reduce((s, d) => s + d.count, 0);
return (
<div className="space-y-6">
<MetricsHeader metrics={metrics} lang={lang} />
<ActivityHeatmap
data={heatmap}
title={t("history.heatmap.title")}
trailing={`${totalHeatmap} ${t("overview.activity.events")}`}
/>
</div>
);
}
function MetricsHeader({
metrics,
lang,
}: {
metrics: ProjectMetrics | null;
lang: ReturnType<typeof useI18n>["lang"];
}) {
const { t } = useI18n();
if (!metrics) return null;
const days = metrics.first_event_at
? Math.max(
1,
differenceInDays(new Date(), parseISO(metrics.first_event_at)) + 1,
)
: 0;
const lastAgo = metrics.last_event_at
? formatDistanceToNow(parseISO(metrics.last_event_at), {
addSuffix: true,
locale: dateFnsLocale(lang),
})
: "—";
return (
<div className="rounded-md border bg-card p-4">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<Metric
label={t("history.metric.days")}
value={days > 0 ? `${days}` : "—"}
/>
<Metric
label={t("history.metric.totalEvents")}
value={metrics.total_events.toLocaleString()}
/>
<Metric
label={t("history.metric.strongMemories")}
value={metrics.strong_memory_count.toLocaleString()}
/>
<Metric
label={t("history.metric.lastActivity")}
value={lastAgo}
mono={false}
/>
</div>
</div>
);
}
function Metric({
label,
value,
mono = true,
}: {
label: string;
value: string;
mono?: boolean;
}) {
return (
<div>
<div className="text-[10px] uppercase tracking-widest text-muted-foreground">
{label}
</div>
<div
className={`mt-1 text-lg font-semibold ${
mono ? "tabular-nums" : "text-base font-medium"
}`}
>
{value}
</div>
</div>
);
}