Admin home page #128 (#259)

* First iteration on the new PieChart component

* #128 Adds new admin home page

* Fixes code after merging conflicts

* Removes empty file

* Uses real data for admin home page, and improves style

* Reverts debugging code

* Defines missing variable

* Caches user stats data for admin home page

* Typo

* Styles improvements

* Invalidates cache on signup/signin

* Implements top 5 biggest games

* Improves styling

* Improves style

* Using generateManifest to get the proper size

* Reading data from cache

* Removes unnecessary import

* Improves caching mechanism for game sizes

* Removes lint errors

* Replaces piechart tooltip with colors in legend

* Fixes caching

* Fixes caching and slight improvement on pie chart colours

* Fixes a few bugs related to caching

* Fixes bug where app signin didn't refresh cache

* feat: style improvements

* fix: lint

---------

Co-authored-by: DecDuck <declanahofmeyr@gmail.com>
This commit is contained in:
Paco
2025-11-07 22:14:45 +00:00
committed by GitHub
parent 289034d0c8
commit dfa30c8a65
40 changed files with 1352 additions and 150 deletions

View File

@ -0,0 +1,45 @@
<template>
<h2 v-if="title" class="text-lg mb-4 w-full">{{ title }}</h2>
<div class="flex flex-col xl:flex-row gap-4">
<div class="relative flex grow max-w-[12rem]">
<svg class="aspect-square grow relative inline" viewBox="0 0 100 100">
<PieChartPieSlice
v-for="slice in slices"
:key="`${slice.percentage}-${slice.totalPercentage}`"
:slice="slice"
/>
</svg>
<div class="absolute inset-0 bg-zinc-900 rounded-full m-12" />
</div>
<ul class="flex flex-col gap-y-1 justify-center text-left">
<li
v-for="slice in slices"
:key="slice.value"
class="text-sm inline-flex items-center gap-x-1"
>
<span
class="size-3 inline-block rounded-sm"
:class="CHART_COLOURS[slice.color].bg"
/>
{{
$t("common.labelValueColon", {
label: slice.label,
value: slice.value,
})
}}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { generateSlices } from "~/components/PieChart/utils";
import type { SliceData } from "~/components/PieChart/types";
const { data, title = undefined } = defineProps<{
data: SliceData[];
title?: string | undefined;
}>();
const slices = generateSlices(data);
</script>

View File

@ -0,0 +1,35 @@
<template>
<path
v-if="slice.percentage !== 0 && slice.percentage !== 100"
:class="[CHART_COLOURS[slice.color].fill]"
:d="`
M ${slice.start}
A ${slice.radius},${slice.radius} 0 ${getFlags(slice.percentage)} ${polarToCartesian(slice.center, slice.radius, percent2Degrees(slice.totalPercentage))}
L ${slice.center}
z
`"
stroke-width="2"
/>
<circle
v-if="slice.percentage === 100"
:r="slice.radius"
:cx="slice.center.x"
:cy="slice.center.y"
:class="[CHART_COLOURS[slice.color].fill]"
stroke-width="2"
/>
</template>
<script setup lang="ts">
import type { Slice } from "~/components/PieChart/types";
import {
getFlags,
percent2Degrees,
polarToCartesian,
} from "~/components/PieChart/utils";
import { CHART_COLOURS } from "~/utils/colors";
const { slice } = defineProps<{
slice: Slice;
}>();
</script>

19
components/PieChart/types.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
import type Tuple from "~/utils/tuple";
import type { ChartColour } from "~/utils/colors";
export type Slice = {
start: Tuple;
center: Tuple;
percentage: number;
totalPercentage: number;
radius: number;
color: ChartColour;
label: string;
value: number;
};
export type SliceData = {
value: number;
color?: ChartColour;
label: string;
};

View File

@ -0,0 +1,50 @@
import Tuple from "~/utils/tuple";
import type { Slice, SliceData } from "~/components/PieChart/types";
import { sum, lastItem } from "~/utils/array";
export const START = new Tuple(50, 10);
export const CENTER = new Tuple(50, 50);
export const RADIUS = 40;
export const polarToCartesian = (
center: Tuple,
radius: number,
angleInDegrees: number,
) => {
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180;
const x = center.x + radius * Math.cos(angleInRadians);
const y = center.y + radius * Math.sin(angleInRadians);
return new Tuple(x, y);
};
export const percent2Degrees = (percentage: number) => (360 * percentage) / 100;
export function generateSlices(data: SliceData[]): Slice[] {
return data.reduce((accumulator, currentValue, index, array) => {
const percentage =
(currentValue.value * 100) / sum(array.map((slice) => slice.value));
return [
...accumulator,
{
start: accumulator.length
? polarToCartesian(
CENTER,
RADIUS,
percent2Degrees(lastItem(accumulator).totalPercentage),
)
: START,
radius: RADIUS,
percentage: percentage,
totalPercentage:
sum(accumulator.map((element) => element.percentage)) + percentage,
center: CENTER,
color: PIE_COLOURS[index % PIE_COLOURS.length],
label: currentValue.label,
value: currentValue.value,
},
];
}, [] as Slice[]);
}
export const getFlags = (percentage: number) =>
percentage > 50 ? new Tuple(1, 1) : new Tuple(0, 1);