diff --git a/src/app.d.ts b/src/app.d.ts index d17213d..76e65b1 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -5,12 +5,7 @@ import type { PocketBase, RecordModel } from "pocketbase"; // for information about these interfaces declare global { namespace App { - interface Locals { - pb: PocketBase; - user: User | undefined; - admin: boolean; - } - + // interface Locals {} // interface Error {} // interface PageData {} // interface PageState {} diff --git a/src/hooks.server.ts b/src/hooks.server.ts deleted file mode 100644 index 848ad97..0000000 --- a/src/hooks.server.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { Graphic, User } from "$lib/schema"; -import type { Handle } from "@sveltejs/kit"; -import { env } from "$env/dynamic/private"; -import PocketBase from "pocketbase"; - -// This function will run serverside on each request. -// The event.locals will be passed onto serverside load functions and handlers. -// We create a new PocketBase client for each request, so it always carries the -// most recent authentication data. -// The authenticated PocketBase client will be available in all *.server.ts files. -export const handle: Handle = async ({ event, resolve }) => { - const requestStartTime: number = Date.now(); - - // If env variables are defined (e.g. in the prod environment), use those. - // Otherwise use the default local development IP:Port. - // Because we imported "$env/dynamic/private", - // the variables will only be available to the server (e.g. .server.ts files). - let pb_url: string = "http://192.168.86.50:8090"; - if (env.PB_PROTOCOL && env.PB_HOST && env.PB_PORT) { - pb_url = `${env.PB_PROTOCOL}://${env.PB_HOST}:${env.PB_PORT}`; - } - if (env.PB_PROTOCOL && env.PB_URL) { - pb_url = `${env.PB_PROTOCOL}://${env.PB_URL}`; - } - - event.locals.pb = new PocketBase(pb_url); - - // Load the most recent authentication data from a cookie (is updated below) - event.locals.pb.authStore.loadFromCookie(event.request.headers.get("cookie") || ""); - - if (event.locals.pb.authStore.isValid) { - // If the authentication data is valid, we make a "user" object easily available. - event.locals.user = structuredClone(event.locals.pb.authStore.model) as User; - - if (event.locals.user) { - if (event.locals.pb.authStore.model.avatar) { - // Fill in the avatar URL - event.locals.user.avatar_url = event.locals.pb.files.getURL( - event.locals.pb.authStore.model, - event.locals.pb.authStore.model.avatar, - ); - } else { - // Fill in the driver_headshot_template URL if no avatar chosen - const driver_headshot_template: Graphic = await event.locals.pb - .collection("graphics") - .getFirstListItem('name="driver_headshot_template"'); - event.locals.user.avatar_url = event.locals.pb.files.getURL( - driver_headshot_template, - driver_headshot_template.file, - ); - } - - // Set admin status for easier access - event.locals.admin = event.locals.user.admin; - } - } else { - event.locals.user = undefined; - } - - // Resolve the request. This is what happens by default. - const response = await resolve(event); - - console.log( - "=====\n", - `Request Date: ${new Date(requestStartTime).toISOString()}\n`, - `Method: ${event.request.method}\n`, - `Path: ${event.url.pathname}\n`, - `Duration: ${Date.now() - requestStartTime}ms\n`, - `Status: ${response.status}`, - ); - - // Store the current authentication data to a cookie, so it can be loaded above. - response.headers.set("set-cookie", event.locals.pb.authStore.exportToCookie({ secure: false })); - - return response; -}; diff --git a/src/hooks.ts b/src/hooks.ts deleted file mode 100644 index b0814a6..0000000 --- a/src/hooks.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Reroute } from "@sveltejs/kit"; - -const rerouted: Record = {}; - -// NOTE: This does not change the browser's address bar (the route path)! -export const reroute: Reroute = ({ url }) => { - if (url.pathname in rerouted) { - return rerouted[url.pathname]; - } -}; diff --git a/src/lib/components/cards/DriverCard.svelte b/src/lib/components/cards/DriverCard.svelte index a119748..fd4d4d5 100644 --- a/src/lib/components/cards/DriverCard.svelte +++ b/src/lib/components/cards/DriverCard.svelte @@ -3,15 +3,19 @@ import { FileDropzone, getModalStore, + getToastStore, SlideToggle, type ModalStore, + type ToastStore, } from "@skeletonlabs/skeleton"; import { Button, Input, Card, Dropdown } from "$lib/components"; import type { Driver, SkeletonData } from "$lib/schema"; import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config"; import { team_dropdown_options } from "$lib/dropdown"; - import { enhance } from "$app/forms"; import { get_driver_headshot_template } from "$lib/database"; + import { get_error_toast } from "$lib/toast"; + import { pb } from "$lib/pocketbase"; + import { invalidateAll } from "$app/navigation"; interface DriverCardProps { /** Data passed from the page context */ @@ -31,14 +35,92 @@ driver = meta.driver; } + const toastStore: ToastStore = getToastStore(); + // Constants const labelwidth: string = "120px"; // Reactive state let required: boolean = $derived(!driver); let disabled: boolean = $derived(!data.admin); + let firstname_input_value: string = $state(driver?.firstname ?? ""); + let lastname_input_value: string = $state(driver?.lastname ?? ""); + let code_input_value: string = $state(driver?.code ?? ""); let team_select_value: string = $state(driver?.team ?? ""); + let headshot_file_value: FileList | undefined = $state(); let active_value: boolean = $state(driver?.active ?? true); + + // Database actions + // TODO: Headshot compression + const update_driver = (create?: boolean): (() => Promise) => { + const handler = async (): Promise => { + if (!firstname_input_value || firstname_input_value === "") { + toastStore.trigger(get_error_toast("Please enter a first name!")); + return; + } + if (!lastname_input_value || lastname_input_value === "") { + toastStore.trigger(get_error_toast("Please enter a last name!")); + return; + } + if (!code_input_value || code_input_value === "") { + toastStore.trigger(get_error_toast("Please enter a driver code!")); + return; + } + if (!team_select_value || team_select_value === "") { + toastStore.trigger(get_error_toast("Please select a team!")); + return; + } + + const driver_data = { + firstname: firstname_input_value, + lastname: lastname_input_value, + code: code_input_value, + team: team_select_value, + active: active_value, + headshot: + headshot_file_value && headshot_file_value.length === 1 + ? headshot_file_value[0] + : undefined, + }; + + try { + if (create) { + if (!headshot_file_value || headshot_file_value.length !== 1) { + toastStore.trigger(get_error_toast("Please upload a single driver headshot!")); + return; + } + await pb.collection("drivers").create(driver_data); + } else { + if (!driver?.id) { + toastStore.trigger(get_error_toast("Invalid driver id!")); + return; + } + await pb.collection("drivers").update(driver.id, driver_data); + } + invalidateAll(); + modalStore.close(); + } catch (error) { + toastStore.trigger(get_error_toast("" + error)); + } + }; + + return handler; + }; + + const delete_driver = async (): Promise => { + if (!driver?.id) { + toastStore.trigger(get_error_toast("Invalid driver id!")); + return; + } + + try { + await pb.collection("drivers").delete(driver.id); + invalidateAll(); + modalStore.close(); + } catch (error) { + toastStore.trigger(get_error_toast("" + error)); + } + }; {#await data.graphics then graphics} @@ -50,97 +132,84 @@ imgheight={DRIVER_HEADSHOT_HEIGHT} imgonclick={(event: Event) => modalStore.close()} > -
modalStore.close()} - > - - - {#if driver && !disabled} - - {/if} +
+ + + First Name + + + Last Name + + + Driver Code + -
- - - First Name - - + {#await data.teams then teams} + - Last Name - - - Driver Code - + Team + + {/await} - - {#await data.teams then teams} - + + + Upload Headshot + + + + +
+
+ - Team - - {/await} - - - - - Upload Headshot - - - - -
-
- -
- {#if driver} - - - {:else} - - {/if} + />
+ {#if driver} + + + {:else} + + {/if}
- +
{/await} diff --git a/src/lib/components/cards/RaceCard.svelte b/src/lib/components/cards/RaceCard.svelte index 2f472be..e594462 100644 --- a/src/lib/components/cards/RaceCard.svelte +++ b/src/lib/components/cards/RaceCard.svelte @@ -1,12 +1,20 @@ {#await data.graphics then graphics} @@ -65,139 +157,112 @@ imgheight={RACE_PICTOGRAM_HEIGHT} imgonclick={(event: Event) => modalStore.close()} > -
modalStore.close()} - > - - - {#if race && !disabled} - - {/if} +
+ + + Name + + + Step + + + PXX + -
- - - Name - - - Step - - - PXX - + + + SQuali + + + SRace + + + Quali + + + Race + - - - SQuali - - - SRace - - - Quali - - - Race - + + + + Upload Pictogram + + - - +
+ + {#if race} + - {#if race} - - - {:else} - - {/if} -
+ + {:else} + + {/if}
- +
{/await} diff --git a/src/lib/components/cards/RacePickCard.svelte b/src/lib/components/cards/RacePickCard.svelte index 2c3aec6..dc1e8f2 100644 --- a/src/lib/components/cards/RacePickCard.svelte +++ b/src/lib/components/cards/RacePickCard.svelte @@ -10,10 +10,17 @@ Substitution, } from "$lib/schema"; import { get_by_value, get_driver_headshot_template } from "$lib/database"; - import { getModalStore, type ModalStore } from "@skeletonlabs/skeleton"; + import { + getModalStore, + getToastStore, + type ModalStore, + type ToastStore, + } from "@skeletonlabs/skeleton"; import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config"; import { driver_dropdown_options } from "$lib/dropdown"; - import { enhance } from "$app/forms"; + import { get_error_toast } from "$lib/toast"; + import { invalidateAll } from "$app/navigation"; + import { pb } from "$lib/pocketbase"; interface RacePickCardProps { /** Data passed from the page context */ @@ -38,6 +45,8 @@ racepick = meta.racepick; } + const toastStore: ToastStore = getToastStore(); + // Await promises let drivers: Driver[] | undefined = $state(undefined); data.drivers.then((d: Driver[]) => (drivers = d)); @@ -96,6 +105,68 @@ Math.floor(Math.random() * active_drivers_and_substitutes.length) ].id; }; + + // Database actions + const update_racepick = (create?: boolean): (() => Promise) => { + const handler = async (): Promise => { + if (!data.user?.id || data.user.id === "") { + toastStore.trigger(get_error_toast("Invalid user id!")); + return; + } + if (!data.currentrace?.id || data.currentrace.id === "") { + toastStore.trigger(get_error_toast("Invalid race id!")); + return; + } + if (!pxx_select_value || pxx_select_value === "") { + toastStore.trigger(get_error_toast("Please enter a PXX guess!")); + return; + } + if (!dnf_select_value || dnf_select_value === "") { + toastStore.trigger(get_error_toast("Please enter a DNF guess!")); + return; + } + + const racepick_data = { + user: data.user.id, + race: data.currentrace.id, + pxx: pxx_select_value, + dnf: dnf_select_value, + }; + + try { + if (create) { + await pb.collection("racepicks").create(racepick_data); + } else { + if (!racepick?.id) { + toastStore.trigger(get_error_toast("Invalid racepick id!")); + return; + } + await pb.collection("racepicks").update(racepick.id, racepick_data); + } + invalidateAll(); + modalStore.close(); + } catch (error) { + toastStore.trigger(get_error_toast("" + error)); + } + }; + + return handler; + }; + + const delete_racepick = async (): Promise => { + if (!racepick?.id) { + toastStore.trigger(get_error_toast("Invalid racepick id!")); + return; + } + + try { + await pb.collection("racepicks").delete(racepick.id); + invalidateAll(); + modalStore.close(); + } catch (error) { + toastStore.trigger(get_error_toast("" + error)); + } + }; {#await Promise.all([data.graphics, data.drivers]) then [graphics, drivers]} @@ -108,78 +179,46 @@ imgheight={DRIVER_HEADSHOT_HEIGHT} imgonclick={(event: Event) => modalStore.close()} > -
modalStore.close()} - > - - - {#if racepick && !disabled} - - {/if} +
+ + + P{data.currentrace?.pxx ?? "XX"} + - - + + + DNF + -
- - - P{data.currentrace?.pxx ?? "XX"} - + - - - DNF - - - - - -
- {#if racepick} - - - {:else} - - {/if} -
+ +
+ {#if racepick} + + + {:else} + + {/if}
- +
{/await} diff --git a/src/lib/components/cards/RaceResultCard.svelte b/src/lib/components/cards/RaceResultCard.svelte index eac1839..195f1c0 100644 --- a/src/lib/components/cards/RaceResultCard.svelte +++ b/src/lib/components/cards/RaceResultCard.svelte @@ -2,15 +2,19 @@ import { Autocomplete, getModalStore, + getToastStore, InputChip, type AutocompleteOption, type ModalStore, + type ToastStore, } from "@skeletonlabs/skeleton"; import { Button, Card, Dropdown } from "$lib/components"; import type { Driver, Race, RaceResult, SkeletonData } from "$lib/schema"; import { get_by_value } from "$lib/database"; import { race_dropdown_options } from "$lib/dropdown"; - import { enhance } from "$app/forms"; + import { pb } from "$lib/pocketbase"; + import { get_error_toast } from "$lib/toast"; + import { invalidateAll } from "$app/navigation"; interface RaceResultCardProps { /** Data passed from the page context */ @@ -30,6 +34,8 @@ result = meta.result; } + const toastStore: ToastStore = getToastStore(); + let races: Race[] | undefined = $state(undefined); data.races.then((r: Race[]) => (races = r)); @@ -166,108 +172,132 @@ const on_dnfs_chip_remove = (event: CustomEvent): void => { dnfs_ids.splice(event.detail.chipIndex, 1); }; + + // Database actions + const update_raceresult = (create?: boolean): (() => Promise) => { + const handler = async (): Promise => { + if (!race_select_value || race_select_value === "") { + toastStore.trigger(get_error_toast("Please select a race!")); + return; + } + if (!pxxs_ids || pxxs_ids.length !== 7) { + toastStore.trigger(get_error_toast("Please select all 7 driver placements!")); + return; + } + + const raceresult_data = { + race: race_select_value, + pxxs: pxxs_ids, + dnfs: dnfs_ids, + }; + + try { + if (create) { + await pb.collection("raceresults").create(raceresult_data); + } else { + if (!result?.id) { + toastStore.trigger(get_error_toast("Invalid result id!")); + return; + } + + await pb.collection("raceresults").update(result.id, raceresult_data); + } + invalidateAll(); + modalStore.close(); + } catch (error) { + toastStore.trigger(get_error_toast("" + error)); + } + }; + + return handler; + }; + + const delete_raceresult = async (): Promise => { + if (!result?.id) { + toastStore.trigger(get_error_toast("Invalid result id!")); + return; + } + + try { + await pb.collection("raceresults").delete(result.id); + invalidateAll(); + modalStore.close(); + } catch (error) { + toastStore.trigger(get_error_toast("" + error)); + } + }; -
modalStore.close()}> - - - {#if result && !disabled} - - {/if} + + {#await data.races then races} + + Race + + {/await} - - {#each pxxs_ids as pxxs_id} - - {/each} - {#each dnfs_ids as dnfs_id} - - {/each} - - - {#await data.races then races} - - Race - - {/await} - -
- - + + +
+ -
- -
- - - -
- -
- - -
- {#if result} - - - {:else} - - {/if} -
- + + + +
+ +
+ + +
+ {#if result} + + + {:else} + + {/if} +
+
diff --git a/src/lib/components/cards/SubstitutionCard.svelte b/src/lib/components/cards/SubstitutionCard.svelte index 7f4ecb9..b387f94 100644 --- a/src/lib/components/cards/SubstitutionCard.svelte +++ b/src/lib/components/cards/SubstitutionCard.svelte @@ -2,10 +2,17 @@ import { Card, Button, Dropdown } from "$lib/components"; import type { Driver, SkeletonData, Substitution } from "$lib/schema"; import { get_by_value, get_driver_headshot_template } from "$lib/database"; - import { getModalStore, type ModalStore } from "@skeletonlabs/skeleton"; + import { + getModalStore, + getToastStore, + type ModalStore, + type ToastStore, + } from "@skeletonlabs/skeleton"; import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config"; import { driver_dropdown_options, race_dropdown_options } from "$lib/dropdown"; - import { enhance } from "$app/forms"; + import { get_error_toast } from "$lib/toast"; + import { pb } from "$lib/pocketbase"; + import { invalidateAll } from "$app/navigation"; interface SubstitutionCardProps { /** Data passed from the page context */ @@ -25,6 +32,8 @@ substitution = meta.substitution; } + const toastStore: ToastStore = getToastStore(); + // Await promises let drivers: Driver[] | undefined = $state(undefined); data.drivers.then((d: Driver[]) => (drivers = d)); @@ -48,6 +57,62 @@ const img: HTMLImageElement = document.getElementById("headshot_preview") as HTMLImageElement; if (img) img.src = src; }); + + // Database actions + const update_substitution = (create?: boolean): (() => Promise) => { + const handler = async (): Promise => { + if (!substitute_value || substitute_value === "") { + toastStore.trigger(get_error_toast("Please select a substitute driver!")); + return; + } + if (!driver_value || driver_value === "") { + toastStore.trigger(get_error_toast("Please select a replaced driver!")); + return; + } + if (!race_value || race_value === "") { + toastStore.trigger(get_error_toast("Please select a race!")); + return; + } + + const substitution_data = { + substitute: substitute_value, + for: driver_value, + race: race_value, + }; + + try { + if (create) { + await pb.collection("substitutions").create(substitution_data); + } else { + if (!substitution?.id) { + toastStore.trigger(get_error_toast("Invalid substitution id!")); + return; + } + await pb.collection("substitutions").update(substitution.id, substitution_data); + } + invalidateAll(); + modalStore.close(); + } catch (error) { + toastStore.trigger(get_error_toast("" + error)); + } + }; + return handler; + }; + + const delete_substitution = async (): Promise => { + if (!substitution?.id) { + toastStore.trigger(get_error_toast("Invalid substitution id!")); + return; + } + + try { + await pb.collection("substitutions").delete(substitution.id); + invalidateAll(); + modalStore.close(); + } catch (error) { + toastStore.trigger(get_error_toast("" + error)); + } + }; {#await Promise.all([data.graphics, data.drivers]) then [graphics, drivers]} @@ -60,91 +125,57 @@ imgheight={DRIVER_HEADSHOT_HEIGHT} imgonclick={(event: Event) => modalStore.close()} > -
modalStore.close()} - > - - - {#if substitution && !disabled} - - {/if} +
+ + + Substitute + -
- + + + For + + + + {#await data.races then races} - Substitute + Race + {/await} - - - For - - - - {#await data.races then races} - - Race - - {/await} - - -
- {#if substitution} - - - {:else} - - {/if} -
+ +
+ {#if substitution} + + + {:else} + + {/if}
- +
{/await} diff --git a/src/lib/components/cards/TeamCard.svelte b/src/lib/components/cards/TeamCard.svelte index 4043059..c084e26 100644 --- a/src/lib/components/cards/TeamCard.svelte +++ b/src/lib/components/cards/TeamCard.svelte @@ -1,11 +1,19 @@ {#await data.graphics then graphics} @@ -44,97 +119,74 @@ imgheight={TEAM_BANNER_HEIGHT} imgonclick={(event: Event) => modalStore.close()} > -
modalStore.close()} - > - - - {#if team && !disabled} - - {/if} +
+ + + Name + -
- - - Name - + + + Color + + C + + - - - Color - - C - - + + + + Upload Banner + + - - - - Upload Banner - - + + + +
+ Upload Logo + +
+
+
- - - -
- Upload Logo - -
-
-
- - -
- {#if team} - - - {:else} - - {/if} -
+ +
+ {#if team} + + + {:else} + + {/if}
- +
{/await} diff --git a/src/lib/form.ts b/src/lib/form.ts deleted file mode 100644 index a31007e..0000000 --- a/src/lib/form.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { error } from "@sveltejs/kit"; - -/** - * Obtain the value of the key "id" and remove it from the FormData. - * Throws SvelteKit error(400) if "id" is not found. - */ -export const form_data_get_and_remove_id = (data: FormData): string => { - const id: string | undefined = data.get("id")?.toString(); - if (!id) error(400, "Missing ID"); - data.delete("id"); - return id; -}; - -/** - * Remove empty fields (even whitespace) and empty files from FormData objects. - * Keys listed in [except] won't be removed although they are empty. - */ -export const form_data_clean = (data: FormData, except: string[] = []): FormData => { - let delete_keys: string[] = []; - - for (const [key, value] of data.entries()) { - if ( - !except.includes(key) && - (value === null || - value === undefined || - (typeof value === "string" && value.trim() === "") || - (typeof value === "object" && "size" in value && value.size === 0)) - ) { - delete_keys.push(key); - } - } - - delete_keys.forEach((key) => { - data.delete(key); - }); - - return data; -}; - -/** - * Remove the specified [keys] from the [data] object. - */ -export const form_data_remove = (data: FormData, keys: string[]): void => { - let delete_keys: string[] = []; - - for (const [key, value] of data.entries()) { - if (keys.includes(key)) { - delete_keys.push(key); - } - } - - delete_keys.forEach((key) => { - data.delete(key); - }); -}; - -/** - * Throws SvelteKit error(400) if form_data does not contain key. - */ -export const form_data_ensure_key = (data: FormData, key: string): void => { - if (!data.get(key)) error(400, `Key "${key}" missing from form_data!`); -}; - -/** - * Throws SvelteKit error(400) if form_data does not contain all keys. - */ -export const form_data_ensure_keys = (data: FormData, keys: string[]): void => { - keys.map((key) => form_data_ensure_key(data, key)); -}; - -/** - * Modify a single [FormData] element to satisfy PocketBase's date format. - * Date format: 2024-12-31 12:00:00.000Z - */ -export const form_data_fix_date = (data: FormData, key: string): boolean => { - const value: string | undefined = data.get(key)?.toString(); - if (!value) return false; - - const date: string = new Date(value).toISOString(); - data.set(key, date); - - return true; -}; - -export const form_data_fix_dates = (data: FormData, keys: string[]): boolean[] => { - return keys.map((key) => form_data_fix_date(data, key)); -}; diff --git a/src/lib/pocketbase.ts b/src/lib/pocketbase.ts new file mode 100644 index 0000000..8e27c8a --- /dev/null +++ b/src/lib/pocketbase.ts @@ -0,0 +1,39 @@ +import Pocketbase, { type AuthRecord } from "pocketbase"; +import type { Graphic, User } from "$lib/schema"; +// TODO: import { PUBLIC_POCKETBASE_URL } from '$env/static/public'; + +export let pb = new Pocketbase("http://192.168.86.50:8090"); +export let pbUser: User | undefined = undefined; + +const update_user = async (record: AuthRecord): Promise => { + if (!record) { + pbUser = undefined; + return; + } + + let avatar_url: string; + if (record.avatar) { + avatar_url = pb.files.getURL(record, record.avatar); + } else { + const driver_headshot_template: Graphic = await pb + .collection("graphics") + .getFirstListItem('name="driver_headshot_template"'); + avatar_url = pb.files.getURL(driver_headshot_template, driver_headshot_template.file); + } + + pbUser = { + id: record.id, + username: record.username, + firstname: record.firstname, + avatar: record.avatar, + avatar_url: avatar_url, + admin: record.admin, + } as User; +}; + +// Update the pbUser object when authStore changes (e.g. after logging in) +pb.authStore.onChange(() => { + update_user(pb.authStore.record); + // console.log("Updating pbUser...") + // console.dir(pbUser, { depth: null }); +}, true); diff --git a/src/lib/toast.ts b/src/lib/toast.ts new file mode 100644 index 0000000..2f0a325 --- /dev/null +++ b/src/lib/toast.ts @@ -0,0 +1,10 @@ +import type { ToastSettings } from "@skeletonlabs/skeleton"; + +export const get_error_toast = (message: string): ToastSettings => { + return { + message, + hideDismiss: true, + timeout: 2000, + background: "variant-filled-primary", + }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 6b4127c..0e9d35c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,10 +1,8 @@ + + {#if $drawerStore.id === "menu_drawer"} @@ -181,39 +268,29 @@

Enter Username and Password

-
- - - - - - - - - - - -
- - -
- + + + + + + + + + +
+ + +
{:else if $drawerStore.id === "profile_drawer" && data.user} @@ -221,56 +298,30 @@

Edit Profile

-
- - - - - - - - - - - Upload Avatar - -
- - -
- + + + + + + + + + Upload Avatar + + +
+ + +
{/if}
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.ts similarity index 62% rename from src/routes/+layout.server.ts rename to src/routes/+layout.ts index ac0c404..feb09ee 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.ts @@ -1,66 +1,69 @@ +import { pb, pbUser } from "$lib/pocketbase"; + +// This makes the page client-side rendered +export const ssr = false; + import type { Driver, Graphic, Race, Substitution, Team } from "$lib/schema"; -import type { LayoutServerLoad } from "./$types"; +import type { LayoutLoad } from "./$types"; // On each page load (every route), this function runs serverside. // The "locals.user" object is only available on the server, // since it's populated inside hooks.server.ts per request. // It will populate the "user" attribute of each page's "data" object, // so each page has access to the current user (or knows if no one is signed in). -export const load: LayoutServerLoad = ({ locals }) => { +export const load: LayoutLoad = () => { const fetch_graphics = async (): Promise => { - const graphics: Graphic[] = await locals.pb - .collection("graphics") - .getFullList({ fetch: fetch }); + const graphics: Graphic[] = await pb.collection("graphics").getFullList({ fetch: fetch }); graphics.map((graphic: Graphic) => { - graphic.file_url = locals.pb.files.getURL(graphic, graphic.file); + graphic.file_url = pb.files.getURL(graphic, graphic.file); }); return graphics; }; const fetch_teams = async (): Promise => { - const teams: Team[] = await locals.pb.collection("teams").getFullList({ + const teams: Team[] = await pb.collection("teams").getFullList({ sort: "+name", fetch: fetch, }); teams.map((team: Team) => { - team.banner_url = locals.pb.files.getURL(team, team.banner); - team.logo_url = locals.pb.files.getURL(team, team.logo); + team.banner_url = pb.files.getURL(team, team.banner); + team.logo_url = pb.files.getURL(team, team.logo); }); return teams; }; const fetch_drivers = async (): Promise => { - const drivers: Driver[] = await locals.pb.collection("drivers").getFullList({ + const drivers: Driver[] = await pb.collection("drivers").getFullList({ sort: "+code", fetch: fetch, }); drivers.map((driver: Driver) => { - driver.headshot_url = locals.pb.files.getURL(driver, driver.headshot); + driver.headshot_url = pb.files.getURL(driver, driver.headshot); }); return drivers; }; const fetch_races = async (): Promise => { - const races: Race[] = await locals.pb.collection("races").getFullList({ + const races: Race[] = await pb.collection("races").getFullList({ sort: "+step", fetch: fetch, }); races.map((race: Race) => { - race.pictogram_url = locals.pb.files.getURL(race, race.pictogram); + race.pictogram_url = pb.files.getURL(race, race.pictogram); }); return races; }; const fetch_substitutions = async (): Promise => { - const substitutions: Substitution[] = await locals.pb.collection("substitutions").getFullList({ + const substitutions: Substitution[] = await pb.collection("substitutions").getFullList({ expand: "race", fetch: fetch, }); @@ -75,8 +78,8 @@ export const load: LayoutServerLoad = ({ locals }) => { return { // User information - user: locals.user, - admin: locals.user?.admin ?? false, + user: pbUser, + admin: pbUser?.admin ?? false, // Return static data asynchronously graphics: fetch_graphics(), diff --git a/src/routes/data/raceresults/+page.server.ts b/src/routes/data/raceresults/+page.server.ts deleted file mode 100644 index a929134..0000000 --- a/src/routes/data/raceresults/+page.server.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - form_data_clean, - form_data_ensure_keys, - form_data_get_and_remove_id, - form_data_remove, -} from "$lib/form"; -import type { Driver, Graphic, Race, RaceResult } from "$lib/schema"; -import type { Actions, PageServerLoad } from "./$types"; - -export const actions = { - create_raceresult: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - form_data_ensure_keys(data, ["race", "pxxs"]); - form_data_remove(data, ["pxxs_codes", "dnfs_codes"]); - - await locals.pb.collection("raceresults").create(data); - }, - - update_raceresult: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - form_data_remove(data, ["pxxs_codes", "dnfs_codes"]); - const id: string = form_data_get_and_remove_id(data); - - console.dir(data, { depth: null }); - - await locals.pb.collection("raceresults").update(id, data); - }, - - delete_raceresult: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - form_data_remove(data, ["pxxs_codes", "dnfs_codes"]); - const id: string = form_data_get_and_remove_id(data); - - await locals.pb.collection("raceresults").delete(id); - }, -} as Actions; - -export const load: PageServerLoad = async ({ fetch, locals }) => { - // TODO: Duplicated code from racepicks/+page.server.ts - const fetch_raceresults = async (): Promise => { - const raceresults: RaceResult[] = await locals.pb.collection("raceresultsdesc").getFullList(); - - return raceresults; - }; - - return { - results: await fetch_raceresults(), - }; -}; diff --git a/src/routes/data/raceresults/+page.ts b/src/routes/data/raceresults/+page.ts new file mode 100644 index 0000000..91c36fb --- /dev/null +++ b/src/routes/data/raceresults/+page.ts @@ -0,0 +1,18 @@ +import { pb } from "$lib/pocketbase"; +import type { RaceResult } from "$lib/schema"; +import type { PageLoad } from "../../$types"; + +export const load: PageLoad = async ({ fetch }) => { + // TODO: Duplicated code from racepicks/+page.server.ts + const fetch_raceresults = async (): Promise => { + const raceresults: RaceResult[] = await pb + .collection("raceresultsdesc") + .getFullList({ fetch: fetch }); + + return raceresults; + }; + + return { + results: await fetch_raceresults(), + }; +}; diff --git a/src/routes/data/season/drivers/+page.server.ts b/src/routes/data/season/drivers/+page.server.ts deleted file mode 100644 index 1e5595c..0000000 --- a/src/routes/data/season/drivers/+page.server.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config"; -import { form_data_clean, form_data_ensure_keys, form_data_get_and_remove_id } from "$lib/form"; -import { image_to_avif } from "$lib/server/image"; -import type { Actions } from "@sveltejs/kit"; - -export const actions = { - create_driver: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - form_data_ensure_keys(data, ["firstname", "lastname", "code", "team", "headshot"]); - - // The toggle switch will report "on" or nothing - data.set("active", data.has("active") ? "true" : "false"); - - // Compress headshot - const headshot_avif: Blob = await image_to_avif( - await (data.get("headshot") as File).arrayBuffer(), - DRIVER_HEADSHOT_WIDTH, - DRIVER_HEADSHOT_HEIGHT, - ); - - data.set("headshot", headshot_avif); - - await locals.pb.collection("drivers").create(data); - }, - - update_driver: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - const id: string = form_data_get_and_remove_id(data); - - // The toggle switch will report "on" or nothing - data.set("active", data.has("active") ? "true" : "false"); - - if (data.has("headshot")) { - // Compress headshot - const headshot_avif: Blob = await image_to_avif( - await (data.get("headshot") as File).arrayBuffer(), - DRIVER_HEADSHOT_WIDTH, - DRIVER_HEADSHOT_HEIGHT, - ); - - data.set("headshot", headshot_avif); - } - - await locals.pb.collection("drivers").update(id, data); - }, - - delete_driver: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - const id: string = form_data_get_and_remove_id(data); - - await locals.pb.collection("drivers").delete(id); - }, -} satisfies Actions; diff --git a/src/routes/data/season/races/+page.server.ts b/src/routes/data/season/races/+page.server.ts deleted file mode 100644 index d4acb43..0000000 --- a/src/routes/data/season/races/+page.server.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { RACE_PICTOGRAM_HEIGHT, RACE_PICTOGRAM_WIDTH } from "$lib/config"; -import { - form_data_clean, - form_data_ensure_keys, - form_data_fix_dates, - form_data_get_and_remove_id, -} from "$lib/form"; -import { image_to_avif } from "$lib/server/image"; -import type { Actions } from "@sveltejs/kit"; - -export const actions = { - create_race: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - form_data_ensure_keys(data, ["name", "step", "pictogram", "pxx", "qualidate", "racedate"]); - form_data_fix_dates(data, ["sprintqualidate", "sprintdate", "qualidate", "racedate"]); - - // Compress pictogram - const pictogram_avif: Blob = await image_to_avif( - await (data.get("pictogram") as File).arrayBuffer(), - RACE_PICTOGRAM_WIDTH, - RACE_PICTOGRAM_HEIGHT, - ); - - data.set("pictogram", pictogram_avif); - - await locals.pb.collection("races").create(data); - }, - - update_race: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - // Do not remove empty sprint dates so they can be cleared by updating the record - const data: FormData = form_data_clean(await request.formData(), [ - "sprintqualidate", - "sprintdate", - ]); - form_data_fix_dates(data, ["sprintqualidate", "sprintdate", "qualidate", "racedate"]); - const id: string = form_data_get_and_remove_id(data); - - if (data.has("pictogram")) { - // Compress pictogram - const pictogram_avif: Blob = await image_to_avif( - await (data.get("pictogram") as File).arrayBuffer(), - RACE_PICTOGRAM_WIDTH, - RACE_PICTOGRAM_HEIGHT, - ); - - data.set("pictogram", pictogram_avif); - } - - await locals.pb.collection("races").update(id, data); - }, - - delete_race: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - const id: string = form_data_get_and_remove_id(data); - - await locals.pb.collection("races").delete(id); - }, -} satisfies Actions; diff --git a/src/routes/data/season/races/+page.svelte b/src/routes/data/season/races/+page.svelte index 597a6e8..6099d4c 100644 --- a/src/routes/data/season/races/+page.svelte +++ b/src/routes/data/season/races/+page.svelte @@ -26,6 +26,7 @@ modalStore.trigger(modalSettings); }; + // TODO: Displayed dates differ from actual dates by 1 hour const races_columns: TableColumn[] = $derived([ { data_value_name: "name", diff --git a/src/routes/data/season/substitutions/+page.server.ts b/src/routes/data/season/substitutions/+page.server.ts deleted file mode 100644 index 37fef2c..0000000 --- a/src/routes/data/season/substitutions/+page.server.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { form_data_clean, form_data_ensure_keys, form_data_get_and_remove_id } from "$lib/form"; -import type { Actions } from "@sveltejs/kit"; - -export const actions = { - create_substitution: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - form_data_ensure_keys(data, ["substitute", "for", "race"]); - - await locals.pb.collection("substitutions").create(data); - }, - - update_substitution: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - const id: string = form_data_get_and_remove_id(data); - - await locals.pb.collection("substitutions").update(id, data); - }, - - delete_substitution: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - const id: string = form_data_get_and_remove_id(data); - - await locals.pb.collection("substitutions").delete(id); - }, -} satisfies Actions; diff --git a/src/routes/data/season/teams/+page.server.ts b/src/routes/data/season/teams/+page.server.ts deleted file mode 100644 index d3b8d4f..0000000 --- a/src/routes/data/season/teams/+page.server.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { Actions } from "./$types"; -import { form_data_clean, form_data_ensure_keys, form_data_get_and_remove_id } from "$lib/form"; -import { image_to_avif } from "$lib/server/image"; -import { - TEAM_BANNER_HEIGHT, - TEAM_BANNER_WIDTH, - TEAM_LOGO_HEIGHT, - TEAM_LOGO_WIDTH, -} from "$lib/config"; - -export const actions = { - create_team: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - form_data_ensure_keys(data, ["name", "banner", "logo", "color"]); - - // Compress banner - const banner_avif: Blob = await image_to_avif( - await (data.get("banner") as File).arrayBuffer(), - TEAM_BANNER_WIDTH, - TEAM_BANNER_HEIGHT, - ); - data.set("banner", banner_avif); - - // Compress logo - const logo_avif: Blob = await image_to_avif( - await (data.get("logo") as File).arrayBuffer(), - TEAM_LOGO_WIDTH, - TEAM_LOGO_HEIGHT, - ); - data.set("logo", logo_avif); - - await locals.pb.collection("teams").create(data); - }, - - update_team: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - const id: string = form_data_get_and_remove_id(data); - - if (data.has("banner")) { - // Compress banner - const banner_avif: Blob = await image_to_avif( - await (data.get("banner") as File).arrayBuffer(), - TEAM_BANNER_WIDTH, - TEAM_BANNER_HEIGHT, - ); - data.set("banner", banner_avif); - } - - if (data.has("logo")) { - // Compress logo - const logo_avif: Blob = await image_to_avif( - await (data.get("logo") as File).arrayBuffer(), - TEAM_LOGO_WIDTH, - TEAM_LOGO_HEIGHT, - ); - data.set("logo", logo_avif); - } - - await locals.pb.collection("teams").update(id, data); - }, - - delete_team: async ({ request, locals }) => { - if (!locals.admin) return { unauthorized: true }; - - const data: FormData = form_data_clean(await request.formData()); - const id: string = form_data_get_and_remove_id(data); - - await locals.pb.collection("teams").delete(id); - }, -} satisfies Actions; diff --git a/src/routes/data/users/+page.server.ts b/src/routes/data/users/+page.server.ts deleted file mode 100644 index 76aad0c..0000000 --- a/src/routes/data/users/+page.server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Graphic, User } from "$lib/schema"; -import type { PageServerLoad } from "./$types"; - -export const load: PageServerLoad = async ({ fetch, locals }) => { - const fetch_users = async (): Promise => { - const users: User[] = await locals.pb - .collection("users") - .getFullList({ fetch: fetch, sort: "+username" }); - - users.map((user: User) => { - user.avatar_url = locals.pb.files.getURL(user, user.avatar); - }); - - return users; - }; - - return { - users: await fetch_users(), - }; -}; diff --git a/src/routes/data/users/+page.ts b/src/routes/data/users/+page.ts new file mode 100644 index 0000000..327d318 --- /dev/null +++ b/src/routes/data/users/+page.ts @@ -0,0 +1,21 @@ +import { pb } from "$lib/pocketbase"; +import type { User } from "$lib/schema"; +import type { PageLoad } from "../../$types"; + +export const load: PageLoad = async ({ fetch }) => { + const fetch_users = async (): Promise => { + const users: User[] = await pb + .collection("users") + .getFullList({ fetch: fetch, sort: "+username" }); + + users.map((user: User) => { + user.avatar_url = pb.files.getURL(user, user.avatar); + }); + + return users; + }; + + return { + users: await fetch_users(), + }; +}; diff --git a/src/routes/profile/+page.server.ts b/src/routes/profile/+page.server.ts deleted file mode 100644 index 533fa4c..0000000 --- a/src/routes/profile/+page.server.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { form_data_clean, form_data_ensure_keys, form_data_get_and_remove_id } from "$lib/form"; -import { error, redirect } from "@sveltejs/kit"; -import type { Actions } from "./$types"; -import { image_to_avif } from "$lib/server/image"; -import { AVATAR_HEIGHT, AVATAR_WIDTH } from "$lib/config"; - -export const actions = { - create_profile: async ({ request, locals }): Promise => { - const data: FormData = form_data_clean(await request.formData()); - form_data_ensure_keys(data, ["username", "firstname", "password", "redirect_url"]); - - // Confirm password lol - await locals.pb.collection("users").create({ - username: data.get("username")?.toString(), - firstname: data.get("firstname")?.toString(), - password: data.get("password")?.toString(), - passwordConfirm: data.get("password")?.toString(), - admin: false, - }); - - // Directly login after registering - await locals.pb - .collection("users") - .authWithPassword(data.get("username")?.toString(), data.get("password")?.toString()); - - // The current page is sent with the form, redirect to that page - redirect(303, data.get("redirect_url")?.toString() ?? "/"); - }, - - // TODO: PocketBase API rule: Only the active user should be able to modify itself - update_profile: async ({ request, locals }): Promise => { - const data: FormData = form_data_clean(await request.formData()); - form_data_ensure_keys(data, ["redirect_url"]); - const id: string = form_data_get_and_remove_id(data); - - if (data.has("avatar")) { - // Compress image - const compressed: Blob = await image_to_avif( - await (data.get("avatar") as File).arrayBuffer(), - AVATAR_WIDTH, - AVATAR_HEIGHT, - ); - - // At most 20kB - if (compressed.size > 20000) { - error(400, "Avatar too large!"); - } - - data.set("avatar", compressed); - } - - await locals.pb.collection("users").update(id, data); - - redirect(303, data.get("redirect_url")?.toString() ?? "/"); - }, - - login: async ({ request, locals }) => { - if (locals.user) { - error(400, "Already logged in!"); - } - - const data: FormData = form_data_clean(await request.formData()); - form_data_ensure_keys(data, ["username", "password", "redirect_url"]); - - try { - await locals.pb - .collection("users") - .authWithPassword(data.get("username")?.toString(), data.get("password")?.toString()); - } catch (err) { - error(400, "Failed to login!"); - } - - redirect(303, data.get("redirect_url")?.toString() ?? "/"); - }, - - logout: async ({ request, locals }) => { - if (!locals.user) { - error(400, "Not logged in!"); - } - - const data: FormData = form_data_clean(await request.formData()); - form_data_ensure_keys(data, ["redirect_url"]); - - locals.pb.authStore.clear(); - locals.user = undefined; - - redirect(303, data.get("redirect_url")?.toString() ?? "/"); - }, -} satisfies Actions; diff --git a/src/routes/racepicks/+page.server.ts b/src/routes/racepicks/+page.server.ts deleted file mode 100644 index 3409c36..0000000 --- a/src/routes/racepicks/+page.server.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { form_data_clean, form_data_ensure_keys, form_data_get_and_remove_id } from "$lib/form"; -import type { CurrentPickedUser, Race, RacePick, RaceResult } from "$lib/schema"; -import type { Actions, PageServerLoad } from "./$types"; - -export const actions = { - create_racepick: async ({ request, locals }) => { - const data: FormData = form_data_clean(await request.formData()); - form_data_ensure_keys(data, ["user", "race", "pxx", "dnf"]); - - if (locals.user?.id !== data.get("user")) return { unauthorized: true }; - - await locals.pb.collection("racepicks").create(data); - }, - - update_racepick: async ({ request, locals }) => { - const data: FormData = form_data_clean(await request.formData()); - form_data_ensure_keys(data, ["user", "race"]); - const id: string = form_data_get_and_remove_id(data); - - if (locals.user?.id !== data.get("user")) return { unauthorized: true }; - - await locals.pb.collection("racepicks").update(id, data); - }, - - delete_racepick: async ({ request, locals }) => { - const data: FormData = form_data_clean(await request.formData()); - form_data_ensure_keys(data, ["user", "race"]); - const id: string = form_data_get_and_remove_id(data); - - if (locals.user?.id !== data.get("user")) return { unauthorized: true }; - - await locals.pb.collection("racepicks").delete(id); - }, -} satisfies Actions; - -export const load: PageServerLoad = async ({ fetch, locals }) => { - const fetch_currentrace = async (): Promise => { - const currentrace: Race[] = await locals.pb.collection("currentrace").getFullList(); - - // The currentrace collection either has a single or no entries - if (currentrace.length == 0) return null; - - currentrace[0].pictogram_url = await locals.pb.files.getURL( - currentrace[0], - currentrace[0].pictogram, - ); - - return currentrace[0]; - }; - - const fetch_racepicks = async (): Promise => { - // Don't expand race/pxx/dnf since we already fetched those - const racepicks: RacePick[] = await locals.pb - .collection("racepicks") - .getFullList({ fetch: fetch, expand: "user" }); - - return racepicks; - }; - - const fetch_currentpickedusers = async (): Promise => { - const currentpickedusers: CurrentPickedUser[] = await locals.pb - .collection("currentpickedusers") - .getFullList(); - - currentpickedusers.map((currentpickeduser: CurrentPickedUser) => { - if (currentpickeduser.avatar) { - currentpickeduser.avatar_url = locals.pb.files.getURL( - currentpickeduser, - currentpickeduser.avatar, - ); - } - }); - - return currentpickedusers; - }; - - // TODO: Duplicated code from data/raceresults/+page.server.ts - const fetch_raceresults = async (): Promise => { - // Don't expand races/pxxs/dnfs since we already fetched those - const raceresults: RaceResult[] = await locals.pb.collection("raceresultsdesc").getFullList(); - - return raceresults; - }; - - return { - racepicks: fetch_racepicks(), - currentpickedusers: fetch_currentpickedusers(), - raceresults: fetch_raceresults(), - - currentrace: await fetch_currentrace(), - }; -}; diff --git a/src/routes/racepicks/+page.ts b/src/routes/racepicks/+page.ts new file mode 100644 index 0000000..c7cc16d --- /dev/null +++ b/src/routes/racepicks/+page.ts @@ -0,0 +1,57 @@ +import { pb } from "$lib/pocketbase"; +import type { CurrentPickedUser, Race, RacePick, RaceResult } from "$lib/schema"; +import type { PageLoad } from "../$types"; + +export const load: PageLoad = async ({ fetch }) => { + const fetch_currentrace = async (): Promise => { + const currentrace: Race[] = await pb.collection("currentrace").getFullList({ fetch: fetch }); + + // The currentrace collection either has a single or no entries + if (currentrace.length == 0) return null; + + currentrace[0].pictogram_url = pb.files.getURL(currentrace[0], currentrace[0].pictogram); + + return currentrace[0]; + }; + + const fetch_racepicks = async (): Promise => { + // Don't expand race/pxx/dnf since we already fetched those + const racepicks: RacePick[] = await pb + .collection("racepicks") + .getFullList({ fetch: fetch, expand: "user" }); + + return racepicks; + }; + + const fetch_currentpickedusers = async (): Promise => { + const currentpickedusers: CurrentPickedUser[] = await pb + .collection("currentpickedusers") + .getFullList({ fetch: fetch }); + + currentpickedusers.map((currentpickeduser: CurrentPickedUser) => { + if (currentpickeduser.avatar) { + currentpickeduser.avatar_url = pb.files.getURL(currentpickeduser, currentpickeduser.avatar); + } + }); + + return currentpickedusers; + }; + + // TODO: Duplicated code from data/raceresults/+page.server.ts + const fetch_raceresults = async (): Promise => { + // Don't expand races/pxxs/dnfs since we already fetched those + const raceresults: RaceResult[] = await pb + .collection("raceresultsdesc") + .getFullList({ fetch: fetch }); + + return raceresults; + }; + + return { + racepicks: fetch_racepicks(), + currentpickedusers: fetch_currentpickedusers(), + raceresults: fetch_raceresults(), + + currentrace: await fetch_currentrace(), + }; +};