chuni rating

This commit is contained in:
sk1982 2024-03-12 06:54:31 -04:00
parent 08578164ec
commit 1ace096da2
4 changed files with 120 additions and 0 deletions

View File

@ -0,0 +1,34 @@
import { getPlaylog } from '@/actions/chuni/playlog';
import { ChuniNameplate } from '@/components/chuni/nameplate';
import { ChuniPlaylogCard } from '@/components/chuni/playlog-card';
import { getUserData, getUserRating } from '@/actions/chuni/profile';
import { requireUser } from '@/actions/auth';
import { notFound } from 'next/navigation';
import { ChuniTopRating } from '@/components/chuni/top-rating';
import { ChuniTopRatingSidebar } from '@/components/chuni/top-rating-sidebar';
export default async function ChuniDashboard() {
const user = await requireUser();
const [profile, rating, playlog] = await Promise.all([
getUserData(user),
getUserRating(user),
getPlaylog({ limit: 72 })
])
if (!profile) return notFound();
return (<div className="flex h-full flex-col md:flex-row">
<ChuniNameplate className="block md:hidden w-full" profile={profile} />
<div className="mr-4 w-full md:w-[16rem] 2xl:w-[32rem] flex-shrink-0">
<ChuniTopRatingSidebar rating={rating} />
</div>
<div className="flex flex-col h-full flex-grow">
<ChuniNameplate className="hidden md:block max-w-[38rem] w-full ml-auto" profile={profile} />
<div className="text-lg font-semibold px-4 pt-4 border-t border-gray-500 md:hidden">Playlog</div>
<div className="my-4 w-full flex-grow grid gap-2 grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 5xl:grid-cols-6 6xl:grid-cols-8">
{playlog.data.map((entry, i) => <ChuniPlaylogCard className="w-full h-48" playlog={entry} key={i} />)}
</div>
</div>
</div>);
}

View File

@ -0,0 +1,38 @@
'use client';
import { ChuniTopRating, ChuniTopRatingProps } from '@/components/chuni/top-rating';
import { getUserRating } from '@/actions/chuni/profile';
import { useState } from 'react';
import { Button, ButtonGroup } from '@nextui-org/react';
export const ChuniTopRatingSidebar = ({ rating }: { rating: Awaited<ReturnType<typeof getUserRating>> }) => {
const [shownRating, setShownRating] = useState<'top' | 'recent' | null>('recent');
return (<div className="w-full mt-4 md:mt-0 px-2 sm:px-0 md:fixed md:overflow-y-auto h-fixed flex md:w-[16rem] 2xl:w-[32rem]">
<div className="hidden 2xl:flex">
<div className="w-1/2 pr-1">
<div>Top</div>
<ChuniTopRating rating={rating.top} />
</div>
<div className="pl-1 w-1/2 mr-2">
<div>Recent</div>
<ChuniTopRating rating={rating.recent.slice(0, 10)} />
</div>
</div>
<div className="w-full flex flex-col 2xl:hidden pr-2">
<ButtonGroup size="sm" className="mb-2 hidden md:block">
<Button color={shownRating === 'top' ? 'primary' : 'default'} onClick={() => setShownRating('top')}>Top</Button>
<Button color={shownRating === 'recent' ? 'primary' : 'default'} onClick={() => setShownRating('recent')}>Recent</Button>
</ButtonGroup>
<div className="flex items-center justify-center overflow-hidden">
<span className="text-lg mr-6 md:hidden font-semibold pb-2">Ratings</span>
<ButtonGroup size="md" className="mb-2 md:hidden">
<Button color={shownRating === 'top' ? 'primary' : 'default'} onClick={() => setShownRating('top')}>Top</Button>
<Button color={shownRating === 'recent' ? 'primary' : 'default'} onClick={() => setShownRating('recent')}>Recent</Button>
<Button color={shownRating === null ? 'primary' : 'default'} onClick={() => setShownRating(null)}>Hide</Button>
</ButtonGroup>
</div>
{shownRating && <ChuniTopRating rating={shownRating === 'top' ? rating.top : rating.recent.slice(0, 10)} />}
</div>
</div>);
};

View File

@ -0,0 +1,40 @@
import { getUserRating } from '@/actions/chuni/profile';
import { getJacketUrl } from '@/helpers/assets';
import { ChuniRating } from '@/components/chuni/rating';
import { floorToDp } from '@/helpers/floor-dp';
import { ChuniScoreBadge, getVariantFromRank, getVariantFromScore } from '@/components/chuni/score-badge';
import { ChuniDifficultyContainer } from '@/components/chuni/difficulty-container';
import { Tooltip } from '@nextui-org/react';
import { ChuniLevelBadge } from '@/components/chuni/level-badge';
import Link from 'next/link';
export type ChuniTopRatingProps = {
className?: string,
rating: Awaited<ReturnType<typeof getUserRating>>['recent' | 'top']
};
export const ChuniTopRating = ({ rating, className }: ChuniTopRatingProps) => {
return (<div className={`flex flex-col ${className ?? ''}`}>
{rating.map((music, i) => <div key={i} className="flex py-2 h-28 border-b border-gray-500">
<ChuniDifficultyContainer difficulty={music.chartId ?? 0} className="flex-shrink-0 w-20 mr-2 self-center">
<div className="p-1">
<img className="aspect-square rounded overflow-hidden" src={getJacketUrl(`chuni/jacket/${music.jacketPath}`)}
alt={music.title ?? ''} />
</div>
<ChuniLevelBadge className="w-11 absolute -right-0.5 -bottom-0.5" music={music} />
</ChuniDifficultyContainer>
<div className="flex flex-col text-sm self-top flex-grow">
<Link href={`/chuni/music/${music.songId}`}>{i + 1}: <span className="underline hover:text-secondary transition">{music.title}</span></Link>
<div className="flex items-baseline mt-auto">
<ChuniRating rating={+music.rating * 100} className={"text-xs"}>RATING&nbsp;</ChuniRating>
<ChuniRating rating={+music.rating * 100} className="text-medium">{floorToDp(music.rating, 2)}</ChuniRating>
</div>
<div className="mt-1 flex items-center">
<ChuniScoreBadge className="h-5" variant={getVariantFromScore(+(music.scoreMax ?? 0))}>{music.scoreMax?.toLocaleString()}</ChuniScoreBadge>
{('pastIndex' in music) && <Tooltip content={`Played ${music.pastIndex + 1} songs ago`}><div className="ml-auto">-{music.pastIndex+1}</div></Tooltip>}
</div>
</div>
</div>)}
</div>)
};

8
src/helpers/floor-dp.ts Normal file
View File

@ -0,0 +1,8 @@
export const floorToDp = (num: number | string, decimals: number) => {
if (typeof num === 'string') {
return num.slice(0, num.indexOf('.') + decimals + 1);
}
const mult = (10 ** decimals);
return (Math.floor(num * mult) / mult).toFixed(decimals);
};