chuni rating
This commit is contained in:
parent
08578164ec
commit
1ace096da2
34
src/app/(with-header)/chuni/dashboard/page.tsx
Normal file
34
src/app/(with-header)/chuni/dashboard/page.tsx
Normal 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>);
|
||||
}
|
||||
|
38
src/components/chuni/top-rating-sidebar.tsx
Normal file
38
src/components/chuni/top-rating-sidebar.tsx
Normal 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>);
|
||||
};
|
40
src/components/chuni/top-rating.tsx
Normal file
40
src/components/chuni/top-rating.tsx
Normal 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 </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
8
src/helpers/floor-dp.ts
Normal 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);
|
||||
};
|
Loading…
Reference in New Issue
Block a user