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, })}
) }