mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-07 13:38:15 -08:00
feat: improve rewind page (#130)
* add timeframe selectors for rewind * alter rewind nav to default to monthly rewind * fix rewind default page * remove superfluous parameters
This commit is contained in:
parent
ddb0becc0f
commit
62267652ba
4 changed files with 197 additions and 38 deletions
|
|
@ -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 <p>Not enough data exists to create a Rewind for this period :(</p>;
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-7">
|
||||
<h2>{props.stats.title}</h2>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<SidebarSearch size={iconSize} />
|
||||
<SidebarItem
|
||||
space={10}
|
||||
to={`/rewind?year=${getRewindYear()}`}
|
||||
to="/rewind"
|
||||
name="Rewind"
|
||||
onClick={() => {}}
|
||||
modal={<></>}
|
||||
|
|
|
|||
|
|
@ -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<string>("(--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<string, string | null>) => {
|
||||
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 (
|
||||
<main className="w-18/20">
|
||||
<title>{stats.title} - Koito</title>
|
||||
<meta property="og:title" content={`${stats.title} - Koito`} />
|
||||
<meta name="description" content={`${stats.title} - Koito`} />
|
||||
<div className="flex flex-col items-start mt-20 gap-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<label htmlFor="show-time-checkbox">Show time listened?</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="show-time-checkbox"
|
||||
checked={showTime}
|
||||
onChange={(e) => setShowTime(!showTime)}
|
||||
></input>
|
||||
<div
|
||||
className="w-full min-h-screen"
|
||||
style={{
|
||||
background: `linear-gradient(to bottom, ${bgColor}, var(--color-bg) 500px)`,
|
||||
transition: "1000",
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col items-start md:flex-row sm:items-center gap-4">
|
||||
<title>{pgTitle}</title>
|
||||
<meta property="og:title" content={pgTitle} />
|
||||
<meta name="description" content={pgTitle} />
|
||||
<div className="flex flex-col items-start mt-20 gap-10 w-19/20 px-20">
|
||||
{stats !== undefined && (
|
||||
<Rewind stats={stats} includeTime={showTime} />
|
||||
)}
|
||||
<div className="flex flex-col items-center gap-4 py-8">
|
||||
<div className="flex items-center gap-6 justify-around">
|
||||
<button
|
||||
onClick={() => navigateMonth("prev")}
|
||||
className="p-2 disabled:text-(--color-fg-tertiary)"
|
||||
disabled={
|
||||
// Previous month is in the future OR
|
||||
new Date(year, month - 2) > new Date() ||
|
||||
// We are looking at current year and prev would take us to full year
|
||||
(new Date().getFullYear() === year && month === 1)
|
||||
}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</button>
|
||||
<p className="font-medium text-xl text-center w-30">
|
||||
{months[month]}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigateMonth("next")}
|
||||
className="p-2 disabled:text-(--color-fg-tertiary)"
|
||||
disabled={new Date(year, month) > new Date()}
|
||||
>
|
||||
<ChevronRight size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 justify-around">
|
||||
<button
|
||||
onClick={() => navigateYear("prev")}
|
||||
className="p-2 disabled:text-(--color-fg-tertiary)"
|
||||
disabled={new Date(year - 1, month) > new Date()}
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</button>
|
||||
<p className="font-medium text-xl text-center w-30">{year}</p>
|
||||
<button
|
||||
onClick={() => navigateYear("next")}
|
||||
className="p-2 disabled:text-(--color-fg-tertiary)"
|
||||
disabled={
|
||||
// Next year date is in the future OR
|
||||
new Date(year + 1, month - 1) > new Date() ||
|
||||
// Next year date is current full year OR
|
||||
(month == 0 && new Date().getFullYear() === year + 1) ||
|
||||
// Next year date is current month
|
||||
(new Date().getMonth() === month - 1 &&
|
||||
new Date().getFullYear() === year + 1)
|
||||
}
|
||||
>
|
||||
<ChevronRight size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<label htmlFor="show-time-checkbox">Show time listened?</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="show-time-checkbox"
|
||||
checked={showTime}
|
||||
onChange={(e) => setShowTime(!showTime)}
|
||||
></input>
|
||||
</div>
|
||||
</div>
|
||||
{stats !== undefined && <Rewind stats={stats} includeTime={showTime} />}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue