Compare commits

...

12 Commits

Author SHA1 Message Date
ba307eb4c4 Leaderboard: Fix leaderboard table sort and account for no seasonpickresult
All checks were successful
Build Formula11 Docker Image / pocketbase-docker (push) Successful in 38s
2025-12-26 21:53:58 +01:00
7cb0931329 Leaderboard: Include seasonpickpoints in leaderboard data
All checks were successful
Build Formula11 Docker Image / pocketbase-docker (push) Successful in 37s
2025-12-26 21:38:32 +01:00
db0365adad Lib: Add schema + fetcher for seasonpickpoints table 2025-12-26 21:38:16 +01:00
5cf7974a79 Seasonpicks: Mark correct and incorrect picks (ugly af/quite appalling)
All checks were successful
Build Formula11 Docker Image / pocketbase-docker (push) Successful in 37s
2025-12-26 21:16:21 +01:00
9d83673a90 Pocketbase: Update schema (add seasonpicks points calc) 2025-12-26 20:25:49 +01:00
9cdbe45ace Data/Seasonpickresults: Increase card size for mobile
All checks were successful
Build Formula11 Docker Image / pocketbase-docker (push) Successful in 38s
2025-12-26 19:10:45 +01:00
9755d06220 Data/Seasonpickresults: Add page to enter seasonpickresults
All checks were successful
Build Formula11 Docker Image / pocketbase-docker (push) Successful in 1m19s
2025-12-26 18:52:04 +01:00
aa14ca6782 Lib: Add options to Card component to further customize the header image appearance 2025-12-26 18:51:48 +01:00
a46a176d59 Lib: Allow card component to have left aligned header image 2025-12-26 17:05:34 +01:00
94e728bf39 Lib: Add seasonpickresult table schema + fetcher 2025-12-25 20:28:07 +01:00
24a713b471 Env: Update check script 2025-12-25 20:27:52 +01:00
9bdf6ea8ef Data: Disable "Create" buttons for non-admins 2025-12-25 20:01:35 +01:00
19 changed files with 1201 additions and 35 deletions

View File

