import { useFetcher, useLocation, useNavigate } from "react-router"; import { useEffect, useState } from "react"; import { average } from "color.js"; import { imageUrl, type PaginatedResponse } from "api/api"; import PeriodSelector from "~/components/PeriodSelector"; interface ChartLayoutProps { title: "Top Albums" | "Top Tracks" | "Top Artists" | "Last Played"; initialData: PaginatedResponse; endpoint: string; render: (opts: { data: PaginatedResponse; page: number; onNext: () => void; onPrev: () => void; }) => React.ReactNode; } export default function ChartLayout({ title, initialData, endpoint, render, }: ChartLayoutProps) { const pgTitle = `${title} - Koito`; const fetcher = useFetcher(); const location = useLocation(); const navigate = useNavigate(); const currentParams = new URLSearchParams(location.search); const currentPage = parseInt(currentParams.get("page") || "1", 10); const data: PaginatedResponse = fetcher.data?.[endpoint] ? fetcher.data[endpoint] : initialData; const [bgColor, setBgColor] = useState("(--color-bg)"); useEffect(() => { if ((data?.items?.length ?? 0) === 0) return; const img = (data.items[0] as any)?.image; if (!img) return; average(imageUrl(img, "small"), { amount: 1 }).then((color) => { setBgColor(`rgba(${color[0]},${color[1]},${color[2]},0.4)`); }); }, [data]); const period = currentParams.get("period") ?? "day"; const year = currentParams.get("year"); const month = currentParams.get("month"); const week = currentParams.get("week"); const updateParams = (params: Record) => { const nextParams = new URLSearchParams(location.search); for (const key in params) { const val = params[key]; if (val !== null) { nextParams.set(key, val); } else { nextParams.delete(key); } } const url = `/${endpoint}?${nextParams.toString()}`; navigate(url, { replace: false }); }; const handleSetPeriod = (p: string) => { updateParams({ period: p, page: "1", year: null, month: null, week: null, }); }; const handleSetYear = (val: string) => { if (val == "") { updateParams({ period: period, page: "1", year: null, month: null, week: null, }); return; } updateParams({ period: null, page: "1", year: val, }); }; const handleSetMonth = (val: string) => { updateParams({ period: null, page: "1", year: year ?? new Date().getFullYear().toString(), month: val, }); }; const handleSetWeek = (val: string) => { updateParams({ period: null, page: "1", year: year ?? new Date().getFullYear().toString(), month: null, week: val, }); }; useEffect(() => { fetcher.load(`/${endpoint}?${currentParams.toString()}`); }, [location.search]); const setPage = (nextPage: number) => { const nextParams = new URLSearchParams(location.search); nextParams.set("page", String(nextPage)); const url = `/${endpoint}?${nextParams.toString()}`; fetcher.load(url); navigate(url, { replace: false }); }; const handleNextPage = () => setPage(currentPage + 1); const handlePrevPage = () => setPage(currentPage - 1); const yearOptions = Array.from( { length: 10 }, (_, i) => `${new Date().getFullYear() - i}` ); const monthOptions = Array.from({ length: 12 }, (_, i) => `${i + 1}`); const weekOptions = Array.from({ length: 53 }, (_, i) => `${i + 1}`); const getDateRange = (): string => { let from: Date; let to: Date; const now = new Date(); const currentYear = now.getFullYear(); const currentMonth = now.getMonth(); // 0-indexed const currentDate = now.getDate(); if (year && month) { from = new Date(parseInt(year), parseInt(month) - 1, 1); to = new Date(from); to.setMonth(from.getMonth() + 1); to.setDate(0); } else if (year && week) { const base = new Date(parseInt(year), 0, 1); // Jan 1 of the year const weekNumber = parseInt(week); from = new Date(base); from.setDate(base.getDate() + (weekNumber - 1) * 7); to = new Date(from); to.setDate(from.getDate() + 6); } else if (year) { from = new Date(parseInt(year), 0, 1); to = new Date(parseInt(year), 11, 31); } else { switch (period) { case "day": from = new Date(now); to = new Date(now); break; case "week": to = new Date(now); from = new Date(now); from.setDate(to.getDate() - 6); break; case "month": to = new Date(now); from = new Date(now); if (currentMonth === 0) { from = new Date(currentYear - 1, 11, currentDate); } else { from = new Date(currentYear, currentMonth - 1, currentDate); } break; case "year": to = new Date(now); from = new Date(currentYear - 1, currentMonth, currentDate); break; case "all_time": return "All Time"; default: return ""; } } const formatter = new Intl.DateTimeFormat(undefined, { year: "numeric", month: "long", day: "numeric", }); return `${formatter.format(from)} - ${formatter.format(to)}`; }; return (
{pgTitle}

{title}

{getDateRange()}

{render({ data, page: currentPage, onNext: handleNextPage, onPrev: handlePrevPage, })}
); }