chuni: add recent/top ratings to board

This commit is contained in:
sk1982 2024-03-12 23:19:26 -04:00
parent 6f3cc058fd
commit 5c5459f5f0
3 changed files with 146 additions and 11 deletions

View File

@ -5,33 +5,58 @@ import { getUserRating } from '@/actions/chuni/profile';
import { useState } from 'react';
import { Button, ButtonGroup } from '@nextui-org/react';
import { useBreakpoint } from '@/helpers/use-breakpoint';
import { BigDecimal } from '@/helpers/big-decimal';
import { ChuniRating } from '@/components/chuni/rating';
export const ChuniTopRatingSidebar = ({ rating }: { rating: Awaited<ReturnType<typeof getUserRating>> }) => {
const [shownRating, setShownRating] = useState<'top' | 'recent' | null>('recent');
const breakpoint = useBreakpoint();
const recent = rating.recent.slice(0, 10);
const topAvg = rating.top.reduce((t, x) => t.add(x.rating), new BigDecimal(0))
.div(30, 2n);
const recentAvg = recent.reduce((t, x) => t.add(x.rating), new BigDecimal(0))
.div(10, 2n);
if (![undefined, 'sm'].includes(breakpoint) && shownRating === null)
setShownRating('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>
<div className="flex items-baseline">
<span>Top&nbsp;</span>
<ChuniRating className="text-xl" rating={+topAvg.mul(100)}>{ topAvg.toFixed(2) }</ChuniRating>
</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 className="flex items-baseline">
<span>Recent&nbsp;</span>
<ChuniRating className="text-xl" rating={+recentAvg.mul(100)}>{ recentAvg.toFixed(2) }</ChuniRating>
</div>
<ChuniTopRating rating={recent} />
</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">
<div className="mb-2 hidden md:flex">
<ButtonGroup size="sm" className="">
<Button color={shownRating === 'top' ? 'primary' : 'default'} onClick={() => setShownRating('top')}>Top</Button>
<Button color={shownRating === 'recent' ? 'primary' : 'default'} onClick={() => setShownRating('recent')}>Recent</Button>
</ButtonGroup>
<ChuniRating className="ml-auto text-xl" rating={+(shownRating === 'top' ? topAvg : recentAvg).mul(100)}>
{(shownRating === 'top' ? topAvg : recentAvg).toFixed(2)}
</ChuniRating>
</div>
<div className="flex items-center justify-center overflow-hidden h-32 md:hidden">
{shownRating && <div className="flex items-baseline mb-2">
Average:&nbsp;
<ChuniRating className="text-xl" rating={+(shownRating === 'top' ? topAvg : recentAvg).mul(100)}>
{(shownRating === 'top' ? topAvg : recentAvg).toFixed(2)}
</ChuniRating>
</div>}
<span className="text-lg mr-6 font-semibold pb-2 ml-auto">Ratings</span>
<ButtonGroup size="md" className="mb-2 h-full">
<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>

109
src/helpers/big-decimal.ts Normal file
View File

@ -0,0 +1,109 @@
type DecimalInput = BigDecimal | number | string;
export class BigDecimal {
private val: bigint;
private decimals: bigint;
constructor(val: DecimalInput) {
if (val instanceof BigDecimal) {
this.val = val.val;
this.decimals = val.decimals;
return;
}
if (typeof val === 'number')
val = val.toString();
const decimalIndex = val.indexOf('.');
val = val.replace('.', '');
this.val = BigInt(val);
if (decimalIndex === -1) {
this.decimals = 0n;
} else {
this.decimals = BigInt(val.length - decimalIndex);
}
}
private coerceDecimals(other: DecimalInput) {
const a = new BigDecimal(other);
const b = new BigDecimal(this);
if (a.decimals > b.decimals) {
b.val *= 10n ** (a.decimals - b.decimals);
b.decimals = a.decimals;
} else if (a.decimals < b.decimals) {
a.val *= 10n ** (b.decimals - a.decimals);
a.decimals = b.decimals;
}
return [a, b];
}
add(other: DecimalInput) {
const [a, b] = this.coerceDecimals(other);
a.val += b.val;
return a;
}
sub(other: DecimalInput) {
const [a, b] = this.coerceDecimals(other);
b.val -= a.val;
return b;
}
mul(other: DecimalInput) {
const a = new BigDecimal(other);
const b = new BigDecimal(this);
a.val *= b.val;
a.decimals += b.decimals;
return a;
}
div(other: DecimalInput, minDecimals=0n) {
const a = new BigDecimal(other);
const b = new BigDecimal(this);
if ((b.decimals - a.decimals) < minDecimals) {
const exp = minDecimals - (b.decimals - a.decimals);
b.val *= 10n ** exp;
b.decimals += exp;
}
b.val /= a.val;
b.decimals -= a.decimals;
if (b.decimals < 0) b.decimals = 0n;
return b;
}
static stringVal(val: bigint, decimals: bigint) {
const str = val.toString();
const pos = -Number(decimals);
return str.slice(0, pos) + '.' + str.slice(pos);
}
toFixed(places: number | bigint) {
places = BigInt(places);
if (places >= this.decimals)
return BigDecimal.stringVal(this.val, this.decimals) + '0'.repeat(Number(places - this.decimals));
return BigDecimal.stringVal(this.val / (10n ** (this.decimals - places)), places);
}
valueOf() {
return +this.toString();
}
toString() {
let val = this.val;
let decimals = this.decimals;
while (val && !(val % 10n)) {
val /= 10n;
--decimals;
}
return BigDecimal.stringVal(val, decimals);
}
}

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"lib": ["dom", "dom.iterable", "esnext", "es2020"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@ -12,6 +12,7 @@
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"target": "es2020",
"plugins": [
{
"name": "next"