@ -307,7 +307,8 @@ rec {
abbr -a pb "pocketbase serve --http 192.168.86.50:8090 --dev" abbr -a pb "pocketbase serve --http 192.168.86.50:8090 --dev"
abbr -a dev "npm run dev -- --host --port 5173" abbr -a dev "npm run dev -- --host --port 5173"
abbr -a prod "npm run build && npm run preview -- --host --port 5173" abbr -a prod "npm run build && npm run preview -- --host --port 5173"
abbr -a check "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" # abbr -a check "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
abbr -a check "npm run check:watch"
''; '';
in in
builtins.concatStringsSep "\n" [ builtins.concatStringsSep "\n" [

8
package-lock.json generated
View File

@ -31,7 +31,7 @@
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"runes2": "^1.1.4", "runes2": "^1.1.4",
"svelte": "^5.23.0", "svelte": "^5.23.0",
"svelte-check": "^4.1.5", "svelte-check": "^4.3.5",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"uuid": "^11.1.0", "uuid": "^11.1.0",
@ -4853,9 +4853,9 @@
} }
}, },
"node_modules/svelte-check": { "node_modules/svelte-check": {
"version": "4.1.5", "version": "4.3.5",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.5.tgz", "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.5.tgz",
"integrity": "sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg==", "integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -30,7 +30,7 @@
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"runes2": "^1.1.4", "runes2": "^1.1.4",
"svelte": "^5.23.0", "svelte": "^5.23.0",
"svelte-check": "^4.1.5", "svelte-check": "^4.3.5",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.8.2", "typescript": "^5.8.2",
"uuid": "^11.1.0", "uuid": "^11.1.0",

View File

@ -1570,6 +1570,157 @@
"indexes": [], "indexes": [],
"system": false "system": false
}, },
{
"id": "pbc_2622411661",
"listRule": "",
"viewRule": "",
"createRule": "@request.auth.id != \"\" &&\n@request.auth.admin = true",
"updateRule": "@request.auth.id != \"\" &&\n@request.auth.admin = true",
"deleteRule": null,
"name": "seasonpickresults",
"type": "base",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": false,
"collectionId": "pbc_1736455494",
"hidden": false,
"id": "relation1202818397",
"maxSelect": 999,
"minSelect": 0,
"name": "correcthottake",
"presentable": false,
"required": false,
"system": false,
"type": "relation"
},
{
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "relation553681702",
"maxSelect": 1,
"minSelect": 0,
"name": "wdcwinner",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"cascadeDelete": false,
"collectionId": "pbc_1568971955",
"hidden": false,
"id": "relation734366271",
"maxSelect": 1,
"minSelect": 0,
"name": "wccwinner",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "relation3896658669",
"maxSelect": 999,
"minSelect": 0,
"name": "mostovertakes",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "relation3731883446",
"maxSelect": 999,
"minSelect": 0,
"name": "mostdnfs",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"hidden": false,
"id": "number1147572598",
"max": 24,
"min": 0,
"name": "doohanstarts",
"onlyInt": false,
"presentable": false,
"required": true,
"system": false,
"type": "number"
},
{
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "relation1938994230",
"maxSelect": 10,
"minSelect": 10,
"name": "teamwinners",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"cascadeDelete": false,
"collectionId": "pbc_1967373549",
"hidden": false,
"id": "relation3915334665",
"maxSelect": 999,
"minSelect": 3,
"name": "podiums",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"indexes": [],
"system": false
},
{ {
"id": "pbc_1473742649", "id": "pbc_1473742649",
"listRule": "@request.auth.id != \"\" // If you know what you're doing you can easily request all picks here. But If I restrict this to the current user, the subscription events are blocked...", "listRule": "@request.auth.id != \"\" // If you know what you're doing you can easily request all picks here. But If I restrict this to the current user, the subscription events are blocked...",
@ -2677,6 +2828,128 @@
"system": false, "system": false,
"viewQuery": "-- This query returns users with an extra field \"picked\", depending on if they made their season picks yet\nSELECT\n user.id, user.username, user.firstname, user.avatar, user.admin,\n -- Generate the additional field \"picked\" that contains the season pick ID if the user occurs in the seasonpicks and false otherwise\n (CASE\n WHEN sp.user IS NOT NULL THEN sp.id\n ELSE NULL\n END) AS picked\nFROM\n users user\n-- Join users and seasonpicks (user ids that are missing from this join will receive picked=false)\nLEFT JOIN\n seasonpicks sp ON user.id = sp.user\nORDER BY user.id ASC;" "viewQuery": "-- This query returns users with an extra field \"picked\", depending on if they made their season picks yet\nSELECT\n user.id, user.username, user.firstname, user.avatar, user.admin,\n -- Generate the additional field \"picked\" that contains the season pick ID if the user occurs in the seasonpicks and false otherwise\n (CASE\n WHEN sp.user IS NOT NULL THEN sp.id\n ELSE NULL\n END) AS picked\nFROM\n users user\n-- Join users and seasonpicks (user ids that are missing from this join will receive picked=false)\nLEFT JOIN\n seasonpicks sp ON user.id = sp.user\nORDER BY user.id ASC;"
}, },
{
"id": "pbc_945270645",
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"name": "seasonpickpoints",
"type": "view",
"fields": [
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3208210256",
"max": 0,
"min": 0,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"cascadeDelete": false,
"collectionId": "pbc_1736455494",
"hidden": false,
"id": "_clone_4tZb",
"maxSelect": 1,
"minSelect": 0,
"name": "user",
"presentable": false,
"required": true,
"system": false,
"type": "relation"
},
{
"hidden": false,
"id": "json368448629",
"maxSize": 1,
"name": "hottake_points",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json3943774131",
"maxSize": 1,
"name": "wdc_points",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json2406505082",
"maxSize": 1,
"name": "wcc_points",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json1467527923",
"maxSize": 1,
"name": "doohan_points",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json1170173829",
"maxSize": 1,
"name": "overtakes_points",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json182958108",
"maxSize": 1,
"name": "dnfs_points",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json1497196675",
"maxSize": 1,
"name": "teamwinner_points",
"presentable": false,
"required": false,
"system": false,
"type": "json"
},
{
"hidden": false,
"id": "json2424735323",
"maxSize": 1,
"name": "podium_points",
"presentable": false,
"required": false,
"system": false,
"type": "json"
}
],
"indexes": [],
"system": false,
"viewQuery": "SELECT\n sp.id,\n sp.user,\n \n (CASE\n -- Correct hottake\n WHEN sr.correcthottake LIKE '[%\"' || sp.user || '\"%]' THEN 10\n ELSE 0 \n END) AS hottake_points,\n\n (CASE\n -- Correct WDC\n WHEN sp.wdcwinner = sr.wdcwinner THEN 10\n ELSE 0\n END) AS wdc_points,\n\n (CASE\n -- Correct WCC\n WHEN sp.wccwinner = sr.wccwinner THEN 10\n ELSE 0\n END) AS wcc_points,\n\n (CASE\n WHEN ABS(sp.doohanstarts - sr.doohanstarts)\n = MIN(ABS(sp.doohanstarts - sr.doohanstarts)) OVER ()\n THEN 5\n ELSE 0\n END) AS doohan_points,\n\n (CASE\n -- Most Overtakes\n WHEN sr.mostovertakes LIKE '[%\"' || sp.mostovertakes || '\"%]' THEN 10\n ELSE 0\n END) AS overtakes_points,\n\n (CASE\n -- Most DNFs\n WHEN sr.mostdnfs LIKE '[%\"' || sp.mostdnfs || '\"%]' THEN 10\n ELSE 0\n END) AS dnfs_points,\n\n (SELECT SUM(CASE\n -- Teamwinners\n WHEN EXISTS (SELECT 1\n FROM json_each(sr.teamwinners) srtw\n WHERE srtw.value = sptw.value\n ) THEN 3\n ELSE -3\n END)\n FROM json_each(sp.teamwinners) sptw) AS teamwinner_points,\n\n (SELECT SUM(CASE\n -- Podiums\n WHEN EXISTS (SELECT 1\n FROM json_each(sr.podiums) srp\n WHERE srp.value = spp.value\n ) THEN 3\n ELSE -2\n END)\n FROM json_each(sp.podiums) spp) AS podium_points\n\nFROM seasonpicks sp\n-- CROSS JOIN: Cartesian Product\nCROSS JOIN seasonpickresults sr;"
},
{ {
"id": "pbc_575507001", "id": "pbc_575507001",
"listRule": "", "listRule": "",

View File

@ -2,8 +2,9 @@
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import { LazyImage } from "$lib/components"; import { LazyImage } from "$lib/components";
import { error } from "@sveltejs/kit"; import { error } from "@sveltejs/kit";
import type { HTMLAttributes } from "svelte/elements";
interface CardProps { interface CardProps extends HTMLAttributes<HTMLDivElement> {
children: Snippet; children: Snippet;
/** The URL for a possible header image. Leave undefined for no header image. Set to empty string for an image not yet loaded. */ /** The URL for a possible header image. Leave undefined for no header image. Set to empty string for an image not yet loaded. */
@ -21,6 +22,18 @@
/** Hide the header image element. It can be shown by removing the "hidden" property using JS and the imgid. */ /** Hide the header image element. It can be shown by removing the "hidden" property using JS and the imgid. */
imghidden?: boolean; imghidden?: boolean;
/** If the header image is positioned at the left of the card */
imgleft?: boolean;
/** If the header image has a shadow */
imgshadow?: boolean;
/** Extra classes to pass to the card header image */
extraimgclass?: string;
/** Extra classes to pass to the card content div */
extraclass?: string;
/** The width class for the card, defaults to [w-auto] */ /** The width class for the card, defaults to [w-auto] */
width?: string; width?: string;
@ -35,6 +48,10 @@
imgheight = undefined, imgheight = undefined,
imgid = undefined, imgid = undefined,
imghidden = false, imghidden = false,
imgleft = false,
imgshadow = true,
extraimgclass = "",
extraclass = "",
width = "w-auto", width = "w-auto",
imgonclick = undefined, imgonclick = undefined,
...restProps ...restProps
@ -45,7 +62,7 @@
} }
</script> </script>
<div class="card {width} overflow-hidden bg-white shadow"> <div class="card {width} overflow-hidden bg-white shadow {imgleft ? 'flex' : ''}">
<!-- Allow empty strings for images that only appear after user action --> <!-- Allow empty strings for images that only appear after user action -->
{#if imgsrc !== undefined} {#if imgsrc !== undefined}
<LazyImage <LazyImage
@ -53,7 +70,7 @@
src={imgsrc} src={imgsrc}
alt="Card header" alt="Card header"
draggable="false" draggable="false"
class="select-none shadow" class="select-none {imgshadow ? 'shadow' : ''} {extraimgclass}"
hidden={imghidden} hidden={imghidden}
imgwidth={imgwidth ?? 0} imgwidth={imgwidth ?? 0}
imgheight={imgheight ?? 0} imgheight={imgheight ?? 0}
@ -61,7 +78,7 @@
/> />
{/if} {/if}
<div class="p-2" {...restProps}> <div class="p-2 {extraclass}" {...restProps}>
{@render children()} {@render children()}
</div> </div>
</div> </div>

View File

@ -18,6 +18,8 @@ import type {
ScrapedTeamStanding, ScrapedTeamStanding,
SeasonPick, SeasonPick,
SeasonPickedUser, SeasonPickedUser,
SeasonPickPoints,
SeasonPickResult,
Substitution, Substitution,
Team, Team,
User, User,
@ -115,6 +117,19 @@ export const fetch_raceresults = async (
return raceresults; return raceresults;
}; };
/**
* Fetch all [SeasonPickResults] from the database. Should either contain 0 or 1 element.
*/
export const fetch_seasonpickresults = async (
fetch: (_: any) => Promise<Response>,
): Promise<SeasonPickResult[]> => {
const seasonpickresults: SeasonPickResult[] = await pb
.collection("seasonpickresults")
.getFullList({ fetch: fetch });
return seasonpickresults;
};
/** /**
* Fetch all [Users] (sorted ascending by username) with file URLs for avatars * Fetch all [Users] (sorted ascending by username) with file URLs for avatars
*/ */
@ -309,6 +324,19 @@ export const fetch_racepickpointstotal = async (
return racepickpointstotal; return racepickpointstotal;
}; };
/**
* Fetch all [SeasonPickPoints] from the database.
*/
export const fetch_seasonpickpoints = async (
fetch: (_: any) => Promise<Response>,
): Promise<SeasonPickPoints[]> => {
const seasonpickpoints: SeasonPickPoints[] = await pb
.collection("seasonpickpoints")
.getFullList({ fetch: fetch });
return seasonpickpoints;
};
/** /**
* Fetch all [ScrapedDriverStandings] from the database, ordered ascendingly by position. * Fetch all [ScrapedDriverStandings] from the database, ordered ascendingly by position.
*/ */

View File

@ -109,6 +109,18 @@ export interface RaceResult {
dnfs: string[]; dnfs: string[];
} }
export interface SeasonPickResult {
id: string;
correcthottake: string[];
wdcwinner: string;
wccwinner: string;
mostovertakes: string[];
mostdnfs: string[];
doohanstarts: number;
teamwinners: string[];
podiums: string[];
}
export interface CurrentPickedUser { export interface CurrentPickedUser {
id: string; id: string;
username: string; username: string;
@ -157,6 +169,19 @@ export interface RacePickPointsTotal {
total_points_per_pick: number; total_points_per_pick: number;
} }
export interface SeasonPickPoints {
id: string;
user: string;
hottake_points: number;
wdc_points: number;
wcc_points: number;
doohan_points: number;
overtakes_points: number;
dnfs_points: number;
teamwinner_points: number;
podium_points: number;
}
// Scraped Data // Scraped Data
export interface ScrapedStartingGrid { export interface ScrapedStartingGrid {

View File

@ -327,6 +327,7 @@
"raceresults", "raceresults",
"races", "races",
"seasonpicks", "seasonpicks",
"seasonpickresults",
"substitutions", "substitutions",
"teams", "teams",
"scraped_startinggrids", "scraped_startinggrids",
@ -346,6 +347,7 @@
"raceresults", "raceresults",
"races", "races",
"seasonpicks", "seasonpicks",
"seasonpickresults",
"substitutions", "substitutions",
"teams", "teams",
"scraped_startinggrids", "scraped_startinggrids",
@ -403,6 +405,15 @@
<Button href="/data/raceresults" onclick={close_drawer} color="surface" width="w-full" shadow> <Button href="/data/raceresults" onclick={close_drawer} color="surface" width="w-full" shadow>
Race Results Race Results
</Button> </Button>
<Button
href="/data/seasonpickresults"
onclick={close_drawer}
color="surface"
width="w-full"
shadow
>
Season Pick Results
</Button>
<Button <Button
href="/data/season/teams" href="/data/season/teams"
onclick={close_drawer} onclick={close_drawer}

View File

@ -5,9 +5,12 @@
import { get_by_value } from "$lib/database"; import { get_by_value } from "$lib/database";
import { PXX_COLORS } from "$lib/config"; import { PXX_COLORS } from "$lib/config";
import type { RaceResult } from "$lib/schema"; import type { RaceResult } from "$lib/schema";
import { pbUser } from "$lib/pocketbase";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
let disabled: boolean = $derived(!$pbUser?.admin);
const modalStore: ModalStore = getModalStore(); const modalStore: ModalStore = getModalStore();
const result_handler = async (event: Event, id?: string) => { const result_handler = async (event: Event, id?: string) => {
@ -92,7 +95,7 @@
</svelte:head> </svelte:head>
<div class="pb-2"> <div class="pb-2">
<Button width="w-full" color="tertiary" onclick={result_handler} shadow> <Button width="w-full" color="tertiary" onclick={result_handler} shadow {disabled}>
<span class="font-bold">Create Race Result</span> <span class="font-bold">Create Race Result</span>
</Button> </Button>
</div> </div>

View File

@ -4,9 +4,12 @@
import type { Driver, Team } from "$lib/schema"; import type { Driver, Team } from "$lib/schema";
import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton"; import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { pbUser } from "$lib/pocketbase";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
let disabled: boolean = $derived(!$pbUser?.admin);
const modalStore: ModalStore = getModalStore(); const modalStore: ModalStore = getModalStore();
const driver_handler = async (event: Event, id?: string) => { const driver_handler = async (event: Event, id?: string) => {
const driver: Driver | undefined = get_by_value(await data.drivers, "id", id ?? "Invalid"); const driver: Driver | undefined = get_by_value(await data.drivers, "id", id ?? "Invalid");
@ -73,10 +76,10 @@
</svelte:head> </svelte:head>
<div class="flex gap-2 pb-2"> <div class="flex gap-2 pb-2">
<Button width="w-full" color="tertiary" onclick={driver_handler} shadow> <Button width="w-full" color="tertiary" onclick={driver_handler} shadow {disabled}>
<span class="font-bold">Create New Driver</span> <span class="font-bold">Create New Driver</span>
</Button> </Button>
<Button width="w-full" color="secondary" onclick={teamswitch_handler} shadow> <Button width="w-full" color="secondary" onclick={teamswitch_handler} shadow {disabled}>
<span class="font-bold">Switch Driver Team</span> <span class="font-bold">Switch Driver Team</span>
</Button> </Button>
</div> </div>

View File

@ -5,9 +5,12 @@
import { get_by_value } from "$lib/database"; import { get_by_value } from "$lib/database";
import type { Race } from "$lib/schema"; import type { Race } from "$lib/schema";
import { format_date, shortdatetimeformat } from "$lib/date"; import { format_date, shortdatetimeformat } from "$lib/date";
import { pbUser } from "$lib/pocketbase";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
let disabled: boolean = $derived(!$pbUser?.admin);
const modalStore: ModalStore = getModalStore(); const modalStore: ModalStore = getModalStore();
const race_handler = async (event: Event, id?: string) => { const race_handler = async (event: Event, id?: string) => {
@ -64,7 +67,7 @@
</svelte:head> </svelte:head>
<div class="pb-2"> <div class="pb-2">
<Button width="w-full" color="tertiary" onclick={race_handler} shadow> <Button width="w-full" color="tertiary" onclick={race_handler} shadow {disabled}>
<span class="font-bold">Create New Race</span> <span class="font-bold">Create New Race</span>
</Button> </Button>
</div> </div>

View File

@ -4,9 +4,12 @@
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import type { Race, Substitution } from "$lib/schema"; import type { Race, Substitution } from "$lib/schema";
import { Button, Table, type TableColumn } from "$lib/components"; import { Button, Table, type TableColumn } from "$lib/components";
import { pbUser } from "$lib/pocketbase";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
let disabled: boolean = $derived(!$pbUser?.admin);
const modalStore: ModalStore = getModalStore(); const modalStore: ModalStore = getModalStore();
const substitution_handler = async (event: Event, id?: string) => { const substitution_handler = async (event: Event, id?: string) => {
const substitution: Substitution | undefined = get_by_value( const substitution: Substitution | undefined = get_by_value(
@ -64,7 +67,7 @@
</svelte:head> </svelte:head>
<div class="pb-2"> <div class="pb-2">
<Button width="w-full" color="tertiary" onclick={substitution_handler} shadow> <Button width="w-full" color="tertiary" onclick={substitution_handler} shadow {disabled}>
<span class="font-bold">Create New Substitution</span> <span class="font-bold">Create New Substitution</span>
</Button> </Button>
</div> </div>

View File

@ -4,9 +4,12 @@
import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton"; import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { get_by_value } from "$lib/database"; import { get_by_value } from "$lib/database";
import { pbUser } from "$lib/pocketbase";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
let disabled: boolean = $derived(!$pbUser?.admin);
const modalStore: ModalStore = getModalStore(); const modalStore: ModalStore = getModalStore();
const team_handler = async (event: Event, id?: string) => { const team_handler = async (event: Event, id?: string) => {
const team: Team | undefined = get_by_value(await data.teams, "id", id ?? "Invalid"); const team: Team | undefined = get_by_value(await data.teams, "id", id ?? "Invalid");
@ -47,7 +50,7 @@
</svelte:head> </svelte:head>
<div class="pb-2"> <div class="pb-2">
<Button width="w-full" color="tertiary" onclick={team_handler} shadow> <Button width="w-full" color="tertiary" onclick={team_handler} shadow {disabled}>
<span class="font-bold">Create New Team</span> <span class="font-bold">Create New Team</span>
</Button> </Button>
</div> </div>

View File

@ -0,0 +1,608 @@
<script lang="ts">
import type { PageData } from "./$types";
import { Button, Card, Dropdown, Input } from "$lib/components";
import {
get_by_value,
get_driver_headshot_template,
get_team_banner_template,
} from "$lib/database";
import {
Autocomplete,
Avatar,
InputChip,
SlideToggle,
getToastStore,
type AutocompleteOption,
type ToastStore,
} from "@skeletonlabs/skeleton";
import { pb, pbUser } from "$lib/pocketbase";
import type { Driver, SeasonPickResult, Team, User } from "$lib/schema";
import {
DRIVER_HEADSHOT_HEIGHT,
DRIVER_HEADSHOT_WIDTH,
TEAM_BANNER_HEIGHT,
TEAM_BANNER_WIDTH,
} from "$lib/config";
import { driver_dropdown_options, team_dropdown_options } from "$lib/dropdown";
import { get_error_toast } from "$lib/toast";
let { data }: { data: PageData } = $props();
const toastStore: ToastStore = getToastStore();
let disabled: boolean = $derived(!$pbUser?.admin);
const labelwidth: string = "150px";
// Await promises
let drivers: Driver[] | undefined = $state(undefined);
data.drivers.then((d: Driver[]) => (drivers = d));
let teams: Team[] | undefined = $state(undefined);
data.teams.then((t: Team[]) => (teams = t));
let seasonpickresult: SeasonPickResult | undefined = $state(undefined);
data.seasonpickresults.then((r: SeasonPickResult[]) => {
if (r.length === 1) {
seasonpickresult = r[0];
}
});
const active_drivers: Driver[] = $derived.by(() => {
if (!drivers) return [];
return drivers.filter((driver: Driver) => driver.active);
});
const hottake_correct: Record<string, boolean> = $state({});
data.users.then((users: User[]) => {
users.forEach((user: User) => {
let contains_user = Object.entries(hottake_correct).some(
([userid, correct]: [string, boolean]) => userid === user.id,
);
if (!contains_user) {
hottake_correct[user.id] = false;
}
});
});
data.seasonpickresults.then((results: SeasonPickResult[]) => {
if (results.length === 0) {
return;
}
const result: SeasonPickResult = results[0];
result.correcthottake.forEach((userid: string) => {
hottake_correct[userid] = true;
});
});
let wdcwinner_select_value: string = $state("INVALID");
let wccwinner_select_value: string = $state("INVALID");
let doohan_starts: string = $state("INVALID");
let overtakes_input: string = $state("");
let overtakes_chips: string[] = $state([]);
let dnfs_input: string = $state("");
let dnfs_chips: string[] = $state([]);
let teamwinners_input: string = $state("");
let teamwinners_chips: string[] = $state([]);
let podiums_input: string = $state("");
let podiums_chips: string[] = $state([]);
// Set all the results once it has loaded
data.seasonpickresults.then((r: SeasonPickResult[]) => {
if (r.length !== 1) {
return;
}
const result = r[0];
wdcwinner_select_value = result.wdcwinner;
wccwinner_select_value = result.wccwinner;
doohan_starts = result.doohanstarts.toString();
});
// Set the teamwinners/podiums states once the drivers are loaded
Promise.all([data.drivers, data.seasonpickresults]).then(
async ([drivers, results]: [Driver[], SeasonPickResult[]]) => {
if (results.length !== 1) {
return;
}
const result: SeasonPickResult = results[0];
overtakes_chips =
result?.mostovertakes.map(
(id: string) => get_by_value(drivers, "id", id)?.code ?? "Invalid",
) ?? [];
dnfs_chips =
result?.mostdnfs.map((id: string) => get_by_value(drivers, "id", id)?.code ?? "Invalid") ??
[];
teamwinners_chips =
result?.teamwinners.map(
(id: string) => get_by_value(drivers, "id", id)?.code ?? "Invalid",
) ?? [];
podiums_chips =
result?.podiums.map((id: string) => get_by_value(drivers, "id", id)?.code ?? "Invalid") ??
[];
},
);
// This is the actual data that gets sent through the form
let overtakes_ids: string[] = $derived.by(() => {
return seasonpickresult?.mostovertakes ?? [];
});
let dnfs_ids: string[] = $derived.by(() => {
return seasonpickresult?.mostdnfs ?? [];
});
let teamwinners_ids: string[] = $derived.by(() => {
return seasonpickresult?.teamwinners ?? [];
});
let podiums_ids: string[] = $derived.by(() => {
return seasonpickresult?.podiums ?? [];
});
let teamwinners_options: AutocompleteOption<string>[] = $derived.by(() =>
(drivers ?? [])
.filter((driver: Driver) => driver.active)
.map((driver: Driver) => {
const teamname: string = get_by_value(teams ?? [], "id", driver.team)?.name ?? "Invalid";
return {
firstname: driver.firstname,
lastname: driver.lastname,
code: driver.code,
teamname: teamname,
};
})
.sort((a, b) => a.teamname.localeCompare(b.teamname))
.map((driver) => {
return {
label: `${driver.teamname}: ${driver.firstname} ${driver.lastname}`,
value: driver.code,
};
}),
);
let teamwinners_whitelist: string[] = $derived.by(() =>
(drivers ?? []).map((driver: Driver) => driver.code),
);
let teamwinners_denylist: string[] = $derived.by(() => {
let denylist: string[] = [];
teamwinners_chips
.map((driver: string) => get_by_value(drivers ?? [], "code", driver))
.forEach((driver: Driver | undefined) => {
if (driver) {
(drivers ?? [])
.filter((d: Driver) => d.active)
.filter((d: Driver) => d.team === driver.team)
.forEach((d: Driver) => {
denylist.push(d.code);
});
}
});
return denylist;
});
let podiums_options: AutocompleteOption<string>[] = $derived.by(() =>
(drivers ?? [])
.filter((driver: Driver) => driver.active) // TODO: This shouldn't be filtered but if I don't the whole page disappears?
.sort((a: Driver, b: Driver) => a.firstname.localeCompare(b.firstname))
.map((driver: Driver) => {
return {
label: `${driver.firstname} ${driver.lastname}`,
value: driver.code,
};
}),
);
// Event handlers
const on_overtakes_chip_select = (event: CustomEvent<AutocompleteOption<string>>): void => {
if (disabled || !drivers) return;
// Can only select a driver once
if (overtakes_chips.includes(event.detail.value)) return;
// Manage labels that are displayed
overtakes_chips.push(event.detail.value);
overtakes_input = "";
// Manage ids that are submitted via form
const id: string = get_by_value(drivers, "code", event.detail.value)?.id ?? "Invalid";
if (!overtakes_ids.includes(id)) {
overtakes_ids.push(id);
}
};
const on_overtakes_chip_remove = (event: CustomEvent): void => {
overtakes_ids.splice(event.detail.chipIndex, 1);
};
const on_dnfs_chip_select = (event: CustomEvent<AutocompleteOption<string>>): void => {
if (disabled || !drivers) return;
// Can only select a driver once
if (dnfs_chips.includes(event.detail.value)) return;
// Manage labels that are displayed
dnfs_chips.push(event.detail.value);
dnfs_input = "";
// Manage ids that are submitted via form
const id: string = get_by_value(drivers, "code", event.detail.value)?.id ?? "Invalid";
if (!dnfs_ids.includes(id)) {
dnfs_ids.push(id);
}
};
const on_dnfs_chip_remove = (event: CustomEvent): void => {
dnfs_ids.splice(event.detail.chipIndex, 1);
};
const on_teamwinners_chip_select = (event: CustomEvent<AutocompleteOption<string>>): void => {
if (disabled || !drivers) return;
// Can only select 10 drivers
if (teamwinners_chips.length >= 10) return;
// Can only select a driver once
if (teamwinners_chips.includes(event.detail.value)) return;
// Manage labels that are displayed
teamwinners_chips.push(event.detail.value);
teamwinners_input = "";
// Manage ids that are submitted via form
const id: string = get_by_value(drivers, "code", event.detail.value)?.id ?? "Invalid";
if (!teamwinners_ids.includes(id)) {
teamwinners_ids.push(id);
}
};
const on_teamwinners_chip_remove = (event: CustomEvent): void => {
teamwinners_ids.splice(event.detail.chipIndex, 1);
};
const on_podiums_chip_select = (event: CustomEvent<AutocompleteOption<string>>): void => {
if (disabled || !drivers) return;
// Can only select a driver once
if (podiums_chips.includes(event.detail.value)) return;
// Manage labels that are displayed
podiums_chips.push(event.detail.value);
podiums_input = "";
// Manage ids that are submitted via form
const id: string = get_by_value(drivers, "code", event.detail.value)?.id ?? "Invalid";
if (!podiums_ids.includes(id)) {
podiums_ids.push(id);
}
};
const on_podiums_chip_remove = (event: CustomEvent): void => {
podiums_ids.splice(event.detail.chipIndex, 1);
};
// Database actions
const update_seasonpickresults = (create?: boolean): (() => Promise<void>) => {
const handler = async (): Promise<void> => {
if (!$pbUser?.id || $pbUser.id === "") {
toastStore.trigger(get_error_toast("Invalid user id!"));
return;
}
if (!wdcwinner_select_value || wdcwinner_select_value === "") {
toastStore.trigger(get_error_toast("Please select a driver for WDC!"));
return;
}
if (!wccwinner_select_value || wccwinner_select_value === "") {
toastStore.trigger(get_error_toast("Please select a team for WCC!"));
return;
}
if (!overtakes_ids || overtakes_ids.length === 0) {
toastStore.trigger(get_error_toast("Please select a driver for most overtakes!"));
return;
}
if (!dnfs_ids || dnfs_ids.length === 0) {
toastStore.trigger(get_error_toast("Please select a driver for most DNFs!"));
return;
}
if (
!doohan_starts ||
doohan_starts === "" ||
parseInt(doohan_starts) <= 0 ||
parseInt(doohan_starts) > 24
) {
toastStore.trigger(
get_error_toast("Please enter between 0 and 24 starts for Jack Doohan!"),
);
return;
}
if (!teamwinners_ids || teamwinners_ids.length !== 10) {
toastStore.trigger(get_error_toast("Please select a winner for each team!"));
return;
}
if (!podiums_ids || podiums_ids.length < 3) {
toastStore.trigger(get_error_toast("Please select at least 3 drivers with podiums!"));
return;
}
const seasonpickresults_data = {
user: $pbUser.id,
correcthottake: Object.entries(hottake_correct)
.filter(([userid, correct]: [string, boolean]) => correct)
.map(([userid, correct]: [string, boolean]) => userid),
wdcwinner: wdcwinner_select_value,
wccwinner: wccwinner_select_value,
mostovertakes: overtakes_ids,
mostdnfs: dnfs_ids,
doohanstarts: doohan_starts,
teamwinners: teamwinners_ids,
podiums: podiums_ids,
};
try {
if (create) {
await pb.collection("seasonpickresults").create(seasonpickresults_data);
} else {
if (!seasonpickresult?.id) {
toastStore.trigger(get_error_toast("Invalid seasonpickresult id!"));
return;
}
await pb
.collection("seasonpickresults")
.update(seasonpickresult.id, seasonpickresults_data);
}
} catch (error) {
toastStore.trigger(get_error_toast("" + error));
}
};
return handler;
};
</script>
<svelte:head>
<title>Formula 11 - Season Pick Results</title>
</svelte:head>
<div class="pb-2">
{#if seasonpickresult}
<Button onclick={update_seasonpickresults()} width="w-full" color="tertiary" shadow {disabled}>
<span class="font-bold">Update Season Pick Results</span>
</Button>
{:else}
<Button
onclick={update_seasonpickresults(true)}
width="w-full"
color="tertiary"
shadow
{disabled}
>
<span class="font-bold">Create Season Pick Results</span>
</Button>
{/if}
</div>
{#await Promise.all( [data.graphics, data.seasonpicks, data.users, data.seasonpickresults, data.drivers, data.teams], ) then [graphics, seasonpicks, users, seasonpickresults, drivers, teams]}
<div class="grid grid-cols-1 gap-2 xl:grid-cols-2">
<!-- WDC -->
<Card
imgsrc={get_by_value<Driver>(drivers, "id", wdcwinner_select_value)?.headshot_url ??
get_driver_headshot_template(graphics)}
imgid="wdcwinner_headshot"
width="w-full h-32"
imgwidth={DRIVER_HEADSHOT_WIDTH}
imgheight={DRIVER_HEADSHOT_HEIGHT}
imgleft={true}
imgshadow={false}
extraimgclass="mt-[20px]"
extraclass="w-full"
>
<h1 class="mb-2 text-lg font-bold">Which driver fucking obliterated this season?</h1>
<Dropdown
bind:value={wdcwinner_select_value}
options={driver_dropdown_options(active_drivers)}
{labelwidth}
{disabled}
class="w-full"
required
>
WDC Winner
</Dropdown>
</Card>
<!-- WCC -->
<Card
imgsrc={get_by_value<Team>(teams, "id", wccwinner_select_value)?.banner_url ??
get_team_banner_template(graphics)}
imgid="wccwinner_banner"
width="w-full h-32"
imgwidth={TEAM_BANNER_WIDTH}
imgheight={TEAM_BANNER_HEIGHT}
imgleft={true}
imgshadow={false}
extraimgclass="mt-[16px] rounded-r-md"
extraclass="w-full"
>
<h1 class="mb-2 text-lg font-bold">Which constructor won this season?</h1>
<Dropdown
bind:value={wccwinner_select_value}
options={team_dropdown_options(teams)}
{labelwidth}
{disabled}
class="w-full"
required
>
WCC Winner
</Dropdown>
</Card>
</div>
<!-- Doohan Starts -->
<div class="mt-2">
<Card
imgsrc={get_by_value<Driver>(drivers, "code", "DOO")?.headshot_url ??
get_driver_headshot_template(graphics)}
imgid="doohan_headshot"
width="w-full h-32"
imgwidth={DRIVER_HEADSHOT_WIDTH}
imgheight={DRIVER_HEADSHOT_HEIGHT}
imgleft={true}
imgshadow={false}
extraimgclass="mt-[20px]"
extraclass="w-full"
>
<h1 class="mb-2 text-lg font-bold">How often did JACK DOOHAN start?</h1>
<Input
bind:value={doohan_starts}
placeholder="JACK DOOHAN"
type="number"
required
min="0"
max="24"
{labelwidth}
>
Doohan Starts
</Input>
</Card>
</div>
<!-- HOTTAKES -->
<div class="mt-4 grid grid-cols-1 gap-2 xl:grid-cols-4">
{#each seasonpicks as seasonpick}
{@const user = get_by_value(users, "id", seasonpick.user)}
<Card>
<div class="flex h-32 gap-2">
<div class="mt-2">
<Avatar
id="{user?.id ?? 'INVALID'}_avatar"
src={user?.avatar_url}
rounded="rounded-full"
width="w-12 h-12"
background="bg-primary-50"
/>
</div>
<div class="w-full">
<h1 class="text-lg font-bold">{user?.username ?? "INVALID"}'s Hottake 💀</h1>
<p>
"{seasonpick.hottake}"
</p>
</div>
<div class="flex flex-col gap-1">
<p class="font-bold">Correct:</p>
<SlideToggle
name="correct"
background="bg-primary-500"
active="bg-tertiary-500"
bind:checked={hottake_correct[user?.id ?? "INVALID"]}
{disabled}
/>
</div>
</div>
</Card>
{/each}
</div>
<div class="mt-4 grid grid-cols-1 gap-2 xl:grid-cols-2">
<!-- Overtakes chips -->
<InputChip
bind:input={overtakes_input}
bind:value={overtakes_chips}
whitelist={teamwinners_whitelist}
allowUpperCase
placeholder="Select Drivers with most Overtakes..."
name="overtakes_codes"
{disabled}
required
on:remove={on_overtakes_chip_remove}
/>
<!-- Overtakes autocomplete options -->
<div class="card max-h-48 w-full overflow-y-auto p-2" tabindex="-1">
<Autocomplete
bind:input={overtakes_input}
options={podiums_options}
denylist={overtakes_chips}
on:selection={on_overtakes_chip_select}
/>
</div>
<!-- DNFs chips -->
<InputChip
bind:input={dnfs_input}
bind:value={dnfs_chips}
whitelist={teamwinners_whitelist}
allowUpperCase
placeholder="Select Drivers with most DNFs..."
name="dnfs_codes"
{disabled}
required
on:remove={on_dnfs_chip_remove}
/>
<!-- DNFs autocomplete options -->
<div class="card max-h-48 w-full overflow-y-auto p-2" tabindex="-1">
<Autocomplete
bind:input={dnfs_input}
options={podiums_options}
denylist={dnfs_chips}
on:selection={on_dnfs_chip_select}
/>
</div>
<!-- Teamwinners chips -->
<InputChip
bind:input={teamwinners_input}
bind:value={teamwinners_chips}
whitelist={teamwinners_whitelist}
allowUpperCase
placeholder="Select Teamwinners..."
name="teamwinners_codes"
{disabled}
required
on:remove={on_teamwinners_chip_remove}
/>
<!-- Teamwinners autocomplete options -->
<div class="card max-h-48 w-full overflow-y-auto p-2" tabindex="-1">
<Autocomplete
bind:input={teamwinners_input}
options={teamwinners_options}
denylist={teamwinners_denylist}
on:selection={on_teamwinners_chip_select}
/>
</div>
<!-- Podiums chips -->
<InputChip
bind:input={podiums_input}
bind:value={podiums_chips}
whitelist={teamwinners_whitelist}
allowUpperCase
placeholder="Select Drivers with Podiums..."
name="podiums_codes"
{disabled}
required
on:remove={on_podiums_chip_remove}
/>
<!-- Podiums autocomplete options -->
<div class="card max-h-48 w-full overflow-y-auto p-2" tabindex="-1">
<Autocomplete
bind:input={podiums_input}
options={podiums_options}
denylist={podiums_chips}
on:selection={on_podiums_chip_select}
/>
</div>
</div>
{/await}

View File

@ -0,0 +1,20 @@
import {
fetch_drivers,
fetch_seasonpickresults,
fetch_teams,
fetch_users,
fetch_visibleseasonpicks,
} from "$lib/fetch";
import type { PageLoad } from "../../$types";
export const load: PageLoad = async ({ fetch, depends }) => {
depends("data:drivers", "data:seasonpickresults", "data:users", "data:seasonpicks", "data:teams");
return {
users: fetch_users(fetch),
drivers: fetch_drivers(fetch),
teams: fetch_teams(fetch),
seasonpicks: fetch_visibleseasonpicks(fetch),
seasonpickresults: fetch_seasonpickresults(fetch),
};
};

View File

@ -2,7 +2,13 @@
import { make_chart_options } from "$lib/chart"; import { make_chart_options } from "$lib/chart";
import { Table, type TableColumn } from "$lib/components"; import { Table, type TableColumn } from "$lib/components";
import { get_by_value } from "$lib/database"; import { get_by_value } from "$lib/database";
import type { RacePickPoints, RacePickPointsAcc, User } from "$lib/schema"; import type {
RacePickPoints,
RacePickPointsAcc,
RacePickPointsTotal,
SeasonPickPoints,
User,
} from "$lib/schema";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { import {
LineChart, LineChart,
@ -24,6 +30,42 @@
let racepickpointsacc: RacePickPointsAcc[] | undefined = $state(undefined); let racepickpointsacc: RacePickPointsAcc[] | undefined = $state(undefined);
data.racepickpointsacc.then((r: RacePickPointsAcc[]) => (racepickpointsacc = r)); data.racepickpointsacc.then((r: RacePickPointsAcc[]) => (racepickpointsacc = r));
let racepickpointstotal: RacePickPointsTotal[] | undefined = $state(undefined);
let seasonpickpoints: SeasonPickPoints[] | undefined = $state(undefined);
Promise.all([data.racepickpointstotal, data.seasonpickpoints]).then(
([rpp, spp]: [RacePickPointsTotal[], SeasonPickPoints[]]) => {
if (spp.length === 0 || !spp) {
racepickpointstotal = rpp;
seasonpickpoints = undefined;
return;
}
racepickpointstotal = rpp.sort((a: RacePickPointsTotal, b: RacePickPointsTotal) => {
let apoints = spp.filter((p: SeasonPickPoints) => p.user === a.user)[0];
let bpoints = spp.filter((p: SeasonPickPoints) => p.user === b.user)[0];
return (
b.total_points +
calc_season_points(bpoints) -
(a.total_points + calc_season_points(apoints))
);
});
seasonpickpoints = spp;
},
);
const calc_season_points = (p: SeasonPickPoints): number => {
return (
p.hottake_points +
p.wdc_points +
p.wcc_points +
p.overtakes_points +
p.dnfs_points +
p.teamwinner_points +
p.podium_points +
p.doohan_points
);
};
const leaderboard_columns: TableColumn[] = $derived([ const leaderboard_columns: TableColumn[] = $derived([
{ {
data_value_name: "user", data_value_name: "user",
@ -32,10 +74,22 @@
`<span class='badge variant-filled-surface'>${get_by_value(await data.users, "id", value)?.firstname ?? "Invalid"}</span>`, `<span class='badge variant-filled-surface'>${get_by_value(await data.users, "id", value)?.firstname ?? "Invalid"}</span>`,
}, },
{ {
data_value_name: "total_points", data_value_name: "user",
label: "Total", label: "Total",
valuefun: async (value: string): Promise<string> => valuefun: async (value: string): Promise<string> => {
`<span class='badge variant-filled-surface'>${value}</span>`, if (!racepickpointstotal) {
return "ERR";
}
// let seasonpoints = await data.seasonpickpoints;
let points = get_by_value(racepickpointstotal, "user", value)?.total_points ?? 0;
if (!seasonpickpoints) {
return `<span class='badge variant-filled-surface'>${points}</span>`;
}
points += calc_season_points(get_by_value(seasonpickpoints, "user", value)!);
return `<span class='badge variant-filled-surface'>${points}</span>`;
},
}, },
{ {
data_value_name: "total_pxx_points", data_value_name: "total_pxx_points",
@ -45,6 +99,18 @@
data_value_name: "total_dnf_points", data_value_name: "total_dnf_points",
label: "DNF", label: "DNF",
}, },
{
data_value_name: "user",
label: "Season",
valuefun: async (value: string): Promise<string> => {
if (!seasonpickpoints) {
return "🍆";
}
let p = seasonpickpoints.filter((p: SeasonPickPoints) => p.user === value)[0];
return calc_season_points(p).toString();
},
},
{ {
data_value_name: "total_points_per_pick", data_value_name: "total_points_per_pick",
label: "Per Pick", label: "Per Pick",
@ -90,7 +156,7 @@
</div> </div>
<div class="mt-2"> <div class="mt-2">
{#await data.racepickpointstotal then racepickpointstotal} {#if racepickpointstotal}
<Table data={racepickpointstotal} columns={leaderboard_columns} /> <Table data={racepickpointstotal} columns={leaderboard_columns} />
{/await} {/if}
</div> </div>

View File

@ -3,16 +3,18 @@ import {
fetch_racepickpoints, fetch_racepickpoints,
fetch_racepickpointsacc, fetch_racepickpointsacc,
fetch_racepickpointstotal, fetch_racepickpointstotal,
fetch_seasonpickpoints,
} from "$lib/fetch"; } from "$lib/fetch";
import type { PageLoad } from "../$types"; import type { PageLoad } from "../$types";
export const load: PageLoad = async ({ fetch, depends }) => { export const load: PageLoad = async ({ fetch, depends }) => {
depends("data:users", "data:raceresults"); depends("data:users", "data:raceresults", "data:seasonpickresults");
return { return {
users: fetch_users(fetch), users: fetch_users(fetch),
racepickpoints: fetch_racepickpoints(fetch), racepickpoints: fetch_racepickpoints(fetch),
racepickpointsacc: fetch_racepickpointsacc(fetch), racepickpointsacc: fetch_racepickpointsacc(fetch),
racepickpointstotal: fetch_racepickpointstotal(fetch), racepickpointstotal: fetch_racepickpointstotal(fetch),
seasonpickpoints: fetch_seasonpickpoints(fetch),
}; };
}; };

View File

@ -7,7 +7,14 @@
type ModalStore, type ModalStore,
} from "@skeletonlabs/skeleton"; } from "@skeletonlabs/skeleton";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import type { Driver, Hottake, SeasonPick, SeasonPickedUser, User } from "$lib/schema"; import type {
Driver,
Hottake,
SeasonPick,
SeasonPickedUser,
SeasonPickResult,
User,
} from "$lib/schema";
import { ChequeredFlagIcon, LazyImage } from "$lib/components"; import { ChequeredFlagIcon, LazyImage } from "$lib/components";
import { import {
get_by_value, get_by_value,
@ -41,6 +48,29 @@
modalStore.trigger(modalSettings); modalStore.trigger(modalSettings);
}; };
// Await promises
let seasonpickresult: SeasonPickResult | undefined = $state(undefined);
data.seasonpickresults.then((r: SeasonPickResult[]) => {
if (r.length === 1) {
seasonpickresult = r[0];
}
});
let correct_doohanstarts: number | undefined = $state(undefined);
Promise.all([data.seasonpickresults, data.seasonpicks]).then(
([results, picks]: [SeasonPickResult[], SeasonPick[]]) => {
if (results.length === 1) {
let result = results[0];
correct_doohanstarts = Math.min(
...picks.map((pick: SeasonPick) => {
return Math.abs(pick.doohanstarts - result.doohanstarts);
}),
);
}
},
);
// Users that have already picked the season // Users that have already picked the season
let pickedusers: Promise<SeasonPickedUser[]> = $derived.by(async () => let pickedusers: Promise<SeasonPickedUser[]> = $derived.by(async () =>
(await data.seasonpickedusers).filter( (await data.seasonpickedusers).filter(
@ -63,7 +93,7 @@
<!-- Await this here so the accordion doesn't lag when opening --> <!-- Await this here so the accordion doesn't lag when opening -->
<!-- Only show the stuff if signed in --> <!-- Only show the stuff if signed in -->
{#if $pbUser} {#if $pbUser}
{#await Promise.all( [data.drivers, data.teams, data.seasonpickedusers, pickedusers, outstandingusers], ) then [drivers, teams, currentpicked, picked, outstanding]} {#await Promise.all( [data.drivers, data.teams, data.seasonpickedusers, data.seasonpickresults, pickedusers, outstandingusers], ) then [drivers, teams, currentpicked, results, picked, outstanding]}
{@const teamwinners = data.seasonpick {@const teamwinners = data.seasonpick
? data.seasonpick.teamwinners ? data.seasonpick.teamwinners
.map((id: string) => get_by_value(drivers, "id", id) as Driver) .map((id: string) => get_by_value(drivers, "id", id) as Driver)
@ -374,16 +404,29 @@
<div class="ml-1 w-full min-w-36"> <div class="ml-1 w-full min-w-36">
<!-- Hottake --> <!-- Hottake -->
<div <div
class="mt-1 h-32 w-full overflow-y-scroll border bg-surface-200 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 {seasonpickresult?.correcthottake.includes(
user.id,
)
? '!bg-tertiary-500'
: seasonpickresult
? '!bg-primary-500'
: ''}"
> >
<div class="mx-auto w-fit text-xs font-bold lg:text-sm"> <div class="mx-auto w-fit rounded-md p-1 text-xs font-bold lg:text-sm">
{hottake?.hottake ?? "?"} {hottake?.hottake ?? "?"}
</div> </div>
</div> </div>
{#if seasonpicks.length > 0} {#if seasonpicks.length > 0}
<!-- Drivers Champion --> <!-- Drivers Champion -->
<div class="mt-1 h-20 w-full border bg-surface-200 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 {seasonpickresult?.wdcwinner ===
wdcwinner?.id
? '!bg-tertiary-500'
: seasonpickresult
? '!bg-primary-500'
: ''}"
>
<div class="mx-auto w-fit"> <div class="mx-auto w-fit">
<!-- NOTE: The containerstyle should be 64x64, don't know why that doesn't fit... (also below) --> <!-- NOTE: The containerstyle should be 64x64, don't know why that doesn't fit... (also below) -->
<LazyImage <LazyImage
@ -398,7 +441,14 @@
</div> </div>
<!-- Constructors Champion --> <!-- Constructors Champion -->
<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="mt-1 h-20 w-full border bg-surface-200 p-1 px-1 py-2 leading-3 lg:px-2 {seasonpickresult?.wccwinner ===
wccwinner?.id
? '!bg-tertiary-500'
: seasonpickresult
? '!bg-primary-500'
: ''}"
>
<div class="mx-auto w-fit"> <div class="mx-auto w-fit">
<LazyImage <LazyImage
src={wccwinner?.banner_url ?? get_team_banner_template(data.graphics)} src={wccwinner?.banner_url ?? get_team_banner_template(data.graphics)}
@ -412,7 +462,15 @@
</div> </div>
<!-- Most Overtakes --> <!-- Most Overtakes -->
<div class="mt-1 h-20 w-full border bg-surface-200 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 {seasonpickresult?.mostovertakes.includes(
mostovertakes?.id ?? 'INVALID',
)
? '!bg-tertiary-500'
: seasonpickresult
? '!bg-primary-500'
: ''}"
>
<div class="mx-auto w-fit"> <div class="mx-auto w-fit">
<LazyImage <LazyImage
src={mostovertakes?.headshot_url ?? get_driver_headshot_template(data.graphics)} src={mostovertakes?.headshot_url ?? get_driver_headshot_template(data.graphics)}
@ -428,7 +486,15 @@
</div> </div>
<!-- Most DNFs --> <!-- Most DNFs -->
<div class="mt-1 h-20 w-full border bg-surface-200 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 {seasonpickresult?.mostdnfs.includes(
mostdnfs?.id ?? 'INVALID',
)
? '!bg-tertiary-500'
: seasonpickresult
? '!bg-primary-500'
: ''}"
>
<div class="mx-auto w-fit"> <div class="mx-auto w-fit">
<LazyImage <LazyImage
src={mostdnfs?.headshot_url ?? get_driver_headshot_template(data.graphics)} src={mostdnfs?.headshot_url ?? get_driver_headshot_template(data.graphics)}
@ -442,7 +508,15 @@
</div> </div>
<!-- Doohan Starts --> <!-- Doohan Starts -->
<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="mt-1 h-20 w-full border bg-surface-200 p-1 px-1 py-2 leading-3 lg:px-2 {Math.abs(
(seasonpickresult?.doohanstarts ?? 0) - pick?.doohanstarts,
) === correct_doohanstarts
? '!bg-tertiary-500'
: seasonpickresult
? '!bg-primary-500'
: ''}"
>
<div class="mx-auto w-fit text-xs lg:text-sm"> <div class="mx-auto w-fit text-xs lg:text-sm">
Jack Doohan startet <span class="font-bold">{pick?.doohanstarts ?? "?"}</span> mal. Jack Doohan startet <span class="font-bold">{pick?.doohanstarts ?? "?"}</span> mal.
</div> </div>
@ -466,7 +540,16 @@
style="color: {color}; background-color: {color};" style="color: {color}; background-color: {color};"
> >
</span> </span>
<span class="w-7 align-middle" style="line-height: 20px;"> <span
class="w-7 rounded-md align-middle {pick.teamwinners.includes(
driver.id,
) && seasonpickresult?.teamwinners.includes(driver.id)
? '!bg-tertiary-500'
: seasonpickresult
? '!bg-primary-500'
: ''}"
style="line-height: 20px;"
>
{driver?.code} {driver?.code}
</span> </span>
</div> </div>
@ -495,7 +578,15 @@
style="color: {color}; background-color: {color};" style="color: {color}; background-color: {color};"
> >
</span> </span>
<span class="w-7 align-middle" style="line-height: 20px;"> <span
class="w-7 rounded-md align-middle {pick.podiums.includes(driver.id) &&
seasonpickresult?.podiums.includes(driver.id)
? '!bg-tertiary-500'
: seasonpickresult
? '!bg-primary-500'
: ''}"
style="line-height: 20px;"
>
{driver?.code} {driver?.code}
</span> </span>
</div> </div>

View File

@ -6,11 +6,19 @@ import {
fetch_visibleseasonpicks, fetch_visibleseasonpicks,
fetch_teams, fetch_teams,
fetch_currentrace, fetch_currentrace,
fetch_seasonpickresults,
} from "$lib/fetch"; } from "$lib/fetch";
import type { PageLoad } from "../$types"; import type { PageLoad } from "../$types";
export const load: PageLoad = async ({ fetch, depends }) => { export const load: PageLoad = async ({ fetch, depends }) => {
depends("data:teams", "data:drivers", "data:seasonpicks", "data:user", "data:users"); depends(
"data:teams",
"data:drivers",
"data:seasonpicks",
"data:user",
"data:users",
"data:seasonpickresults",
);
return { return {
teams: fetch_teams(fetch), teams: fetch_teams(fetch),
@ -19,6 +27,7 @@ export const load: PageLoad = async ({ fetch, depends }) => {
hottakes: fetch_hottakes(fetch), hottakes: fetch_hottakes(fetch),
seasonpickedusers: fetch_seasonpickedusers(fetch), seasonpickedusers: fetch_seasonpickedusers(fetch),
currentrace: fetch_currentrace(fetch), // Used for countdown currentrace: fetch_currentrace(fetch), // Used for countdown
seasonpickresults: fetch_seasonpickresults(fetch),
seasonpick: await fetch_currentseasonpick(fetch), seasonpick: await fetch_currentseasonpick(fetch),
}; };