import React from "react";
import { fetchStats, StatsResponse, getBookCoverUrl } from "../lib/api";
import { Card, CardContent, CardHeader, CardTitle } from "./components/ui";
import { RcDonutChart, RcBarChart, RcAreaChart, RcStackedBar, RcHorizontalBar } from "./components/DashboardCharts";
import { PeriodToggle } from "./components/PeriodToggle";
import Image from "next/image";
import Link from "next/link";
import { getServerTranslations } from "../lib/i18n/server";
import type { TranslateFunction } from "../lib/i18n/dictionaries";
export const dynamic = "force-dynamic";
function formatBytes(bytes: number): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
}
function formatNumber(n: number, locale: string): string {
return n.toLocaleString(locale === "fr" ? "fr-FR" : "en-US");
}
function formatChartLabel(raw: string, period: "day" | "week" | "month", locale: string): string {
const loc = locale === "fr" ? "fr-FR" : "en-US";
if (period === "month") {
// raw = "YYYY-MM"
const [y, m] = raw.split("-");
const d = new Date(Number(y), Number(m) - 1, 1);
return d.toLocaleDateString(loc, { month: "short" });
}
if (period === "week") {
// raw = "YYYY-MM-DD" (Monday of the week)
const d = new Date(raw + "T00:00:00");
return d.toLocaleDateString(loc, { day: "numeric", month: "short" });
}
// day: raw = "YYYY-MM-DD"
const d = new Date(raw + "T00:00:00");
return d.toLocaleDateString(loc, { weekday: "short", day: "numeric" });
}
// Horizontal progress bar for metadata quality (stays server-rendered, no recharts needed)
function HorizontalBar({ label, value, max, subLabel, color = "var(--color-primary)" }: { label: string; value: number; max: number; subLabel?: string; color?: string }) {
const pct = max > 0 ? (value / max) * 100 : 0;
return (
{label}
{subLabel || value}
);
}
export default async function DashboardPage({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParamsAwaited = await searchParams;
const rawPeriod = searchParamsAwaited.period;
const period = rawPeriod === "day" ? "day" as const : rawPeriod === "week" ? "week" as const : "month" as const;
const { t, locale } = await getServerTranslations();
let stats: StatsResponse | null = null;
try {
stats = await fetchStats(period);
} catch (e) {
console.error("Failed to fetch stats:", e);
}
if (!stats) {
return (
StripStream Backoffice
{t("dashboard.loadError")}
);
}
const { overview, reading_status, currently_reading = [], recently_read = [], reading_over_time = [], by_format, by_library, top_series, additions_over_time, metadata } = stats;
const readingColors = ["hsl(220 13% 70%)", "hsl(45 93% 47%)", "hsl(142 60% 45%)"];
const formatColors = [
"hsl(198 78% 37%)", "hsl(142 60% 45%)", "hsl(45 93% 47%)",
"hsl(2 72% 48%)", "hsl(280 60% 50%)", "hsl(32 80% 50%)",
"hsl(170 60% 45%)", "hsl(220 60% 50%)",
];
const noDataLabel = t("common.noData");
return (
{/* Header */}
{t("dashboard.title")}
{t("dashboard.subtitle")}
{/* Overview stat cards */}
{/* Currently reading + Recently read */}
{(currently_reading.length > 0 || recently_read.length > 0) && (
{/* Currently reading */}
{t("dashboard.currentlyReading")}
{currently_reading.length === 0 ? (
{t("dashboard.noCurrentlyReading")}
) : (
{currently_reading.slice(0, 8).map((book) => {
const pct = book.page_count > 0 ? Math.round((book.current_page / book.page_count) * 100) : 0;
return (
{book.title}
{book.series &&
{book.series}
}
{t("dashboard.pageProgress", { current: book.current_page, total: book.page_count })}
);
})}
)}
{/* Recently read */}
{t("dashboard.recentlyRead")}
{recently_read.length === 0 ? (
{t("dashboard.noRecentlyRead")}
) : (
{recently_read.map((book) => (
{book.title}
{book.series &&
{book.series}
}
{book.last_read_at}
))}
)}
)}
{/* Reading activity line chart */}
{t("dashboard.readingActivity")}
({ label: formatChartLabel(m.month, period, locale), value: m.books_read }))}
color="hsl(142 60% 45%)"
/>
{/* Charts row */}
{/* Reading status donut */}
{t("dashboard.readingStatus")}
{/* By format donut */}
{t("dashboard.byFormat")}
({
name: (f.format || t("dashboard.unknown")).toUpperCase(),
value: f.count,
color: formatColors[i % formatColors.length],
}))}
/>
{/* By library donut */}
{t("dashboard.byLibrary")}
({
name: l.library_name,
value: l.book_count,
color: formatColors[i % formatColors.length],
}))}
/>
{/* Metadata row */}
{/* Series metadata coverage donut */}
{t("dashboard.metadataCoverage")}
{/* By provider donut */}
{t("dashboard.byProvider")}
({
name: p.provider.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
value: p.count,
color: formatColors[i % formatColors.length],
}))}
/>
{/* Book metadata quality */}
{t("dashboard.bookMetadata")}
0 ? `${Math.round((metadata.books_with_summary / overview.total_books) * 100)}%` : "0%"}
color="hsl(198 78% 37%)"
/>
0 ? `${Math.round((metadata.books_with_isbn / overview.total_books) * 100)}%` : "0%"}
color="hsl(280 60% 50%)"
/>
{/* Libraries breakdown + Top series */}
{by_library.length > 0 && (
{t("dashboard.libraries")}
({
name: lib.library_name,
read: lib.read_count,
reading: lib.reading_count,
unread: lib.unread_count,
sizeLabel: formatBytes(lib.size_bytes),
}))}
labels={{
read: t("status.read"),
reading: t("status.reading"),
unread: t("status.unread"),
books: t("dashboard.books"),
}}
/>
)}
{/* Top series */}
{t("dashboard.popularSeries")}
({
name: s.series,
value: s.book_count,
subLabel: t("dashboard.readCount", { read: s.read_count, total: s.book_count }),
}))}
color="hsl(142 60% 45%)"
/>
{/* Additions line chart – full width */}
{t("dashboard.booksAdded")}
({ label: formatChartLabel(m.month, period, locale), value: m.books_added }))}
color="hsl(198 78% 37%)"
/>
{/* Quick links */}
);
}
function StatCard({ icon, label, value, color }: { icon: string; label: string; value: string; color: string }) {
const icons: Record = {
book: ,
series: ,
library: ,
pages: ,
author: ,
size: ,
};
const colorClasses: Record = {
primary: "bg-primary/10 text-primary",
success: "bg-success/10 text-success",
warning: "bg-warning/10 text-warning",
};
return (
);
}
function QuickLinks({ t }: { t: TranslateFunction }) {
const links = [
{ href: "/libraries", label: t("nav.libraries"), bg: "bg-primary/10", text: "text-primary", hoverBg: "group-hover:bg-primary", hoverText: "group-hover:text-primary-foreground", icon: },
{ href: "/books", label: t("nav.books"), bg: "bg-success/10", text: "text-success", hoverBg: "group-hover:bg-success", hoverText: "group-hover:text-white", icon: },
{ href: "/series", label: t("nav.series"), bg: "bg-warning/10", text: "text-warning", hoverBg: "group-hover:bg-warning", hoverText: "group-hover:text-white", icon: },
{ href: "/jobs", label: t("nav.jobs"), bg: "bg-destructive/10", text: "text-destructive", hoverBg: "group-hover:bg-destructive", hoverText: "group-hover:text-destructive-foreground", icon: },
];
return (
{links.map((l) => (
{l.label}
))}
);
}