forked from sk1982/actaeon
chuni: add recent/top ratings to board
This commit is contained in:
parent
6f3cc058fd
commit
5c5459f5f0
@ -5,33 +5,58 @@ import { getUserRating } from '@/actions/chuni/profile';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, ButtonGroup } from '@nextui-org/react';
|
import { Button, ButtonGroup } from '@nextui-org/react';
|
||||||
import { useBreakpoint } from '@/helpers/use-breakpoint';
|
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>> }) => {
|
export const ChuniTopRatingSidebar = ({ rating }: { rating: Awaited<ReturnType<typeof getUserRating>> }) => {
|
||||||
const [shownRating, setShownRating] = useState<'top' | 'recent' | null>('recent');
|
const [shownRating, setShownRating] = useState<'top' | 'recent' | null>('recent');
|
||||||
const breakpoint = useBreakpoint();
|
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)
|
if (![undefined, 'sm'].includes(breakpoint) && shownRating === null)
|
||||||
setShownRating('recent');
|
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]">
|
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="hidden 2xl:flex">
|
||||||
<div className="w-1/2 pr-1">
|
<div className="w-1/2 pr-1">
|
||||||
<div>Top</div>
|
<div className="flex items-baseline">
|
||||||
|
<span>Top </span>
|
||||||
|
<ChuniRating className="text-xl" rating={+topAvg.mul(100)}>{ topAvg.toFixed(2) }</ChuniRating>
|
||||||
|
</div>
|
||||||
<ChuniTopRating rating={rating.top} />
|
<ChuniTopRating rating={rating.top} />
|
||||||
</div>
|
</div>
|
||||||
<div className="pl-1 w-1/2 mr-2">
|
<div className="pl-1 w-1/2 mr-2">
|
||||||
<div>Recent</div>
|
<div className="flex items-baseline">
|
||||||
<ChuniTopRating rating={rating.recent.slice(0, 10)} />
|
<span>Recent </span>
|
||||||
|
<ChuniRating className="text-xl" rating={+recentAvg.mul(100)}>{ recentAvg.toFixed(2) }</ChuniRating>
|
||||||
|
</div>
|
||||||
|
<ChuniTopRating rating={recent} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-col 2xl:hidden pr-2">
|
<div className="w-full flex flex-col 2xl:hidden pr-2">
|
||||||
<ButtonGroup size="sm" className="mb-2 hidden md:block">
|
<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 === 'top' ? 'primary' : 'default'} onClick={() => setShownRating('top')}>Top</Button>
|
||||||
<Button color={shownRating === 'recent' ? 'primary' : 'default'} onClick={() => setShownRating('recent')}>Recent</Button>
|
<Button color={shownRating === 'recent' ? 'primary' : 'default'} onClick={() => setShownRating('recent')}>Recent</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<div className="flex items-center justify-center overflow-hidden">
|
<ChuniRating className="ml-auto text-xl" rating={+(shownRating === 'top' ? topAvg : recentAvg).mul(100)}>
|
||||||
<span className="text-lg mr-6 md:hidden font-semibold pb-2">Ratings</span>
|
{(shownRating === 'top' ? topAvg : recentAvg).toFixed(2)}
|
||||||
<ButtonGroup size="md" className="mb-2 md:hidden">
|
</ChuniRating>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center overflow-hidden h-32 md:hidden">
|
||||||
|
{shownRating && <div className="flex items-baseline mb-2">
|
||||||
|
Average:
|
||||||
|
<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 === 'top' ? 'primary' : 'default'} onClick={() => setShownRating('top')}>Top</Button>
|
||||||
<Button color={shownRating === 'recent' ? 'primary' : 'default'} onClick={() => setShownRating('recent')}>Recent</Button>
|
<Button color={shownRating === 'recent' ? 'primary' : 'default'} onClick={() => setShownRating('recent')}>Recent</Button>
|
||||||
<Button color={shownRating === null ? 'primary' : 'default'} onClick={() => setShownRating(null)}>Hide</Button>
|
<Button color={shownRating === null ? 'primary' : 'default'} onClick={() => setShownRating(null)}>Hide</Button>
|
||||||
|
109
src/helpers/big-decimal.ts
Normal file
109
src/helpers/big-decimal.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext", "es2020"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@ -12,6 +12,7 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
|
"target": "es2020",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
|
Loading…
Reference in New Issue
Block a user