diff --git a/client/app/components/rewind/Rewind.tsx b/client/app/components/rewind/Rewind.tsx index 8e1908c..2553b35 100644 --- a/client/app/components/rewind/Rewind.tsx +++ b/client/app/components/rewind/Rewind.tsx @@ -8,9 +8,16 @@ interface Props { } export default function Rewind(props: Props) { - const artistimg = props.stats.top_artists[0].image; - const albumimg = props.stats.top_albums[0].image; - const trackimg = props.stats.top_tracks[0].image; + const artistimg = props.stats.top_artists[0]?.image; + const albumimg = props.stats.top_albums[0]?.image; + const trackimg = props.stats.top_tracks[0]?.image; + if ( + !props.stats.top_artists[0] || + !props.stats.top_albums[0] || + !props.stats.top_tracks[0] + ) { + return

Not enough data exists to create a Rewind for this period :(

; + } return (

{props.stats.title}

diff --git a/client/app/components/sidebar/Sidebar.tsx b/client/app/components/sidebar/Sidebar.tsx index 15ac8b5..2bd88f3 100644 --- a/client/app/components/sidebar/Sidebar.tsx +++ b/client/app/components/sidebar/Sidebar.tsx @@ -2,7 +2,7 @@ import { ExternalLink, History, Home, Info } from "lucide-react"; import SidebarSearch from "./SidebarSearch"; import SidebarItem from "./SidebarItem"; import SidebarSettings from "./SidebarSettings"; -import { getRewindYear } from "~/utils/utils"; +import { getRewindParams, getRewindYear } from "~/utils/utils"; export default function Sidebar() { const iconSize = 20; @@ -45,7 +45,7 @@ export default function Sidebar() { {}} modal={<>} diff --git a/client/app/routes/RewindPage.tsx b/client/app/routes/RewindPage.tsx index b14e5fc..1c1727e 100644 --- a/client/app/routes/RewindPage.tsx +++ b/client/app/routes/RewindPage.tsx @@ -1,52 +1,201 @@ import Rewind from "~/components/rewind/Rewind"; import type { Route } from "./+types/Home"; -import { type RewindStats } from "api/api"; -import { useState } from "react"; +import { imageUrl, type RewindStats } from "api/api"; +import { useEffect, useState } from "react"; import type { LoaderFunctionArgs } from "react-router"; import { useLoaderData } from "react-router"; -import { getRewindYear } from "~/utils/utils"; +import { getRewindParams, getRewindYear } from "~/utils/utils"; +import { useNavigate } from "react-router"; +import { average } from "color.js"; +import { ChevronLeft, ChevronRight } from "lucide-react"; + +// TODO: Bind year and month selectors to what data actually exists + +const months = [ + "Full Year", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; export async function clientLoader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); - const year = url.searchParams.get("year") || getRewindYear(); + const year = + parseInt(url.searchParams.get("year") || "0") || getRewindParams().year; + const month = + parseInt(url.searchParams.get("month") || "0") || getRewindParams().month; - const res = await fetch(`/apis/web/v1/summary?year=${year}`); + const res = await fetch(`/apis/web/v1/summary?year=${year}&month=${month}`); if (!res.ok) { throw new Response("Failed to load summary", { status: 500 }); } const stats: RewindStats = await res.json(); - stats.title = `Your ${year} Rewind`; + stats.title = `Your ${month === 0 ? "" : months[month]} ${year} Rewind`; return { stats }; } -export function meta({}: Route.MetaArgs) { - return [ - { title: `Rewind - Koito` }, - { name: "description", content: "Rewind - Koito" }, - ]; -} - export default function RewindPage() { + const currentParams = new URLSearchParams(location.search); + let year = parseInt(currentParams.get("year") || "0"); + let month = parseInt(currentParams.get("month") || "0"); + const navigate = useNavigate(); const [showTime, setShowTime] = useState(false); const { stats: stats } = useLoaderData<{ stats: RewindStats }>(); + + const [bgColor, setBgColor] = useState("(--color-bg)"); + + useEffect(() => { + if (!stats.top_artists[0]) return; + + const img = (stats.top_artists[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)`); + }); + }, [stats]); + + const updateParams = (params: Record) => { + const nextParams = new URLSearchParams(location.search); + + for (const key in params) { + const val = params[key]; + + if (val !== null && val !== "0") { + nextParams.set(key, val); + } else { + nextParams.delete(key); + } + } + + const url = `/rewind?${nextParams.toString()}`; + + navigate(url, { replace: false }); + }; + + const navigateMonth = (direction: "prev" | "next") => { + if (direction === "next") { + if (month === 12) { + month = 0; + } else { + month += 1; + } + } else { + if (month === 0) { + month = 12; + } else { + month -= 1; + } + } + + updateParams({ + year: year.toString(), + month: month.toString(), + }); + }; + const navigateYear = (direction: "prev" | "next") => { + if (direction === "next") { + year += 1; + } else { + year -= 1; + } + + updateParams({ + year: year.toString(), + month: month.toString(), + }); + }; + + const pgTitle = `${stats.title} - Koito`; + return ( -
- {stats.title} - Koito - - -
-
- - setShowTime(!showTime)} - > +
+
+ {pgTitle} + + +
+ {stats !== undefined && ( + + )} +
+
+ +

+ {months[month]} +

+ +
+
+ +

{year}

+ +
+
+
+ + setShowTime(!showTime)} + > +
- {stats !== undefined && }
-
+
); } diff --git a/client/app/utils/utils.ts b/client/app/utils/utils.ts index 50c0c16..4acbad5 100644 --- a/client/app/utils/utils.ts +++ b/client/app/utils/utils.ts @@ -16,12 +16,15 @@ const timeframeToInterval = (timeframe: Timeframe): string => { }; const getRewindYear = (): number => { + return new Date().getFullYear() - 1; +}; + +const getRewindParams = (): { month: number; year: number } => { const today = new Date(); - if (today.getMonth() > 10 && today.getDate() >= 30) { - // if we are in december 30/31, just serve current year - return today.getFullYear(); + if (today.getMonth() == 0) { + return { month: 0, year: today.getFullYear() - 1 }; } else { - return today.getFullYear() - 1; + return { month: today.getMonth(), year: today.getFullYear() }; } }; @@ -114,5 +117,5 @@ const timeListenedString = (seconds: number) => { return `${minutes} minutes listened`; }; -export { hexToHSL, timeListenedString, getRewindYear }; +export { hexToHSL, timeListenedString, getRewindYear, getRewindParams }; export type { hsl };