mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-08 23:18:15 -07:00
feat: Rewind (#116)
* wip * chore: update counts to allow unix timeframe * feat: add db functions for counting new items * wip: endpoint working * wip * wip: initial ui done * add header, adjust ui * add time listened toggle * fix layout, year param * param fixes
This commit is contained in:
parent
c0a8c64243
commit
d4ac96f780
64 changed files with 2252 additions and 1055 deletions
72
client/app/components/rewind/Rewind.tsx
Normal file
72
client/app/components/rewind/Rewind.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { imageUrl, type RewindStats } from "api/api";
|
||||
import RewindStatText from "./RewindStatText";
|
||||
import { RewindTopItem } from "./RewindTopItem";
|
||||
|
||||
interface Props {
|
||||
stats: RewindStats;
|
||||
includeTime?: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
return (
|
||||
<div className="flex flex-col gap-7">
|
||||
<h2>{props.stats.title}</h2>
|
||||
<RewindTopItem
|
||||
title="Top Artist"
|
||||
imageSrc={imageUrl(artistimg, "medium")}
|
||||
items={props.stats.top_artists}
|
||||
getLabel={(a) => a.name}
|
||||
includeTime={props.includeTime}
|
||||
/>
|
||||
|
||||
<RewindTopItem
|
||||
title="Top Album"
|
||||
imageSrc={imageUrl(albumimg, "medium")}
|
||||
items={props.stats.top_albums}
|
||||
getLabel={(a) => a.title}
|
||||
includeTime={props.includeTime}
|
||||
/>
|
||||
|
||||
<RewindTopItem
|
||||
title="Top Track"
|
||||
imageSrc={imageUrl(trackimg, "medium")}
|
||||
items={props.stats.top_tracks}
|
||||
getLabel={(t) => t.title}
|
||||
includeTime={props.includeTime}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-3 gap-y-5">
|
||||
<RewindStatText
|
||||
figure={`${props.stats.minutes_listened}`}
|
||||
text="Minutes listened"
|
||||
/>
|
||||
<RewindStatText figure={`${props.stats.unique_tracks}`} text="Tracks" />
|
||||
<RewindStatText
|
||||
figure={`${props.stats.new_tracks}`}
|
||||
text="New tracks"
|
||||
/>
|
||||
<RewindStatText figure={`${props.stats.plays}`} text="Plays" />
|
||||
<RewindStatText figure={`${props.stats.unique_albums}`} text="Albums" />
|
||||
<RewindStatText
|
||||
figure={`${props.stats.new_albums}`}
|
||||
text="New albums"
|
||||
/>
|
||||
<RewindStatText
|
||||
figure={`${props.stats.avg_plays_per_day.toFixed(1)}`}
|
||||
text="Plays per day"
|
||||
/>
|
||||
<RewindStatText
|
||||
figure={`${props.stats.unique_artists}`}
|
||||
text="Artists"
|
||||
/>
|
||||
<RewindStatText
|
||||
figure={`${props.stats.new_artists}`}
|
||||
text="New artists"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
client/app/components/rewind/RewindStatText.tsx
Normal file
32
client/app/components/rewind/RewindStatText.tsx
Normal 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-23 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>
|
||||
);
|
||||
}
|
||||
55
client/app/components/rewind/RewindTopItem.tsx
Normal file
55
client/app/components/rewind/RewindTopItem.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
type TopItemProps<T> = {
|
||||
title: string;
|
||||
imageSrc: string;
|
||||
items: T[];
|
||||
getLabel: (item: T) => string;
|
||||
includeTime?: boolean;
|
||||
};
|
||||
|
||||
export function RewindTopItem<
|
||||
T extends {
|
||||
id: string | number;
|
||||
listen_count: number;
|
||||
time_listened: number;
|
||||
}
|
||||
>({ title, imageSrc, items, getLabel, includeTime }: TopItemProps<T>) {
|
||||
const [top, ...rest] = items;
|
||||
|
||||
if (!top) return null;
|
||||
|
||||
return (
|
||||
<div className="flex gap-5">
|
||||
<div className="rewind-top-item-image">
|
||||
<img className="max-w-48 max-h-48" src={imageSrc} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="-mb-1">{title}</h4>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue