Compare commits

...

5 Commits

Author SHA1 Message Date
3ca967591e Leaderboard: Implement simple points cumsum chart
All checks were successful
Build Formula11 Docker Image / pocketbase-docker (push) Successful in 1m3s
2025-06-07 19:59:39 +02:00
6e6ce020a3 Env: Add carbon-charts dep 2025-06-07 19:59:22 +02:00
d54ee01227 Lib: Update fetchers and schema after database changes 2025-06-07 19:58:34 +02:00
3339ffaa5f Seasonpicks: Invert table background colors 2025-06-06 16:26:53 +02:00
78ee291795 Racepicks: Invert table background colors 2025-06-06 16:26:46 +02:00
9 changed files with 1205 additions and 32 deletions

1016
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@carbon/charts-svelte": "^1.22.18",
"@floating-ui/dom": "^1.6.13",
"@fsouza/prettierd": "^0.26.1",
"@skeletonlabs/skeleton": "^2.10.4",

View File

@ -9,6 +9,7 @@ import type {
RacePick,
RacePickPoints,
RacePickPointsAcc,
RacePickPointsTotal,
RaceResult,
ScrapedDriverStanding,
ScrapedRaceResult,
@ -281,7 +282,7 @@ export const fetch_racepickpoints = async (
};
/**
* Fetch all [RacePickPointsAcc] from the database, ordered descendingly by total points.
* Fetch all [RacePickPointsAcc] from the database, ordered ascendingly by step.
*/
export const fetch_racepickpointsacc = async (
fetch: (_: any) => Promise<Response>,
@ -293,6 +294,19 @@ export const fetch_racepickpointsacc = async (
return racepickpointsacc;
};
/**
* Fetch all [RacePickPointsTotal] from the database, ordered descendingly by total points.
*/
export const fetch_racepickpointstotal = async (
fetch: (_: any) => Promise<Response>,
): Promise<RacePickPointsTotal[]> => {
const racepickpointstotal: RacePickPointsTotal[] = await pb
.collection("racepickpointstotal")
.getFullList({ fetch: fetch });
return racepickpointstotal;
};
/**
* Fetch all [ScrapedDriverStandings] from the database, ordered ascendingly by position.
*/

View File

@ -140,11 +140,21 @@ export interface RacePickPoints {
}
export interface RacePickPointsAcc {
id: string;
user: string;
step: number;
acc_pxx_points: number;
acc_dnf_points: number;
acc_points: number;
}
export interface RacePickPointsTotal {
id: string;
user: string;
total_pxx_points: number;
total_dnf_points: number;
total_points: number;
total_points_per_pick: number;
}
// Scraped Data

View File

@ -1,10 +1,28 @@
<script lang="ts">
import { Table, type TableColumn } from "$lib/components";
import { get_by_value } from "$lib/database";
import type { RacePickPoints, RacePickPointsAcc, User } from "$lib/schema";
import type { PageData } from "./$types";
import {
LineChart,
ScaleTypes,
type ChartTabularData,
type LineChartOptions,
} from "@carbon/charts-svelte";
import "@carbon/charts-svelte/styles.css";
let { data }: { data: PageData } = $props();
// Await promises
let users: User[] | undefined = $state(undefined);
data.users.then((u: User[]) => (users = u));
let racepickpoints: RacePickPoints[] | undefined = $state(undefined);
data.racepickpoints.then((r: RacePickPoints[]) => (racepickpoints = r));
let racepickpointsacc: RacePickPointsAcc[] | undefined = $state(undefined);
data.racepickpointsacc.then((r: RacePickPointsAcc[]) => (racepickpointsacc = r));
const leaderboard_columns: TableColumn[] = $derived([
{
data_value_name: "user",
@ -14,15 +32,124 @@
},
{
data_value_name: "total_points",
label: "Points",
label: "Total",
},
{
data_value_name: "total_pxx_points",
label: "PXX",
},
{
data_value_name: "total_dnf_points",
label: "DNF",
},
{
data_value_name: "total_points_per_pick",
label: "Per Pick",
valuefun: async (value: string): Promise<string> => Number.parseFloat(value).toFixed(2),
},
]);
const points_chart_data: ChartTabularData = $derived.by(() => {
if (!users || !racepickpointsacc) return [];
return users
.map((user: User) => {
return {
group: user.firstname,
step: "0",
points: 0,
};
})
.concat(
racepickpointsacc.map((points: RacePickPointsAcc) => {
return {
group: get_by_value(users ?? [], "id", points.user)?.firstname || "INVALID",
step: points.step.toString(),
points: points.acc_points,
};
}),
);
});
const points_chart_options: LineChartOptions = {
title: "I ❤️ CumSum",
axes: {
bottom: {
mapsTo: "step",
scaleType: ScaleTypes.LABELS,
},
left: {
mapsTo: "points",
scaleType: ScaleTypes.LINEAR,
},
},
curve: "curveMonotoneX",
// toolbar: {
// enabled: false,
// },
animations: true,
// canvasZoom: {
// enabled: false,
// },
grid: {
x: {
enabled: true,
alignWithAxisTicks: true,
},
y: {
enabled: true,
alignWithAxisTicks: true,
},
},
legend: {
enabled: true,
clickable: true,
position: "top",
},
points: {
enabled: true,
radius: 5,
},
tooltip: {
showTotal: false,
},
resizable: true,
width: "100%",
height: "400px",
};
</script>
<svelte:head>
<title>Formula 11 - Leaderboard</title>
</svelte:head>
{#await Promise.all( [data.users, data.racepickpoints, data.racepickpointsacc], ) then [users, racepickpoints, racepickpointsacc]}
<Table data={racepickpointsacc} columns={leaderboard_columns} />
{#await Promise.all( [data.users, data.racepickpoints, data.racepickpointsacc, data.racepickpointstotal], ) then [users, racepickpoints, racepickpointsacc, racepickpointstotal]}
<div class="flex gap-2">
<!-- Podium -->
<!-- <div class="card w-60 bg-surface-100 p-2 shadow"> -->
<!-- <div class="flex h-20 w-full gap-1"></div> -->
<!-- <div class="flex h-20 w-full gap-1"> -->
<!-- <div class="w-20"> -->
<!-- <div class="h-[30%] w-full"></div> -->
<!-- <div class="h-[70%] w-full bg-surface-500"></div> -->
<!-- </div> -->
<!-- <div class="w-20"> -->
<!-- <div class="h-[100%] w-full bg-surface-500"></div> -->
<!-- </div> -->
<!-- <div class="w-20"> -->
<!-- <div class="h-[60%] w-full"></div> -->
<!-- <div class="h-[40%] w-full bg-surface-600"></div> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->
<!-- Points chart -->
<div class="card w-full bg-surface-100 p-2 shadow">
<LineChart data={points_chart_data} options={points_chart_options} />
</div>
</div>
<div class="mt-2">
<Table data={racepickpointstotal} columns={leaderboard_columns} />
</div>
{/await}

View File

@ -1,4 +1,9 @@
import { fetch_users, fetch_racepickpoints, fetch_racepickpointsacc } from "$lib/fetch";
import {
fetch_users,
fetch_racepickpoints,
fetch_racepickpointsacc,
fetch_racepickpointstotal,
} from "$lib/fetch";
import type { PageLoad } from "../$types";
export const load: PageLoad = async ({ fetch, depends }) => {
@ -8,5 +13,6 @@ export const load: PageLoad = async ({ fetch, depends }) => {
users: fetch_users(fetch),
racepickpoints: fetch_racepickpoints(fetch),
racepickpointsacc: fetch_racepickpointsacc(fetch),
racepickpointstotal: fetch_racepickpointstotal(fetch),
};
};

View File

@ -256,8 +256,8 @@
{#await data.currentpickedusers then currentpicked}
{#each currentpicked as user}
<div
class="card ml-1 mt-2 w-full min-w-14 rounded-b-none py-2
{$pbUser && $pbUser.username === user.username ? 'bg-primary-300' : ''}"
class="card ml-1 mt-2 w-full min-w-14 rounded-b-none bg-surface-400 py-2
{$pbUser && $pbUser.username === user.username ? '!bg-primary-400' : ''}"
>
<!-- Avatar + name display at the top -->
<div class="m-auto flex h-10 w-fit">
@ -293,7 +293,7 @@
<div
use:popup={race_popupsettings(race?.id ?? "Invalid")}
class="card mt-1 flex h-20 w-7 cursor-pointer flex-col !rounded-r-none lg:w-36 lg:p-2"
class="card mt-1 flex h-16 w-7 cursor-pointer flex-col !rounded-r-none bg-surface-400 lg:h-20 lg:w-36 lg:p-2"
>
<!-- For large screens -->
<span class="hidden text-sm font-bold lg:block">
@ -306,13 +306,13 @@
<!-- For small screens -->
<!-- TODO: This requires the race name to end with an emoji, but this is never enforced -->
<div class="mx-[2px] my-[25px] block text-lg font-bold lg:hidden">
<div class="mx-[2px] my-[18px] block text-lg font-bold lg:hidden">
{runes(race?.name ?? "Invalid").at(-1)}
</div>
</div>
<!-- The race result popup is triggered on click on the race -->
<div data-popup={race?.id ?? "Invalid"} class="card z-50 p-2 shadow">
<div data-popup={race?.id ?? "Invalid"} class="card z-50 bg-surface-400 p-2 shadow">
<span class="font-bold">Result:</span>
<div class="mt-2 flex flex-col gap-1">
{#each result.pxxs as pxx, index}
@ -360,7 +360,7 @@
result.dnfs.indexOf(pick?.dnf ?? "Invalid") >= 0 ? PXX_COLORS[3] : PXX_COLORS[-1]}
{#if pick}
<div class="mt-1 h-20 w-full border bg-surface-300 px-1 py-2 lg:px-2">
<div class="mt-1 h-16 w-full border bg-surface-200 px-1 py-2 lg:h-20 lg:px-2">
<div class="mx-auto flex h-full w-fit flex-col justify-evenly">
<span
class="w-10 p-1 text-center text-xs rounded-container-token lg:text-sm"
@ -377,7 +377,7 @@
</div>
</div>
{:else}
<div class="mt-1 h-20 w-full px-1 py-2 lg:px-2"></div>
<div class="mt-1 h-16 w-full px-1 py-2 lg:h-20 lg:px-2"></div>
{/if}
{/each}
</div>

View File

@ -264,8 +264,8 @@
{#await data.seasonpickedusers then seasonpicked}
{#each seasonpicked as user}
<div
class="card ml-1 mt-2 w-full min-w-36 rounded-b-none py-2
{$pbUser && $pbUser.username === user.username ? 'bg-primary-300' : ''}"
class="card ml-1 mt-2 w-full min-w-36 rounded-b-none bg-surface-400 py-2
{$pbUser && $pbUser.username === user.username ? '!bg-primary-400' : ''}"
>
<!-- Avatar + name display at the top -->
<div class="m-auto flex h-10 w-fit">
@ -296,7 +296,7 @@
class="sticky left-0 z-10 w-7 min-w-7 max-w-7 bg-surface-50 lg:w-36 lg:min-w-36 lg:max-w-36"
>
<!-- Hottake -->
<div class="card mt-1 flex h-32 w-7 flex-col !rounded-r-none p-2 lg:w-36">
<div class="card mt-1 flex h-32 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">Hottake</span>
<span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">Hottake</span>
</div>
@ -304,13 +304,13 @@
{#await data.seasonpicks then seasonpicks}
{#if seasonpicks.length > 0}
<!-- Drivers Champion -->
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none p-2 lg:w-36">
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">Drivers<br />Champion</span>
<span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">WDC</span>
</div>
<!-- Constructors Champion -->
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none p-2 lg:w-36">
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">
Constructors<br />Champion
</span>
@ -318,25 +318,25 @@
</div>
<!-- Overtakes -->
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none p-2 lg:w-36">
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">Most Overtakes</span>
<span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">Overtakes</span>
</div>
<!-- DNFs -->
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none p-2 lg:w-36">
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">Most DNFs</span>
<span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">DNFs</span>
</div>
<!-- Doohan Starts -->
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none p-2 lg:w-36">
<div class="card mt-1 flex h-20 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">Doohan Starts</span>
<span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">Doohan</span>
</div>
<!-- Teamwinners -->
<div class="card mt-1 flex h-64 w-7 flex-col !rounded-r-none p-2 lg:w-36">
<div class="card mt-1 flex h-64 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block"
>Team-Battle<br />Winners</span
>
@ -344,7 +344,7 @@
</div>
<!-- Podiums -->
<div class="card mt-1 flex h-64 w-7 flex-col !rounded-r-none p-2 lg:w-36">
<div class="card mt-1 flex h-64 w-7 flex-col !rounded-r-none bg-surface-400 p-2 lg:w-36">
<span class="hidden text-nowrap text-sm font-bold lg:block">Podiums</span>
<span class="block rotate-90 text-nowrap text-xs font-bold lg:hidden">Podiums</span>
</div>
@ -374,7 +374,7 @@
<div class="ml-1 w-full min-w-36">
<!-- Hottake -->
<div
class="mt-1 h-32 w-full overflow-y-scroll border bg-surface-300 px-1 py-2 leading-3 lg:px-2"
class="mt-1 h-32 w-full overflow-y-scroll border bg-surface-200 px-1 py-2 leading-3 lg:px-2"
>
<div class="mx-auto w-fit text-xs font-bold lg:text-sm">
{hottake?.hottake ?? "?"}
@ -383,7 +383,7 @@
{#if seasonpicks.length > 0}
<!-- Drivers Champion -->
<div class="mt-1 h-20 w-full border bg-surface-300 px-1 py-2 leading-3 lg:px-2">
<div class="mt-1 h-20 w-full border bg-surface-200 px-1 py-2 leading-3 lg:px-2">
<div class="mx-auto w-fit">
<!-- NOTE: The containerstyle should be 64x64, don't know why that doesn't fit... (also below) -->
<LazyImage
@ -398,7 +398,7 @@
</div>
<!-- Constructors Champion -->
<div class="mt-1 h-20 w-full border bg-surface-300 p-1 px-1 py-2 leading-3 lg:px-2">
<div class="mt-1 h-20 w-full border bg-surface-200 p-1 px-1 py-2 leading-3 lg:px-2">
<div class="mx-auto w-fit">
<LazyImage
src={wccwinner?.banner_url ?? get_team_banner_template(data.graphics)}
@ -412,7 +412,7 @@
</div>
<!-- Most Overtakes -->
<div class="mt-1 h-20 w-full border bg-surface-300 px-1 py-2 leading-3 lg:px-2">
<div class="mt-1 h-20 w-full border bg-surface-200 px-1 py-2 leading-3 lg:px-2">
<div class="mx-auto w-fit">
<LazyImage
src={mostovertakes?.headshot_url ?? get_driver_headshot_template(data.graphics)}
@ -428,7 +428,7 @@
</div>
<!-- Most DNFs -->
<div class="mt-1 h-20 w-full border bg-surface-300 px-1 py-2 leading-3 lg:px-2">
<div class="mt-1 h-20 w-full border bg-surface-200 px-1 py-2 leading-3 lg:px-2">
<div class="mx-auto w-fit">
<LazyImage
src={mostdnfs?.headshot_url ?? get_driver_headshot_template(data.graphics)}
@ -442,7 +442,7 @@
</div>
<!-- Doohan Starts -->
<div class="mt-1 h-20 w-full border bg-surface-300 p-1 px-1 py-2 leading-3 lg:px-2">
<div class="mt-1 h-20 w-full border bg-surface-200 p-1 px-1 py-2 leading-3 lg:px-2">
<div class="mx-auto w-fit text-xs lg:text-sm">
Jack Doohan startet <span class="font-bold">{pick?.doohanstarts ?? "?"}</span> mal.
</div>
@ -450,7 +450,7 @@
<!-- Teamwinners -->
<div
class="mt-1 h-64 w-full overflow-y-scroll border bg-surface-300 p-1 px-1 py-2 leading-3 lg:px-2"
class="mt-1 h-64 w-full overflow-y-scroll border bg-surface-200 p-1 px-1 py-2 leading-3 lg:px-2"
>
{#if pick && pick.teamwinners}
<div class="grid grid-cols-2 gap-1">
@ -479,7 +479,7 @@
<!-- Podiums -->
<!-- TODO: Replace all style tags throughout the page with custom classes like height here -->
<div
class="mt-1 h-64 w-full overflow-y-scroll border bg-surface-300 p-1 px-1 py-2 leading-3 shadow lg:px-2"
class="mt-1 h-64 w-full overflow-y-scroll border bg-surface-200 p-1 px-1 py-2 leading-3 shadow lg:px-2"
>
{#if pick && pick.podiums}
<div class="grid grid-cols-2 gap-1">

View File

@ -3,6 +3,9 @@ import { defineConfig } from "vite";
export default defineConfig({
plugins: [sveltekit()],
ssr: {
noExternal: process.env.NODE_ENV === "production" ? ["@carbon/charts"] : [],
},
build: {
rollupOptions: {
external: ["sharp"],