mirror of
https://github.com/apricote/Listory.git
synced 2026-01-13 13:11:02 +00:00
feat(frontend): add visual indicator to top lists
This commit is contained in:
parent
3228b22741
commit
8377b2f6d0
6 changed files with 79 additions and 13 deletions
|
|
@ -5,6 +5,7 @@ import { TimeOptions } from "../api/entities/time-options";
|
|||
import { TimePreset } from "../api/entities/time-preset.enum";
|
||||
import { useTopAlbums } from "../hooks/use-api";
|
||||
import { useAuth } from "../hooks/use-auth";
|
||||
import { getMaxCount } from "../util/getMaxCount";
|
||||
import { ReportTimeOptions } from "./ReportTimeOptions";
|
||||
import { TopListItem } from "./TopListItem";
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ export const ReportTopAlbums: React.FC = () => {
|
|||
const { topAlbums, isLoading } = useTopAlbums(options);
|
||||
|
||||
const reportHasItems = !isLoading && topAlbums.length !== 0;
|
||||
const maxCount = getMaxCount(topAlbums);
|
||||
|
||||
if (!user) {
|
||||
return <Redirect to="/" />;
|
||||
|
|
@ -55,7 +57,12 @@ export const ReportTopAlbums: React.FC = () => {
|
|||
)}
|
||||
{reportHasItems &&
|
||||
topAlbums.map(({ album, count }) => (
|
||||
<ReportItem album={album} count={count} />
|
||||
<ReportItem
|
||||
key={album.id}
|
||||
album={album}
|
||||
count={count}
|
||||
maxCount={maxCount}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -66,7 +73,8 @@ export const ReportTopAlbums: React.FC = () => {
|
|||
const ReportItem: React.FC<{
|
||||
album: Album;
|
||||
count: number;
|
||||
}> = ({ album, count }) => {
|
||||
maxCount: number;
|
||||
}> = ({ album, count, maxCount }) => {
|
||||
const artists = album.artists?.map((artist) => artist.name).join(", ") || "";
|
||||
|
||||
return (
|
||||
|
|
@ -75,6 +83,7 @@ const ReportItem: React.FC<{
|
|||
title={album.name}
|
||||
subTitle={artists}
|
||||
count={count}
|
||||
maxCount={maxCount}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { TimeOptions } from "../api/entities/time-options";
|
|||
import { TimePreset } from "../api/entities/time-preset.enum";
|
||||
import { useTopArtists } from "../hooks/use-api";
|
||||
import { useAuth } from "../hooks/use-auth";
|
||||
import { getMaxCount } from "../util/getMaxCount";
|
||||
import { ReportTimeOptions } from "./ReportTimeOptions";
|
||||
import { TopListItem } from "./TopListItem";
|
||||
|
||||
|
|
@ -26,6 +27,7 @@ export const ReportTopArtists: React.FC = () => {
|
|||
const { topArtists, isLoading } = useTopArtists(options);
|
||||
|
||||
const reportHasItems = !isLoading && topArtists.length !== 0;
|
||||
const maxCount = getMaxCount(topArtists);
|
||||
|
||||
if (!user) {
|
||||
return <Redirect to="/" />;
|
||||
|
|
@ -54,7 +56,12 @@ export const ReportTopArtists: React.FC = () => {
|
|||
)}
|
||||
{reportHasItems &&
|
||||
topArtists.map(({ artist, count }) => (
|
||||
<TopListItem key={artist.id} title={artist.name} count={count} />
|
||||
<TopListItem
|
||||
key={artist.id}
|
||||
title={artist.name}
|
||||
count={count}
|
||||
maxCount={maxCount}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { TimePreset } from "../api/entities/time-preset.enum";
|
|||
import { Track } from "../api/entities/track";
|
||||
import { useTopTracks } from "../hooks/use-api";
|
||||
import { useAuth } from "../hooks/use-auth";
|
||||
import { getMaxCount } from "../util/getMaxCount";
|
||||
import { ReportTimeOptions } from "./ReportTimeOptions";
|
||||
import { TopListItem } from "./TopListItem";
|
||||
|
||||
|
|
@ -32,6 +33,8 @@ export const ReportTopTracks: React.FC = () => {
|
|||
return <Redirect to="/" />;
|
||||
}
|
||||
|
||||
const maxCount = getMaxCount(topTracks);
|
||||
|
||||
return (
|
||||
<div className="md:flex md:justify-center p-4">
|
||||
<div className="md:flex-shrink-0 min-w-full xl:min-w-0 xl:w-2/3 max-w-screen-lg">
|
||||
|
|
@ -55,7 +58,7 @@ export const ReportTopTracks: React.FC = () => {
|
|||
)}
|
||||
{reportHasItems &&
|
||||
topTracks.map(({ track, count }) => (
|
||||
<ReportItem track={track} count={count} />
|
||||
<ReportItem track={track} count={count} maxCount={maxCount} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -66,7 +69,8 @@ export const ReportTopTracks: React.FC = () => {
|
|||
const ReportItem: React.FC<{
|
||||
track: Track;
|
||||
count: number;
|
||||
}> = ({ track, count }) => {
|
||||
maxCount: number;
|
||||
}> = ({ track, count, maxCount }) => {
|
||||
const artists = track.artists?.map((artist) => artist.name).join(", ") || "";
|
||||
|
||||
return (
|
||||
|
|
@ -75,6 +79,7 @@ const ReportItem: React.FC<{
|
|||
title={track.name}
|
||||
subTitle={artists}
|
||||
count={count}
|
||||
maxCount={maxCount}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,23 +1,55 @@
|
|||
import React from "react";
|
||||
|
||||
export const TopListItem: React.FC<{
|
||||
export interface TopListItemProps {
|
||||
key: string;
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
count: number;
|
||||
}> = ({ key, title, subTitle, count }) => {
|
||||
|
||||
/**
|
||||
* Highest Number that is displayed in the top list. Used to display a "progress bar".
|
||||
*/
|
||||
maxCount?: number;
|
||||
}
|
||||
|
||||
export const TopListItem: React.FC<TopListItemProps> = ({
|
||||
key,
|
||||
title,
|
||||
subTitle,
|
||||
count,
|
||||
maxCount,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="hover:bg-gray-100 border-b border-gray-200 flex md:justify-around text-gray-700 md:px-2 py-2"
|
||||
className="group hover:bg-gray-200 bg-gray-100 border-b border-gray-200 md:justify-around text-gray-700 md:px-2"
|
||||
>
|
||||
<div className="md:flex w-11/12">
|
||||
<div className={`${subTitle ? "md:w-1/2" : "md:w-full"} font-bold`}>
|
||||
{title}
|
||||
<div className="flex pt-2">
|
||||
<div className="md:flex w-11/12">
|
||||
<div className={`${subTitle ? "md:w-1/2" : "md:w-full"} font-bold`}>
|
||||
{title}
|
||||
</div>
|
||||
{subTitle && <div className="md:w-1/3">{subTitle}</div>}
|
||||
</div>
|
||||
{subTitle && <div className="md:w-1/3">{subTitle}</div>}
|
||||
<div className="w-1/12 self-center">{count}</div>
|
||||
</div>
|
||||
<div className="w-1/12 self-center">{count}</div>
|
||||
{maxCount && isMaxCountValid(maxCount) && (
|
||||
<div className="h-1 w-full bg-gradient-to-r from-teal-200 via-green-400 to-violet-400 flex flex-row-reverse">
|
||||
<div
|
||||
style={{ width: numberToPercent(1 - count / maxCount) }}
|
||||
className="h-full group-hover:bg-gray-200 bg-gray-100"
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const isMaxCountValid = (maxCount: number) =>
|
||||
!(Number.isNaN(maxCount) || maxCount === 0);
|
||||
|
||||
const numberToPercent = (ratio: number) =>
|
||||
ratio.toLocaleString(undefined, {
|
||||
style: "percent",
|
||||
minimumFractionDigits: 2,
|
||||
});
|
||||
|
|
|
|||
11
frontend/src/util/getMaxCount.ts
Normal file
11
frontend/src/util/getMaxCount.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
interface TopListItemEntity {
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get max count for top list. Returns at least 1 to make sure we do not run into issues
|
||||
* with empty list (would normally return -Infinity) or 0 (could cause divide by zero error).
|
||||
*/
|
||||
export function getMaxCount(items: TopListItemEntity[]): number {
|
||||
return Math.max(1, ...items.map(({ count }) => count));
|
||||
}
|
||||
|
|
@ -39,6 +39,8 @@ module.exports = {
|
|||
},
|
||||
|
||||
yellow: colors.yellow,
|
||||
teal: colors.teal,
|
||||
violet: colors.violet,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue