wip: initial ui done

This commit is contained in:
Gabe Farrell 2025-12-31 17:38:00 -05:00
parent b5e8d88451
commit 442221327d
5 changed files with 133 additions and 120 deletions

View file

@ -63,7 +63,7 @@
@media (min-width: 60rem) { @media (min-width: 60rem) {
:root { :root {
--header-xl: 78px; --header-xl: 78px;
--header-lg: 44px; --header-lg: 36px;
--header-md: 22px; --header-md: 22px;
--header-sm: 16px; --header-sm: 16px;
--header-xl-weight: 600; --header-xl-weight: 600;

View file

@ -1,9 +1,10 @@
import { imageUrl, type RewindStats } from "api/api"; import { imageUrl, type RewindStats } from "api/api";
import RewindTopItem from "./RewindTopItem"; import RewindStatText from "./RewindStatText";
import { RewindTopItem } from "./RewindTopItem";
interface Props { interface Props {
stats: RewindStats; stats: RewindStats;
includeTime: boolean; includeTime?: boolean;
} }
export default function Rewind(props: Props) { export default function Rewind(props: Props) {
@ -11,103 +12,60 @@ export default function Rewind(props: Props) {
const albumimg = props.stats.top_albums[0].image; const albumimg = props.stats.top_albums[0].image;
const trackimg = props.stats.top_tracks[0].image; const trackimg = props.stats.top_tracks[0].image;
return ( return (
<div className="flex flex-col gap-10"> <div className="flex flex-col gap-7">
<h1>{props.stats.title}</h1> <h1>{props.stats.title}</h1>
<div className="flex gap-5"> <RewindTopItem
<div className="rewind-top-item-image"> title="Top Artist"
<img className="w-58 h-58" src={imageUrl(artistimg, "medium")} /> imageSrc={imageUrl(artistimg, "medium")}
</div> items={props.stats.top_artists}
<div className="flex flex-col gap-1"> getLabel={(a) => a.name}
<h4>Top Artist</h4> includeTime={props.includeTime}
<div className="flex items-center gap-2"> />
<div className="flex flex-col items-start mb-3">
<h2>{props.stats.top_artists[0].name}</h2> <RewindTopItem
<span className="text-(--color-fg-tertiary) -mt-3"> title="Top Album"
{`${props.stats.top_artists[0].listen_count} plays`} imageSrc={imageUrl(albumimg, "medium")}
{props.includeTime items={props.stats.top_albums}
? ` (${Math.floor( getLabel={(a) => a.title}
props.stats.top_artists[0].time_listened / 60 includeTime={props.includeTime}
)} minutes)` />
: ``}
</span> <RewindTopItem
</div> title="Top Track"
</div> imageSrc={imageUrl(trackimg, "medium")}
{props.stats.top_artists.slice(1).map((e, i) => ( items={props.stats.top_tracks}
<div className="" key={e.id}> getLabel={(t) => t.title}
{e.name} includeTime={props.includeTime}
<span className="text-(--color-fg-tertiary)"> />
{` - ${e.listen_count} plays`}
{props.includeTime <div className="grid grid-cols-3 gap-5">
? ` (${Math.floor(e.time_listened / 60)} minutes)` <RewindStatText
: ``} figure={`${props.stats.minutes_listened}`}
</span> text="Minutes listened"
</div> />
))} <RewindStatText figure={`${props.stats.unique_tracks}`} text="Tracks" />
</div> <RewindStatText
</div> figure={`${props.stats.new_tracks}`}
<div className="flex gap-5"> text="New tracks"
<div className="rewind-top-item-image"> />
<img className="w-58 h-58" src={imageUrl(albumimg, "medium")} /> <RewindStatText figure={`${props.stats.plays}`} text="Plays" />
</div> <RewindStatText figure={`${props.stats.unique_albums}`} text="Albums" />
<div className="flex flex-col gap-1"> <RewindStatText
<h4>Top Album</h4> figure={`${props.stats.new_albums}`}
<div className="flex items-center gap-2"> text="New albums"
<div className="flex flex-col items-start mb-3"> />
<h2>{props.stats.top_albums[0].title}</h2> <RewindStatText
<span className="text-(--color-fg-tertiary) -mt-3"> figure={`${props.stats.avg_plays_per_day.toFixed(1)}`}
{`${props.stats.top_albums[0].listen_count} plays`} text="Plays per day"
{props.includeTime />
? ` (${Math.floor( <RewindStatText
props.stats.top_albums[0].time_listened / 60 figure={`${props.stats.unique_artists}`}
)} minutes)` text="Artists"
: ``} />
</span> <RewindStatText
</div> figure={`${props.stats.new_artists}`}
</div> text="New artists"
{props.stats.top_albums.slice(1).map((e, i) => ( />
<div className="" key={e.id}>
{e.title}
<span className="text-(--color-fg-tertiary)">
{` - ${e.listen_count} plays`}
{props.includeTime
? ` (${Math.floor(e.time_listened / 60)} minutes)`
: ``}
</span>
</div>
))}
</div>
</div>
<div className="flex gap-5">
<div className="rewind-top-item-image">
<img className="w-58 h-58" src={imageUrl(trackimg, "medium")} />
</div>
<div className="flex flex-col gap-1">
<h4>Top Track</h4>
<div className="flex items-center gap-2">
<div className="flex flex-col items-start mb-3">
<h2>{props.stats.top_tracks[0].title}</h2>
<span className="text-(--color-fg-tertiary) -mt-3">
{`${props.stats.top_tracks[0].listen_count} plays`}
{props.includeTime
? ` (${Math.floor(
props.stats.top_tracks[0].time_listened / 60
)} minutes)`
: ``}
</span>
</div>
</div>
{props.stats.top_tracks.slice(1).map((e, i) => (
<div className="" key={e.id}>
{e.title}
<span className="text-(--color-fg-tertiary)">
{` - ${e.listen_count} plays`}
{props.includeTime
? ` (${Math.floor(e.time_listened / 60)} minutes)`
: ``}
</span>
</div>
))}
</div>
</div> </div>
</div> </div>
); );

View file

@ -0,0 +1,32 @@
interface Props {
figure: string;
text: string;
}
export default function RewindStatText(props: Props) {
return (
<div className="flex items-baseline gap-1.5">
<div className="w-20 text-end shrink-0">
<span
className="
relative inline-block
text-2xl font-semibold
"
>
<span
className="
absolute inset-0
-translate-x-2 translate-y-8
bg-(--color-primary)
z-0
h-1
"
aria-hidden
/>
<span className="relative z-1">{props.figure}</span>
</span>
</div>
<span className="text-sm">{props.text}</span>
</div>
);
}

View file

@ -1,30 +1,53 @@
import { imageUrl, type Artist } from "api/api"; type TopItemProps<T> = {
title: string;
imageSrc: string;
items: T[];
getLabel: (item: T) => string;
includeTime?: boolean;
};
interface args { export function RewindTopItem<
title?: string; T extends {
name?: string; id: string | number;
image: string; listen_count: number;
minutes_listened: number; time_listened: number;
time_listened: number;
artists?: Artist;
}
export default function RewindTopItem(args: args[]) {
console.log(args);
if (args === undefined || args.length < 1) {
return <></>;
} }
const img = imageUrl(args[0].image, "medium"); >({ title, imageSrc, items, getLabel, includeTime }: TopItemProps<T>) {
const [top, ...rest] = items;
if (!top) return null;
return ( return (
<div className="flex gap-2"> <div className="flex gap-5">
<div className="rewind-top-item-image"> <div className="rewind-top-item-image">
<img src={img} /> <img className="w-50 h-50" src={imageSrc} />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h3>{args[0].title || args[0].name}</h3> <h4 className="-mb-1">{title}</h4>
{args.map((e) => (
<div className="">{e.title || e.name}</div> <div className="flex items-center gap-2">
<div className="flex flex-col items-start mb-2">
<h2>{getLabel(top)}</h2>
<span className="text-(--color-fg-tertiary) -mt-3 text-sm">
{`${top.listen_count} plays`}
{includeTime
? ` (${Math.floor(top.time_listened / 60)} minutes)`
: ``}
</span>
</div>
</div>
{rest.map((e) => (
<div key={e.id} className="text-sm">
{getLabel(e)}
<span className="text-(--color-fg-tertiary)">
{` - ${e.listen_count} plays`}
{includeTime
? ` (${Math.floor(e.time_listened / 60)} minutes)`
: ``}
</span>
</div>
))} ))}
</div> </div>
</div> </div>

View file

@ -12,5 +12,5 @@ export default function RewindPage() {
useEffect(() => { useEffect(() => {
getRewindStats({ year: 2025 }).then((r) => setStats(r)); getRewindStats({ year: 2025 }).then((r) => setStats(r));
}, []); }, []);
return <>{stats !== undefined && <Rewind stats={stats} includeTime />}</>; return <>{stats !== undefined && <Rewind stats={stats} />}</>;
} }