Compare commits
12 Commits
7434165ab0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
ba307eb4c4
|
|||
|
7cb0931329
|
|||
|
db0365adad
|
|||
|
5cf7974a79
|
|||
|
9d83673a90
|
|||
|
9cdbe45ace
|
|||
|
9755d06220
|
|||
|
aa14ca6782
|
|||
|
a46a176d59
|
|||
|
94e728bf39
|
|||
|
24a713b471
|
|||
|
9bdf6ea8ef
|
@ -307,7 +307,8 @@ rec {
|
||||
abbr -a pb "pocketbase serve --http 192.168.86.50:8090 --dev"
|
||||
abbr -a dev "npm run dev -- --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
|
||||
builtins.concatStringsSep "\n" [
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -31,7 +31,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"runes2": "^1.1.4",
|
||||
"svelte": "^5.23.0",
|
||||
"svelte-check": "^4.1.5",
|
||||
"svelte-check": "^4.3.5",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.8.2",
|
||||
"uuid": "^11.1.0",
|
||||
@ -4853,9 +4853,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.5.tgz",
|
||||
"integrity": "sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg==",
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.5.tgz",
|
||||
"integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"runes2": "^1.1.4",
|
||||
"svelte": "^5.23.0",
|
||||
"svelte-check": "^4.1.5",
|
||||
"svelte-check": "^4.3.5",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.8.2",
|
||||
"uuid": "^11.1.0",
|
||||
|
||||
273
pb_schema.json
273
pb_schema.json
@ -1570,6 +1570,157 @@
|
||||
"indexes": [],
|
||||
"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",
|
||||
"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,
|
||||
"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",
|
||||
"listRule": "",
|
||||
|
||||
@ -2,8 +2,9 @@
|
||||
import type { Snippet } from "svelte";
|
||||
import { LazyImage } from "$lib/components";
|
||||
import { error } from "@sveltejs/kit";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
interface CardProps {
|
||||
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
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. */
|
||||
@ -21,6 +22,18 @@
|
||||
/** Hide the header image element. It can be shown by removing the "hidden" property using JS and the imgid. */
|
||||
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] */
|
||||
width?: string;
|
||||
|
||||
@ -35,6 +48,10 @@
|
||||
imgheight = undefined,
|
||||
imgid = undefined,
|
||||
imghidden = false,
|
||||
imgleft = false,
|
||||
imgshadow = true,
|
||||
extraimgclass = "",
|
||||
extraclass = "",
|
||||
width = "w-auto",
|
||||
imgonclick = undefined,
|
||||
...restProps
|
||||
@ -45,7 +62,7 @@
|
||||
}
|
||||
</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 -->
|
||||
{#if imgsrc !== undefined}
|
||||
<LazyImage
|
||||
@ -53,7 +70,7 @@
|
||||
src={imgsrc}
|
||||
alt="Card header"
|
||||
draggable="false"
|
||||
class="select-none shadow"
|
||||
class="select-none {imgshadow ? 'shadow' : ''} {extraimgclass}"
|
||||
hidden={imghidden}
|
||||
imgwidth={imgwidth ?? 0}
|
||||
imgheight={imgheight ?? 0}
|
||||
@ -61,7 +78,7 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="p-2" {...restProps}>
|
||||
<div class="p-2 {extraclass}" {...restProps}>
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -18,6 +18,8 @@ import type {
|
||||
ScrapedTeamStanding,
|
||||
SeasonPick,
|
||||
SeasonPickedUser,
|
||||
SeasonPickPoints,
|
||||
SeasonPickResult,
|
||||
Substitution,
|
||||
Team,
|
||||
User,
|
||||
@ -115,6 +117,19 @@ export const fetch_raceresults = async (
|
||||
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
|
||||
*/
|
||||
@ -309,6 +324,19 @@ export const fetch_racepickpointstotal = async (
|
||||
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.
|
||||
*/
|
||||
|
||||
@ -109,6 +109,18 @@ export interface RaceResult {
|
||||
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 {
|
||||
id: string;
|
||||
username: string;
|
||||
@ -157,6 +169,19 @@ export interface RacePickPointsTotal {
|
||||
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
|
||||
|
||||
export interface ScrapedStartingGrid {
|
||||
|
||||
@ -327,6 +327,7 @@
|
||||
"raceresults",
|
||||
"races",
|
||||
"seasonpicks",
|
||||
"seasonpickresults",
|
||||
"substitutions",
|
||||
"teams",
|
||||
"scraped_startinggrids",
|
||||
@ -346,6 +347,7 @@
|
||||
"raceresults",
|
||||
"races",
|
||||
"seasonpicks",
|
||||
"seasonpickresults",
|
||||
"substitutions",
|
||||
"teams",
|
||||
"scraped_startinggrids",
|
||||
@ -403,6 +405,15 @@
|
||||
<Button href="/data/raceresults" onclick={close_drawer} color="surface" width="w-full" shadow>
|
||||
Race Results
|
||||
</Button>
|
||||
<Button
|
||||
href="/data/seasonpickresults"
|
||||
onclick={close_drawer}
|
||||
color="surface"
|
||||
width="w-full"
|
||||
shadow
|
||||
>
|
||||
Season Pick Results
|
||||
</Button>
|
||||
<Button
|
||||
href="/data/season/teams"
|
||||
onclick={close_drawer}
|
||||
|
||||
@ -5,9 +5,12 @@
|
||||
import { get_by_value } from "$lib/database";
|
||||
import { PXX_COLORS } from "$lib/config";
|
||||
import type { RaceResult } from "$lib/schema";
|
||||
import { pbUser } from "$lib/pocketbase";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let disabled: boolean = $derived(!$pbUser?.admin);
|
||||
|
||||
const modalStore: ModalStore = getModalStore();
|
||||
|
||||
const result_handler = async (event: Event, id?: string) => {
|
||||
@ -92,7 +95,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<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>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -4,9 +4,12 @@
|
||||
import type { Driver, Team } from "$lib/schema";
|
||||
import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton";
|
||||
import type { PageData } from "./$types";
|
||||
import { pbUser } from "$lib/pocketbase";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let disabled: boolean = $derived(!$pbUser?.admin);
|
||||
|
||||
const modalStore: ModalStore = getModalStore();
|
||||
const driver_handler = async (event: Event, id?: string) => {
|
||||
const driver: Driver | undefined = get_by_value(await data.drivers, "id", id ?? "Invalid");
|
||||
@ -73,10 +76,10 @@
|
||||
</svelte:head>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -5,9 +5,12 @@
|
||||
import { get_by_value } from "$lib/database";
|
||||
import type { Race } from "$lib/schema";
|
||||
import { format_date, shortdatetimeformat } from "$lib/date";
|
||||
import { pbUser } from "$lib/pocketbase";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let disabled: boolean = $derived(!$pbUser?.admin);
|
||||
|
||||
const modalStore: ModalStore = getModalStore();
|
||||
|
||||
const race_handler = async (event: Event, id?: string) => {
|
||||
@ -64,7 +67,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<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>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -4,9 +4,12 @@
|
||||
import type { PageData } from "./$types";
|
||||
import type { Race, Substitution } from "$lib/schema";
|
||||
import { Button, Table, type TableColumn } from "$lib/components";
|
||||
import { pbUser } from "$lib/pocketbase";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let disabled: boolean = $derived(!$pbUser?.admin);
|
||||
|
||||
const modalStore: ModalStore = getModalStore();
|
||||
const substitution_handler = async (event: Event, id?: string) => {
|
||||
const substitution: Substitution | undefined = get_by_value(
|
||||
@ -64,7 +67,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<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>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -4,9 +4,12 @@
|
||||
import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton";
|
||||
import type { PageData } from "./$types";
|
||||
import { get_by_value } from "$lib/database";
|
||||
import { pbUser } from "$lib/pocketbase";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
let disabled: boolean = $derived(!$pbUser?.admin);
|
||||
|
||||
const modalStore: ModalStore = getModalStore();
|
||||
const team_handler = async (event: Event, id?: string) => {
|
||||
const team: Team | undefined = get_by_value(await data.teams, "id", id ?? "Invalid");
|
||||
@ -47,7 +50,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<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>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
608
src/routes/data/seasonpickresults/+page.svelte
Normal file
608
src/routes/data/seasonpickresults/+page.svelte
Normal 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}
|
||||
20
src/routes/data/seasonpickresults/+page.ts
Normal file
20
src/routes/data/seasonpickresults/+page.ts
Normal 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),
|
||||
};
|
||||
};
|
||||
@ -2,7 +2,13 @@
|
||||
import { make_chart_options } from "$lib/chart";
|
||||
import { Table, type TableColumn } from "$lib/components";
|
||||
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 {
|
||||
LineChart,
|
||||
@ -24,6 +30,42 @@
|
||||
let racepickpointsacc: RacePickPointsAcc[] | undefined = $state(undefined);
|
||||
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([
|
||||
{
|
||||
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>`,
|
||||
},
|
||||
{
|
||||
data_value_name: "total_points",
|
||||
data_value_name: "user",
|
||||
label: "Total",
|
||||
valuefun: async (value: string): Promise<string> =>
|
||||
`<span class='badge variant-filled-surface'>${value}</span>`,
|
||||
valuefun: async (value: string): Promise<string> => {
|
||||
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",
|
||||
@ -45,6 +99,18 @@
|
||||
data_value_name: "total_dnf_points",
|
||||
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",
|
||||
label: "Per Pick",
|
||||
@ -90,7 +156,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
{#await data.racepickpointstotal then racepickpointstotal}
|
||||
{#if racepickpointstotal}
|
||||
<Table data={racepickpointstotal} columns={leaderboard_columns} />
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@ -3,16 +3,18 @@ import {
|
||||
fetch_racepickpoints,
|
||||
fetch_racepickpointsacc,
|
||||
fetch_racepickpointstotal,
|
||||
fetch_seasonpickpoints,
|
||||
} from "$lib/fetch";
|
||||
import type { PageLoad } from "../$types";
|
||||
|
||||
export const load: PageLoad = async ({ fetch, depends }) => {
|
||||
depends("data:users", "data:raceresults");
|
||||
depends("data:users", "data:raceresults", "data:seasonpickresults");
|
||||
|
||||
return {
|
||||
users: fetch_users(fetch),
|
||||
racepickpoints: fetch_racepickpoints(fetch),
|
||||
racepickpointsacc: fetch_racepickpointsacc(fetch),
|
||||
racepickpointstotal: fetch_racepickpointstotal(fetch),
|
||||
seasonpickpoints: fetch_seasonpickpoints(fetch),
|
||||
};
|
||||
};
|
||||
|
||||
@ -7,7 +7,14 @@
|
||||
type ModalStore,
|
||||
} from "@skeletonlabs/skeleton";
|
||||
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 {
|
||||
get_by_value,
|
||||
@ -41,6 +48,29 @@
|
||||
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
|
||||
let pickedusers: Promise<SeasonPickedUser[]> = $derived.by(async () =>
|
||||
(await data.seasonpickedusers).filter(
|
||||
@ -63,7 +93,7 @@
|
||||
<!-- Await this here so the accordion doesn't lag when opening -->
|
||||
<!-- Only show the stuff if signed in -->
|
||||
{#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
|
||||
? data.seasonpick.teamwinners
|
||||
.map((id: string) => get_by_value(drivers, "id", id) as Driver)
|
||||
@ -374,16 +404,29 @@
|
||||
<div class="ml-1 w-full min-w-36">
|
||||
<!-- Hottake -->
|
||||
<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 ?? "?"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if seasonpicks.length > 0}
|
||||
<!-- 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">
|
||||
<!-- NOTE: The containerstyle should be 64x64, don't know why that doesn't fit... (also below) -->
|
||||
<LazyImage
|
||||
@ -398,7 +441,14 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<LazyImage
|
||||
src={wccwinner?.banner_url ?? get_team_banner_template(data.graphics)}
|
||||
@ -412,7 +462,15 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<LazyImage
|
||||
src={mostovertakes?.headshot_url ?? get_driver_headshot_template(data.graphics)}
|
||||
@ -428,7 +486,15 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<LazyImage
|
||||
src={mostdnfs?.headshot_url ?? get_driver_headshot_template(data.graphics)}
|
||||
@ -442,7 +508,15 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
Jack Doohan startet <span class="font-bold">{pick?.doohanstarts ?? "?"}</span> mal.
|
||||
</div>
|
||||
@ -466,7 +540,16 @@
|
||||
style="color: {color}; background-color: {color};"
|
||||
>
|
||||
</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}
|
||||
</span>
|
||||
</div>
|
||||
@ -495,7 +578,15 @@
|
||||
style="color: {color}; background-color: {color};"
|
||||
>
|
||||
</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}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -6,11 +6,19 @@ import {
|
||||
fetch_visibleseasonpicks,
|
||||
fetch_teams,
|
||||
fetch_currentrace,
|
||||
fetch_seasonpickresults,
|
||||
} from "$lib/fetch";
|
||||
import type { PageLoad } from "../$types";
|
||||
|
||||
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 {
|
||||
teams: fetch_teams(fetch),
|
||||
@ -19,6 +27,7 @@ export const load: PageLoad = async ({ fetch, depends }) => {
|
||||
hottakes: fetch_hottakes(fetch),
|
||||
seasonpickedusers: fetch_seasonpickedusers(fetch),
|
||||
currentrace: fetch_currentrace(fetch), // Used for countdown
|
||||
seasonpickresults: fetch_seasonpickresults(fetch),
|
||||
|
||||
seasonpick: await fetch_currentseasonpick(fetch),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user