Compare commits

..

6 Commits

20 changed files with 416 additions and 495 deletions

View File

@ -6,14 +6,18 @@
SlideToggle, SlideToggle,
type ModalStore, type ModalStore,
} from "@skeletonlabs/skeleton"; } from "@skeletonlabs/skeleton";
import { Button, Input, Card, Dropdown, type DropdownOption } from "$lib/components"; import { Button, Input, Card, Dropdown } from "$lib/components";
import type { Driver } from "$lib/schema"; import type { Driver, Team } from "$lib/schema";
import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config"; import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config";
import { team_dropdown_options } from "$lib/dropdown";
interface DriverCardProps { interface DriverCardProps {
/** The [Driver] object used to prefill values. */ /** The [Driver] object used to prefill values. */
driver?: Driver | undefined; driver?: Driver | undefined;
/** The teams (for the dropdown options) */
teams: Team[];
/** Disable all inputs if [true] */ /** Disable all inputs if [true] */
disable_inputs?: boolean; disable_inputs?: boolean;
@ -28,20 +32,17 @@
// This also applies to the other card components... // This also applies to the other card components...
team_select_value: string; team_select_value: string;
/** The options this component's team select dropdown will display */
team_select_options: DropdownOption[];
/** The value this component's active switch will bind to */ /** The value this component's active switch will bind to */
active_value: boolean; active_value: boolean;
} }
let { let {
driver = undefined, driver = undefined,
teams,
disable_inputs = false, disable_inputs = false,
require_inputs = false, require_inputs = false,
headshot_template = undefined, headshot_template = undefined,
team_select_value, team_select_value,
team_select_options,
active_value, active_value,
}: DriverCardProps = $props(); }: DriverCardProps = $props();
@ -51,8 +52,8 @@
// Stuff thats required for the "update" card // Stuff thats required for the "update" card
driver = meta.driver; driver = meta.driver;
teams = meta.teams;
team_select_value = meta.team_select_value; team_select_value = meta.team_select_value;
team_select_options = meta.team_select_options;
active_value = meta.active_value; active_value = meta.active_value;
disable_inputs = meta.disable_inputs; disable_inputs = meta.disable_inputs;
@ -116,7 +117,7 @@
<Dropdown <Dropdown
name="team" name="team"
input_variable={team_select_value} input_variable={team_select_value}
options={team_select_options} options={team_dropdown_options(teams)}
labelwidth="120px" labelwidth="120px"
disabled={disable_inputs} disabled={disable_inputs}
required={require_inputs} required={require_inputs}

View File

@ -1,10 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Card, Button, Dropdown, type DropdownOption } from "$lib/components"; import { Card, Button, Dropdown } from "$lib/components";
import type { Driver, Race, RacePick, User } from "$lib/schema"; import type { Driver, Race, RacePick, User } from "$lib/schema";
import { get_by_value } from "$lib/database"; import { get_by_value } from "$lib/database";
import type { Action } from "svelte/action"; import type { Action } from "svelte/action";
import { getModalStore, type ModalStore } from "@skeletonlabs/skeleton"; import { getModalStore, type ModalStore } from "@skeletonlabs/skeleton";
import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config"; import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config";
import { driver_dropdown_options } from "$lib/dropdown";
interface RacePickCardProps { interface RacePickCardProps {
/** The [RacePick] object used to prefill values. */ /** The [RacePick] object used to prefill values. */
@ -30,9 +31,6 @@
/** The value this component's dnf select dropdown will bind to */ /** The value this component's dnf select dropdown will bind to */
dnf_select_value: string; dnf_select_value: string;
/** The options this component's driver select dropdowns will display */
driver_select_options: DropdownOption[];
} }
let { let {
@ -44,7 +42,6 @@
headshot_template = "", headshot_template = "",
pxx_select_value, pxx_select_value,
dnf_select_value, dnf_select_value,
driver_select_options,
}: RacePickCardProps = $props(); }: RacePickCardProps = $props();
const modalStore: ModalStore = getModalStore(); const modalStore: ModalStore = getModalStore();
@ -60,7 +57,6 @@
headshot_template = meta.headshot_template; headshot_template = meta.headshot_template;
pxx_select_value = meta.pxx_select_value; pxx_select_value = meta.pxx_select_value;
dnf_select_value = meta.dnf_select_value; dnf_select_value = meta.dnf_select_value;
driver_select_options = meta.driver_select_options;
} }
// This action is used on the <Dropdown> element. // This action is used on the <Dropdown> element.
@ -111,7 +107,7 @@
name="pxx" name="pxx"
input_variable={pxx_select_value} input_variable={pxx_select_value}
action={register_pxx_preview_handler} action={register_pxx_preview_handler}
options={driver_select_options} options={driver_dropdown_options(drivers)}
labelwidth="60px" labelwidth="60px"
disabled={disable_inputs} disabled={disable_inputs}
> >
@ -122,7 +118,7 @@
<Dropdown <Dropdown
name="dnf" name="dnf"
input_variable={dnf_select_value} input_variable={dnf_select_value}
options={driver_select_options} options={driver_dropdown_options(drivers)}
labelwidth="60px" labelwidth="60px"
disabled={disable_inputs} disabled={disable_inputs}
> >

View File

@ -6,10 +6,10 @@
type AutocompleteOption, type AutocompleteOption,
type ModalStore, type ModalStore,
} from "@skeletonlabs/skeleton"; } from "@skeletonlabs/skeleton";
import { Button, Card, Dropdown, type DropdownOption } from "$lib/components"; import { Button, Card, Dropdown } from "$lib/components";
import type { Driver, Race, RaceResult } from "$lib/schema"; import type { Driver, Race, RaceResult } from "$lib/schema";
import { get_by_value } from "$lib/database"; import { get_by_value } from "$lib/database";
import { RACE_PICTOGRAM_HEIGHT, RACE_PICTOGRAM_WIDTH } from "$lib/config"; import { race_dropdown_options } from "$lib/dropdown";
interface RaceResultCardProps { interface RaceResultCardProps {
/** The [RaceResult] object used to prefill values. */ /** The [RaceResult] object used to prefill values. */
@ -64,15 +64,6 @@
const require_inputs2 = require_inputs; const require_inputs2 = require_inputs;
let race_select_value: string = currentrace?.id ?? ""; let race_select_value: string = currentrace?.id ?? "";
const race_select_options: DropdownOption[] = races2.map((race: Race) => {
return {
label: race.name,
value: race.id,
icon_url: race.pictogram_url,
icon_width: RACE_PICTOGRAM_WIDTH,
icon_height: RACE_PICTOGRAM_HEIGHT,
};
});
let pxxs_input: string = $state(""); let pxxs_input: string = $state("");
let pxxs_chips: string[] = $state( let pxxs_chips: string[] = $state(
@ -160,7 +151,7 @@
<Dropdown <Dropdown
name="race" name="race"
input_variable={race_select_value} input_variable={race_select_value}
options={race_select_options} options={race_dropdown_options(races2)}
labelwidth="70px" labelwidth="70px"
disabled={disable_inputs2} disabled={disable_inputs2}
required={require_inputs2} required={require_inputs2}

View File

@ -1,10 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Card, Button, Dropdown, type DropdownOption } from "$lib/components"; import { Card, Button, Dropdown } from "$lib/components";
import type { Driver, Substitution } from "$lib/schema"; import type { Driver, Race, Substitution } from "$lib/schema";
import { get_by_value } from "$lib/database"; import { get_by_value } from "$lib/database";
import type { Action } from "svelte/action"; import type { Action } from "svelte/action";
import { getModalStore, type ModalStore } from "@skeletonlabs/skeleton"; import { getModalStore, type ModalStore } from "@skeletonlabs/skeleton";
import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config"; import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config";
import { driver_dropdown_options, race_dropdown_options } from "$lib/dropdown";
interface SubstitutionCardProps { interface SubstitutionCardProps {
/** The [Substitution] object used to prefill values. */ /** The [Substitution] object used to prefill values. */
@ -13,6 +14,8 @@
/** The drivers (to display the headshot) */ /** The drivers (to display the headshot) */
drivers: Driver[]; drivers: Driver[];
races: Race[];
/** Disable all inputs if [true] */ /** Disable all inputs if [true] */
disable_inputs?: boolean; disable_inputs?: boolean;
@ -30,25 +33,18 @@
/** The value this component's race select dropdown will bind to */ /** The value this component's race select dropdown will bind to */
race_select_value: string; race_select_value: string;
/** The options this component's substitute/driver select dropdowns will display */
driver_select_options: DropdownOption[];
/** The options this component's race select dropdown will display */
race_select_options: DropdownOption[];
} }
let { let {
substitution = undefined, substitution = undefined,
drivers, drivers,
races,
disable_inputs = false, disable_inputs = false,
require_inputs = false, require_inputs = false,
headshot_template = "", headshot_template = "",
substitute_select_value, substitute_select_value,
driver_select_value, driver_select_value,
race_select_value, race_select_value,
driver_select_options,
race_select_options,
}: SubstitutionCardProps = $props(); }: SubstitutionCardProps = $props();
const modalStore: ModalStore = getModalStore(); const modalStore: ModalStore = getModalStore();
@ -58,12 +54,11 @@
// Stuff thats required for the "update" card // Stuff thats required for the "update" card
substitution = meta.substitution; substitution = meta.substitution;
drivers = meta.drivers; drivers = meta.drivers;
races = meta.races;
disable_inputs = meta.disable_inputs; disable_inputs = meta.disable_inputs;
substitute_select_value = meta.substitute_select_value; substitute_select_value = meta.substitute_select_value;
driver_select_value = meta.driver_select_value; driver_select_value = meta.driver_select_value;
race_select_value = meta.race_select_value; race_select_value = meta.race_select_value;
driver_select_options = meta.driver_select_options;
race_select_options = meta.race_select_options;
// Stuff thats additionally required for the "create" card // Stuff thats additionally required for the "create" card
require_inputs = meta.require_inputs; require_inputs = meta.require_inputs;
@ -116,7 +111,7 @@
name="substitute" name="substitute"
input_variable={substitute_select_value} input_variable={substitute_select_value}
action={register_substitute_preview_handler} action={register_substitute_preview_handler}
options={driver_select_options} options={driver_dropdown_options(drivers)}
labelwidth="120px" labelwidth="120px"
disabled={disable_inputs} disabled={disable_inputs}
required={require_inputs} required={require_inputs}
@ -128,7 +123,7 @@
<Dropdown <Dropdown
name="for" name="for"
input_variable={driver_select_value} input_variable={driver_select_value}
options={driver_select_options} options={driver_dropdown_options(drivers)}
labelwidth="120px" labelwidth="120px"
disabled={disable_inputs} disabled={disable_inputs}
required={require_inputs} required={require_inputs}
@ -140,7 +135,7 @@
<Dropdown <Dropdown
name="race" name="race"
input_variable={race_select_value} input_variable={race_select_value}
options={race_select_options} options={race_dropdown_options(races)}
labelwidth="120px" labelwidth="120px"
disabled={disable_inputs} disabled={disable_inputs}
required={require_inputs} required={require_inputs}

View File

@ -88,6 +88,8 @@
placeholder="Enter as '#XXXXXX'" placeholder="Enter as '#XXXXXX'"
disabled={disable_inputs} disabled={disable_inputs}
required={require_inputs} required={require_inputs}
minlength={7}
maxlength={7}
onchange={(event: Event) => { onchange={(event: Event) => {
colorpreview = (event.target as HTMLInputElement).value; colorpreview = (event.target as HTMLInputElement).value;
}} }}

View File

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { page } from "$app/stores"; import { page } from "$app/stores";
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import type { HTMLButtonAttributes } from "svelte/elements";
import { popup, type PopupSettings } from "@skeletonlabs/skeleton"; import { popup, type PopupSettings } from "@skeletonlabs/skeleton";
const is_at_path = (path: string): boolean => { const is_at_path = (path: string): boolean => {
@ -10,7 +9,7 @@
return pathname.endsWith(path); return pathname.endsWith(path);
}; };
interface ButtonProps extends HTMLButtonAttributes { interface ButtonProps {
children: Snippet; children: Snippet;
/** The main color variant, e.g. "primary" or "secondary". */ /** The main color variant, e.g. "primary" or "secondary". */
@ -39,6 +38,15 @@
/** Additional classes to insert */ /** Additional classes to insert */
extraclass?: string; extraclass?: string;
/** An optional onclick event for the button */
onclick?: (event: Event) => void;
/** An optional formaction for the button */
formaction?: string;
/** Optionally disable the button */
disabled?: boolean;
} }
let { let {
@ -52,24 +60,26 @@
trigger_popup = { event: "click", target: "invalid" }, trigger_popup = { event: "click", target: "invalid" },
shadow = false, shadow = false,
extraclass = "", extraclass = "",
onclick = () => {},
formaction = undefined,
disabled = false,
...restProps ...restProps
}: ButtonProps = $props(); }: ButtonProps = $props();
</script> </script>
{#if href} {#if href}
<!-- HACK: Make the button act as a link using a form --> <a
<form action={href} class="contents"> {href}
<button class="btn m-0 select-none px-2 py-2 {color ? `variant-filled-${color}` : ''} {width} {activate
type="submit" ? 'btn-hover'
class="btn m-0 select-none px-2 py-2 {color : ''} {activate_href && is_at_path(href) ? 'btn-hover' : ''} {shadow
? `variant-filled-${color}` ? 'shadow'
: ''} {width} {activate ? 'btn-hover' : ''} {activate_href && is_at_path(href) : ''} {extraclass}"
? 'btn-hover' {onclick}
: ''} {shadow ? 'shadow' : ''} {extraclass}" {...restProps}
draggable="false" >
{...restProps}>{@render children()}</button {@render children()}
> </a>
</form>
{:else} {:else}
<button <button
type={submit ? "submit" : "button"} type={submit ? "submit" : "button"}
@ -78,6 +88,11 @@
: ''} {shadow ? 'shadow' : ''} {extraclass}" : ''} {shadow ? 'shadow' : ''} {extraclass}"
draggable="false" draggable="false"
use:popup={trigger_popup} use:popup={trigger_popup}
{...restProps}>{@render children()}</button {onclick}
{formaction}
{disabled}
{...restProps}
> >
{@render children()}
</button>
{/if} {/if}

View File

@ -1,3 +1,5 @@
import type { Graphic } from "$lib/schema";
/** /**
* Select an element from an [objects] array where [key] matches [value]. * Select an element from an [objects] array where [key] matches [value].
* Supposed to be used on collections returned by the [PocketBase] client. * Supposed to be used on collections returned by the [PocketBase] client.
@ -9,3 +11,15 @@ export const get_by_value = <T extends object>(
): T | undefined => { ): T | undefined => {
return objects.find((o: T) => (key in o ? o[key] === value : false)); return objects.find((o: T) => (key in o ? o[key] === value : false));
}; };
export const get_team_banner_template = (graphics: Graphic[]) =>
get_by_value(graphics, "name", "team_banner_template")?.file_url ?? "Invalid";
export const get_team_logo_template = (graphics: Graphic[]) =>
get_by_value(graphics, "name", "team_logo_template")?.file_url ?? "Invalid";
export const get_driver_headshot_template = (graphics: Graphic[]) =>
get_by_value(graphics, "name", "driver_headshot_template")?.file_url ?? "Invalid";
export const get_race_pictogram_template = (graphics: Graphic[]) =>
get_by_value(graphics, "name", "race_pictogram_template")?.file_url ?? "Invalid";

79
src/lib/dropdown.ts Normal file
View File

@ -0,0 +1,79 @@
import type { DropdownOption } from "$lib/components";
import type { Driver, Race, Team } from "$lib/schema";
import {
DRIVER_HEADSHOT_HEIGHT,
DRIVER_HEADSHOT_WIDTH,
RACE_PICTOGRAM_HEIGHT,
RACE_PICTOGRAM_WIDTH,
TEAM_BANNER_HEIGHT,
TEAM_BANNER_WIDTH,
} from "$lib/config";
let team_dropdown_opts: DropdownOption[] | null = null;
/**
* Generates a list of [DropdownOptions] for a <Dropdown> component.
* Cached until page reload.
*/
export const team_dropdown_options = (teams: Team[]): DropdownOption[] => {
if (!team_dropdown_opts) {
console.log("team_dropdown_options");
team_dropdown_opts = teams.map((team: Team) => {
return {
label: team.name,
value: team.id,
icon_url: team.banner_url,
icon_width: TEAM_BANNER_WIDTH,
icon_height: TEAM_BANNER_HEIGHT,
};
});
}
return team_dropdown_opts;
};
let driver_dropdown_opts: DropdownOption[] | null = null;
/**
* Generates a list of [DropdownOptions] for a <Dropdown> component.
* Cached until page reload.
*/
export const driver_dropdown_options = (drivers: Driver[]): DropdownOption[] => {
if (!driver_dropdown_opts) {
console.log("driver_dropdown_options");
driver_dropdown_opts = drivers.map((driver: Driver) => {
return {
label: driver.code,
value: driver.id,
icon_url: driver.headshot_url,
icon_width: DRIVER_HEADSHOT_WIDTH,
icon_height: DRIVER_HEADSHOT_HEIGHT,
};
});
}
return driver_dropdown_opts;
};
let race_dropdown_opts: DropdownOption[] | null = null;
/**
* Generates a list of [DropdownOptions] for a <Dropdown> component.
* Cached until page reload.
*/
export const race_dropdown_options = (races: Race[]): DropdownOption[] => {
if (!race_dropdown_opts) {
console.log("race_dropdown_options");
race_dropdown_opts = races.map((race: Race) => {
return {
label: race.name,
value: race.id,
icon_url: race.pictogram_url,
icon_width: RACE_PICTOGRAM_WIDTH,
icon_height: RACE_PICTOGRAM_HEIGHT,
};
});
}
return race_dropdown_opts;
};

View File

@ -1,3 +1,4 @@
import type { Driver, Graphic, Race, Substitution, Team } from "$lib/schema";
import type { LayoutServerLoad } from "./$types"; import type { LayoutServerLoad } from "./$types";
// On each page load (every route), this function runs serverside. // On each page load (every route), this function runs serverside.
@ -6,14 +7,82 @@ import type { LayoutServerLoad } from "./$types";
// It will populate the "user" attribute of each page's "data" object, // 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). // 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: LayoutServerLoad = ({ locals }) => {
if (locals.user) { const fetch_graphics = async (): Promise<Graphic[]> => {
return { const graphics: Graphic[] = await locals.pb
user: locals.user, .collection("graphics")
admin: locals.user.admin, .getFullList({ fetch: fetch });
};
} graphics.map((graphic: Graphic) => {
graphic.file_url = locals.pb.files.getURL(graphic, graphic.file);
});
return graphics;
};
const fetch_teams = async (): Promise<Team[]> => {
const teams: Team[] = await locals.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);
});
return teams;
};
const fetch_drivers = async (): Promise<Driver[]> => {
const drivers: Driver[] = await locals.pb.collection("drivers").getFullList({
sort: "+code",
fetch: fetch,
});
drivers.map((driver: Driver) => {
driver.headshot_url = locals.pb.files.getURL(driver, driver.headshot);
});
return drivers;
};
const fetch_races = async (): Promise<Race[]> => {
const races: Race[] = await locals.pb.collection("races").getFullList({
sort: "+step",
fetch: fetch,
});
races.map((race: Race) => {
race.pictogram_url = locals.pb.files.getURL(race, race.pictogram);
});
return races;
};
const fetch_substitutions = async (): Promise<Substitution[]> => {
const substitutions: Substitution[] = await locals.pb.collection("substitutions").getFullList({
expand: "race",
fetch: fetch,
});
// Sort by race step (ascending)
substitutions.sort(
(a: Substitution, b: Substitution) => a.expand.race.step - b.expand.race.step,
);
return substitutions;
};
return { return {
user: undefined, // User information
user: locals.user,
admin: locals.user?.admin ?? false,
// Return static data asynchronously
graphics: fetch_graphics(),
teams: fetch_teams(),
drivers: fetch_drivers(),
races: fetch_races(),
substitutions: fetch_substitutions(),
}; };
}; };

View File

@ -49,51 +49,7 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
return raceresults; return raceresults;
}; };
// TODO: Duplicated code from data/season/+layout.server.ts and racepicks/+page.server.ts
const fetch_races = async (): Promise<Race[]> => {
const races: Race[] = await locals.pb.collection("races").getFullList({
sort: "+step",
fetch: fetch,
});
races.map((race: Race) => {
race.pictogram_url = locals.pb.files.getURL(race, race.pictogram);
});
return races;
};
// TODO: Duplicated code from data/season/+layout.server.ts and racepicks/+page.server.ts
const fetch_drivers = async (): Promise<Driver[]> => {
const drivers: Driver[] = await locals.pb.collection("drivers").getFullList({
sort: "+code",
fetch: fetch,
});
drivers.map((driver: Driver) => {
driver.headshot_url = locals.pb.files.getURL(driver, driver.headshot);
});
return drivers;
};
// TODO: Duplicated code from racepicks/+page.server.ts + users/+page.server.ts
const fetch_graphics = async (): Promise<Graphic[]> => {
const graphics: Graphic[] = await locals.pb
.collection("graphics")
.getFullList({ fetch: fetch });
graphics.map((graphic: Graphic) => {
graphic.file_url = locals.pb.files.getURL(graphic, graphic.file);
});
return graphics;
};
return { return {
results: await fetch_raceresults(), results: await fetch_raceresults(),
races: await fetch_races(),
drivers: await fetch_drivers(),
graphics: await fetch_graphics(),
}; };
}; };

View File

@ -12,28 +12,30 @@
data_value_name: "race", data_value_name: "race",
label: "Step", label: "Step",
valuefun: async (value: string): Promise<string> => valuefun: async (value: string): Promise<string> =>
`<span class='badge variant-filled-surface'>${get_by_value(data.races, "id", value)?.step}</span>`, `<span class='badge variant-filled-surface'>${get_by_value(await data.races, "id", value)?.step}</span>`,
}, },
{ {
data_value_name: "race", data_value_name: "race",
label: "Race", label: "Race",
valuefun: async (value: string): Promise<string> => valuefun: async (value: string): Promise<string> =>
`<span>${get_by_value(data.races, "id", value)?.name}</span>`, `<span>${get_by_value(await data.races, "id", value)?.name}</span>`,
}, },
{ {
data_value_name: "race", data_value_name: "race",
label: "Guessed", label: "Guessed",
valuefun: async (value: string): Promise<string> => valuefun: async (value: string): Promise<string> =>
`<span>P${get_by_value(data.races, "id", value)?.pxx}</span>`, `<span>P${get_by_value(await data.races, "id", value)?.pxx}</span>`,
}, },
{ {
data_value_name: "pxxs", data_value_name: "pxxs",
label: "Standing", label: "Standing",
valuefun: async (value: string): Promise<string> => { valuefun: async (value: string): Promise<string> => {
const pxxs_array: string[] = value.toString().split(","); const pxxs_array: string[] = value.toString().split(",");
const pxxs_codes: string[] = pxxs_array.map( const pxxs_codes: string[] = await Promise.all(
(id: string, index: number) => pxxs_array.map(
`<span class='w-10 badge mr-2 text-center' style='background: ${PXX_COLORS[index]};'>${get_by_value(data.drivers, "id", id)?.code ?? "Invalid"}</span>`, async (id: string, index: number) =>
`<span class='w-10 badge mr-2 text-center' style='background: ${PXX_COLORS[index]};'>${get_by_value(await data.drivers, "id", id)?.code ?? "Invalid"}</span>`,
),
); );
return pxxs_codes.join(""); return pxxs_codes.join("");
@ -46,9 +48,11 @@
if (value.length === 0 || value === "") return ""; if (value.length === 0 || value === "") return "";
const dnfs_array: string[] = value.toString().split(","); const dnfs_array: string[] = value.toString().split(",");
const dnfs_codes: string[] = dnfs_array.map( const dnfs_codes: string[] = await Promise.all(
(id: string) => dnfs_array.map(
`<span class='w-10 text-center badge mr-2' style='background: ${PXX_COLORS[3]}'>${get_by_value(data.drivers, "id", id)?.code ?? "Invalid"}</span>`, async (id: string) =>
`<span class='w-10 text-center badge mr-2' style='background: ${PXX_COLORS[3]}'>${get_by_value(await data.drivers, "id", id)?.code ?? "Invalid"}</span>`,
),
); );
return dnfs_codes.join(""); return dnfs_codes.join("");
@ -64,8 +68,8 @@
component: "raceResultCard", component: "raceResultCard",
meta: { meta: {
disable_inputs: !data.admin, disable_inputs: !data.admin,
drivers: data.drivers, drivers: await data.drivers,
races: data.races, races: await data.races,
result: get_by_value(data.results, "id", id), result: get_by_value(data.results, "id", id),
}, },
}; };
@ -73,14 +77,14 @@
modalStore.trigger(modalSettings); modalStore.trigger(modalSettings);
}; };
const create_result_handler = (event: Event) => { const create_result_handler = async (event: Event) => {
const modalSettings: ModalSettings = { const modalSettings: ModalSettings = {
type: "component", type: "component",
component: "raceResultCard", component: "raceResultCard",
meta: { meta: {
disable_inputs: !data.admin, disable_inputs: !data.admin,
drivers: data.drivers, drivers: await data.drivers,
races: data.races, races: await data.races,
require_inputs: true, require_inputs: true,
}, },
}; };

View File

@ -1,83 +0,0 @@
import type { Team, Driver, Race, Substitution, Graphic } from "$lib/schema";
import type { LayoutServerLoad } from "./$types";
// This "load" function runs serverside only, as it's located inside +layout.server.ts
export const load: LayoutServerLoad = async ({ fetch, locals }) => {
// TODO: Duplicated code from racepicks/+page.server.ts + users/+page.server.ts
const fetch_graphics = async (): Promise<Graphic[]> => {
const graphics: Graphic[] = await locals.pb
.collection("graphics")
.getFullList({ fetch: fetch });
graphics.map((graphic: Graphic) => {
graphic.file_url = locals.pb.files.getURL(graphic, graphic.file);
});
return graphics;
};
const fetch_teams = async (): Promise<Team[]> => {
const teams: Team[] = await locals.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);
});
return teams;
};
// TODO: Duplicated code from racepicks/+page.server.ts and data/raceresults/+page.server.ts
const fetch_drivers = async (): Promise<Driver[]> => {
const drivers: Driver[] = await locals.pb.collection("drivers").getFullList({
sort: "+code",
fetch: fetch,
});
drivers.map((driver: Driver) => {
driver.headshot_url = locals.pb.files.getURL(driver, driver.headshot);
});
return drivers;
};
// TODO: Duplicated code from racepicks/+page.server.ts and data/raceresults/+page.server.ts
const fetch_races = async (): Promise<Race[]> => {
const races: Race[] = await locals.pb.collection("races").getFullList({
sort: "+step",
fetch: fetch,
});
races.map((race: Race) => {
race.pictogram_url = locals.pb.files.getURL(race, race.pictogram);
});
return races;
};
const fetch_substitutions = async (): Promise<Substitution[]> => {
const substitutions: Substitution[] = await locals.pb.collection("substitutions").getFullList({
expand: "race",
fetch: fetch,
});
// Sort by race step (ascending)
substitutions.sort((a, b) => a.expand.race.step - b.expand.race.step);
return substitutions;
};
return {
// Graphics and teams are awaited, since those are visible on page load.
graphics: await fetch_graphics(),
teams: await fetch_teams(),
// The rest is streamed gradually, since the user has to switch pages to need them.
drivers: fetch_drivers(),
races: fetch_races(),
substitutions: fetch_substitutions(),
};
};

View File

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Button, type DropdownOption, type TableColumn, Table } from "$lib/components"; import { Button, type TableColumn, Table } from "$lib/components";
import { TEAM_LOGO_HEIGHT, TEAM_LOGO_WIDTH } from "$lib/config"; import { get_by_value, get_driver_headshot_template } from "$lib/database";
import { get_by_value } from "$lib/database";
import type { Driver, Team } from "$lib/schema"; import type { Driver, Team } from "$lib/schema";
import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton"; import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
@ -19,18 +18,6 @@
update_driver_team_select_values["create"] = ""; update_driver_team_select_values["create"] = "";
update_driver_active_values["create"] = true; update_driver_active_values["create"] = true;
// All options to create a <Dropdown> component for the teams
const team_dropdown_options: DropdownOption[] = [];
data.teams.forEach((team: Team) => {
team_dropdown_options.push({
label: team.name,
value: team.id,
icon_url: team.logo_url,
icon_width: TEAM_LOGO_WIDTH,
icon_height: TEAM_LOGO_HEIGHT,
});
});
const drivers_columns: TableColumn[] = [ const drivers_columns: TableColumn[] = [
{ {
data_value_name: "code", data_value_name: "code",
@ -44,7 +31,7 @@
data_value_name: "team", data_value_name: "team",
label: "Team", label: "Team",
valuefun: async (value: string): Promise<string> => { valuefun: async (value: string): Promise<string> => {
const team: Team | undefined = get_by_value(data.teams, "id", value); const team: Team | undefined = get_by_value(await data.teams, "id", value);
return team return team
? `<span class='badge border mr-2' style='color: ${team.color}; background: ${team.color};'>C</span>${team.name}` ? `<span class='badge border mr-2' style='color: ${team.color}; background: ${team.color};'>C</span>${team.name}`
: "<span class='badge variant-filled-primary'>Invalid</span>"; : "<span class='badge variant-filled-primary'>Invalid</span>";
@ -70,8 +57,8 @@
component: "driverCard", component: "driverCard",
meta: { meta: {
driver: driver, driver: driver,
teams: await data.teams,
team_select_value: update_driver_team_select_values[driver.id], team_select_value: update_driver_team_select_values[driver.id],
team_select_options: team_dropdown_options,
active_value: update_driver_active_values[driver.id], active_value: update_driver_active_values[driver.id],
disable_inputs: !data.admin, disable_inputs: !data.admin,
}, },
@ -85,13 +72,12 @@
type: "component", type: "component",
component: "driverCard", component: "driverCard",
meta: { meta: {
teams: await data.teams,
team_select_value: update_driver_team_select_values["create"], team_select_value: update_driver_team_select_values["create"],
team_select_options: team_dropdown_options,
active_value: update_driver_active_values["create"], active_value: update_driver_active_values["create"],
disable_inputs: !data.admin, disable_inputs: !data.admin,
require_inputs: true, require_inputs: true,
headshot_template: headshot_template: get_driver_headshot_template(await data.graphics),
get_by_value(data.graphics, "name", "driver_headshot_template")?.file_url ?? "Invalid",
}, },
}; };

View File

@ -2,7 +2,7 @@
import { Button, Table, type TableColumn } from "$lib/components"; import { Button, Table, type TableColumn } from "$lib/components";
import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton"; import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { get_by_value } from "$lib/database"; import { get_by_value, get_race_pictogram_template } from "$lib/database";
import type { Race } from "$lib/schema"; import type { Race } from "$lib/schema";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@ -62,8 +62,7 @@
meta: { meta: {
disable_inputs: !data.admin, disable_inputs: !data.admin,
require_inputs: true, require_inputs: true,
pictogram_template: pictogram_template: get_race_pictogram_template(await data.graphics),
get_by_value(data.graphics, "name", "race_pictogram_template")?.file_url ?? "Invalid",
}, },
}; };

View File

@ -1,18 +1,13 @@
<script lang="ts"> <script lang="ts">
import { get_by_value } from "$lib/database"; import { get_by_value, get_driver_headshot_template } from "$lib/database";
import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton"; import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import type { Driver, Race, Substitution } from "$lib/schema"; import type { Race, Substitution } from "$lib/schema";
import { Button, Table, type DropdownOption, type TableColumn } from "$lib/components"; import { Button, Table, type TableColumn } from "$lib/components";
import {
DRIVER_HEADSHOT_HEIGHT,
DRIVER_HEADSHOT_WIDTH,
RACE_PICTOGRAM_HEIGHT,
RACE_PICTOGRAM_WIDTH,
} from "$lib/config";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
// TODO: Cleanup
const update_substitution_substitute_select_values: { [key: string]: string } = $state({}); const update_substitution_substitute_select_values: { [key: string]: string } = $state({});
const update_substitution_for_select_values: { [key: string]: string } = $state({}); const update_substitution_for_select_values: { [key: string]: string } = $state({});
const update_substitution_race_select_values: { [key: string]: string } = $state({}); const update_substitution_race_select_values: { [key: string]: string } = $state({});
@ -27,33 +22,6 @@
update_substitution_for_select_values["create"] = ""; update_substitution_for_select_values["create"] = "";
update_substitution_race_select_values["create"] = ""; update_substitution_race_select_values["create"] = "";
// TODO: Duplicated code in substitutions/+page.svelte
const driver_dropdown_options: DropdownOption[] = [];
data.drivers.then((drivers: Driver[]) =>
drivers.forEach((driver: Driver) => {
driver_dropdown_options.push({
label: driver.code,
value: driver.id,
icon_url: driver.headshot_url,
icon_width: DRIVER_HEADSHOT_WIDTH,
icon_height: DRIVER_HEADSHOT_HEIGHT,
});
}),
);
const race_dropdown_options: DropdownOption[] = [];
data.races.then((races: Race[]) =>
races.forEach((race: Race) => {
race_dropdown_options.push({
label: race.name,
value: race.id,
icon_url: race.pictogram_url,
icon_width: RACE_PICTOGRAM_WIDTH,
icon_height: RACE_PICTOGRAM_HEIGHT,
});
}),
);
const substitutions_columns: TableColumn[] = [ const substitutions_columns: TableColumn[] = [
{ {
data_value_name: "expand", data_value_name: "expand",
@ -95,11 +63,10 @@
meta: { meta: {
substitution: substitution, substitution: substitution,
drivers: await data.drivers, drivers: await data.drivers,
races: await data.races,
substitute_select_value: update_substitution_substitute_select_values[substitution.id], substitute_select_value: update_substitution_substitute_select_values[substitution.id],
driver_select_value: update_substitution_for_select_values[substitution.id], driver_select_value: update_substitution_for_select_values[substitution.id],
race_select_value: update_substitution_race_select_values[substitution.id], race_select_value: update_substitution_race_select_values[substitution.id],
driver_select_options: driver_dropdown_options,
race_select_options: race_dropdown_options,
disable_inputs: !data.admin, disable_inputs: !data.admin,
}, },
}; };
@ -113,15 +80,13 @@
component: "substitutionCard", component: "substitutionCard",
meta: { meta: {
drivers: await data.drivers, drivers: await data.drivers,
races: await data.races,
substitute_select_value: update_substitution_substitute_select_values["create"], substitute_select_value: update_substitution_substitute_select_values["create"],
driver_select_value: update_substitution_for_select_values["create"], driver_select_value: update_substitution_for_select_values["create"],
disable_inputs: !data.admin, disable_inputs: !data.admin,
race_select_value: update_substitution_race_select_values["create"], race_select_value: update_substitution_race_select_values["create"],
driver_select_options: driver_dropdown_options,
race_select_options: race_dropdown_options,
require_inputs: true, require_inputs: true,
headshot_template: headshot_template: get_driver_headshot_template(await data.graphics),
get_by_value(data.graphics, "name", "driver_headshot_template")?.file_url ?? "Invalid",
}, },
}; };

View File

@ -3,7 +3,7 @@
import type { Team } from "$lib/schema"; import type { Team } from "$lib/schema";
import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton"; import { getModalStore, type ModalSettings, type ModalStore } from "@skeletonlabs/skeleton";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import { get_by_value } from "$lib/database"; import { get_by_value, get_team_banner_template, get_team_logo_template } from "$lib/database";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@ -25,7 +25,7 @@
const modalStore: ModalStore = getModalStore(); const modalStore: ModalStore = getModalStore();
const teams_handler = async (event: Event, id: string) => { const teams_handler = async (event: Event, id: string) => {
const team: Team | undefined = get_by_value(data.teams, "id", id); const team: Team | undefined = get_by_value(await data.teams, "id", id);
if (!team) return; if (!team) return;
const modalSettings: ModalSettings = { const modalSettings: ModalSettings = {
@ -40,15 +40,13 @@
modalStore.trigger(modalSettings); modalStore.trigger(modalSettings);
}; };
const create_team_handler = (event: Event) => { const create_team_handler = async (event: Event) => {
const modalSettings: ModalSettings = { const modalSettings: ModalSettings = {
type: "component", type: "component",
component: "teamCard", component: "teamCard",
meta: { meta: {
banner_template: banner_template: get_team_banner_template(await data.graphics),
get_by_value(data.graphics, "name", "team_banner_template")?.file_url ?? "Invalid", logo_template: get_team_logo_template(await data.graphics),
logo_template:
get_by_value(data.graphics, "name", "team_logo_template")?.file_url ?? "Invalid",
require_inputs: true, require_inputs: true,
disable_inputs: !data.admin, disable_inputs: !data.admin,
}, },
@ -63,4 +61,6 @@
<span class="font-bold">Create New Team</span> <span class="font-bold">Create New Team</span>
</Button> </Button>
</div> </div>
<Table data={data.teams} columns={teams_columns} handler={teams_handler} /> {#await data.teams then teams}
<Table data={teams} columns={teams_columns} handler={teams_handler} />
{/await}

View File

@ -14,21 +14,7 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
return users; return users;
}; };
// TODO: Duplicated code from data/season/+layout.server.ts + racepicks/+page.server.ts
const fetch_graphics = async (): Promise<Graphic[]> => {
const graphics: Graphic[] = await locals.pb
.collection("graphics")
.getFullList({ fetch: fetch });
graphics.map((graphic: Graphic) => {
graphic.file_url = locals.pb.files.getURL(graphic, graphic.file);
});
return graphics;
};
return { return {
users: await fetch_users(), users: await fetch_users(),
graphics: await fetch_graphics(),
}; };
}; };

View File

@ -21,7 +21,7 @@
data_value_name: "avatar_url", data_value_name: "avatar_url",
label: "Avatar", label: "Avatar",
valuefun: async (value: string): Promise<string> => valuefun: async (value: string): Promise<string> =>
`<img class='rounded-full w-10 bg-surface-400' src='${value ? value : get_by_value(data.graphics, "name", "driver_headshot_template")?.file_url}'/>`, `<img class='rounded-full w-10 bg-surface-400' src='${value ? value : get_by_value(await data.graphics, "name", "driver_headshot_template")?.file_url}'/>`,
}, },
{ {
data_value_name: "admin", data_value_name: "admin",

View File

@ -1,5 +1,5 @@
import { form_data_clean, form_data_ensure_keys, form_data_get_and_remove_id } from "$lib/form"; import { form_data_clean, form_data_ensure_keys, form_data_get_and_remove_id } from "$lib/form";
import type { CurrentPickedUser, Driver, Graphic, Race, RacePick, RaceResult } from "$lib/schema"; import type { CurrentPickedUser, Race, RacePick, RaceResult } from "$lib/schema";
import type { Actions, PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ fetch, locals }) => { export const load: PageServerLoad = async ({ fetch, locals }) => {
@ -53,55 +53,11 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
return raceresults; return raceresults;
}; };
// TODO: Duplicated code from data/season/+layout.server.ts and data/raceresults/+page.server.ts
const fetch_drivers = async (): Promise<Driver[]> => {
const drivers: Driver[] = await locals.pb.collection("drivers").getFullList({
sort: "+code",
fetch: fetch,
});
drivers.map((driver: Driver) => {
driver.headshot_url = locals.pb.files.getURL(driver, driver.headshot);
});
return drivers;
};
// TODO: Duplicated code from data/season/+layout.server.ts and data/raceresults/+page.server.ts
const fetch_races = async (): Promise<Race[]> => {
const races: Race[] = await locals.pb.collection("races").getFullList({
sort: "+step",
fetch: fetch,
});
races.map((race: Race) => {
race.pictogram_url = locals.pb.files.getURL(race, race.pictogram);
});
return races;
};
// TODO: Duplicated code from data/season/+layout.server.ts + users/+page.server.ts
const fetch_graphics = async (): Promise<Graphic[]> => {
const graphics: Graphic[] = await locals.pb
.collection("graphics")
.getFullList({ fetch: fetch });
graphics.map((graphic: Graphic) => {
graphic.file_url = locals.pb.files.getURL(graphic, graphic.file);
});
return graphics;
};
return { return {
racepicks: await fetch_racepicks(), racepicks: await fetch_racepicks(),
currentrace: await fetch_currentrace(), currentrace: await fetch_currentrace(),
currentpickedusers: await fetch_currentpickedusers(), currentpickedusers: await fetch_currentpickedusers(),
raceresults: await fetch_raceresults(), raceresults: await fetch_raceresults(),
drivers: await fetch_drivers(),
races: await fetch_races(),
graphics: await fetch_graphics(),
}; };
}; };

View File

@ -1,12 +1,5 @@
<script lang="ts"> <script lang="ts">
import { import { ChequeredFlagIcon, Countdown, LazyImage, StopwatchIcon } from "$lib/components";
Button,
ChequeredFlagIcon,
Countdown,
LazyImage,
StopwatchIcon,
type DropdownOption,
} from "$lib/components";
import { import {
Accordion, Accordion,
AccordionItem, AccordionItem,
@ -26,8 +19,8 @@
RACE_PICTOGRAM_HEIGHT, RACE_PICTOGRAM_HEIGHT,
RACE_PICTOGRAM_WIDTH, RACE_PICTOGRAM_WIDTH,
} from "$lib/config"; } from "$lib/config";
import type { CurrentPickedUser, Driver, Race, RacePick } from "$lib/schema"; import type { CurrentPickedUser, RacePick } from "$lib/schema";
import { get_by_value } from "$lib/database"; import { get_by_value, get_driver_headshot_template } from "$lib/database";
import { format } from "date-fns"; import { format } from "date-fns";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@ -42,18 +35,6 @@
let pxx_select_value: string = $state(currentpick?.pxx ?? ""); let pxx_select_value: string = $state(currentpick?.pxx ?? "");
let dnf_select_value: string = $state(currentpick?.dnf ?? ""); let dnf_select_value: string = $state(currentpick?.dnf ?? "");
// TODO: Duplicated code in cards/substitutioncard.svelte
const driver_dropdown_options: DropdownOption[] = [];
data.drivers.forEach((driver: Driver) => {
driver_dropdown_options.push({
label: driver.code,
value: driver.id,
icon_url: driver.headshot_url,
icon_width: DRIVER_HEADSHOT_WIDTH,
icon_height: DRIVER_HEADSHOT_HEIGHT,
});
});
const modalStore: ModalStore = getModalStore(); const modalStore: ModalStore = getModalStore();
const create_guess_handler = async (event: Event) => { const create_guess_handler = async (event: Event) => {
const modalSettings: ModalSettings = { const modalSettings: ModalSettings = {
@ -63,26 +44,20 @@
racepick: currentpick, racepick: currentpick,
currentrace: data.currentrace, currentrace: data.currentrace,
user: data.user, user: data.user,
drivers: data.drivers, drivers: await data.drivers,
disable_inputs: false, // TODO: Datelock disable_inputs: false, // TODO: Datelock
headshot_template: headshot_template: get_driver_headshot_template(await data.graphics),
get_by_value(data.graphics, "name", "driver_headshot_template")?.file_url ?? "Invalid",
pxx_select_value: pxx_select_value, pxx_select_value: pxx_select_value,
dnf_select_value: dnf_select_value, dnf_select_value: dnf_select_value,
driver_select_options: driver_dropdown_options,
}, },
}; };
modalStore.trigger(modalSettings); modalStore.trigger(modalSettings);
}; };
const getrace = (id: string): Race | undefined => get_by_value(data.races, "id", id);
const getdriver = (id: string): Driver | undefined => get_by_value(data.drivers, "id", id);
const pickedusers = data.currentpickedusers.filter( const pickedusers = data.currentpickedusers.filter(
(currentpickeduser: CurrentPickedUser) => currentpickeduser.picked, (currentpickeduser: CurrentPickedUser) => currentpickeduser.picked,
); );
// pickedusers = pickedusers.concat(pickedusers, pickedusers);
const outstandingusers = data.currentpickedusers.filter( const outstandingusers = data.currentpickedusers.filter(
(currentpickeduser: CurrentPickedUser) => !currentpickeduser.picked, (currentpickeduser: CurrentPickedUser) => !currentpickeduser.picked,
); );
@ -90,9 +65,6 @@
const dateformat: string = "dd.MM' 'HH:mm"; const dateformat: string = "dd.MM' 'HH:mm";
const formatdate = (date: string): string => format(new Date(date), dateformat); const formatdate = (date: string): string => format(new Date(date), dateformat);
const graphicfallback = (graphic: string | undefined, fallback: string): string =>
graphic ?? get_by_value(data.graphics, "name", fallback)?.file_url ?? "Invalid";
const race_popupsettings = (target: string): PopupSettings => { const race_popupsettings = (target: string): PopupSettings => {
return { return {
event: "click", event: "click",
@ -159,33 +131,37 @@
<div class="mt-2 flex gap-2"> <div class="mt-2 flex gap-2">
<div class="card w-full p-2 pb-0 shadow"> <div class="card w-full p-2 pb-0 shadow">
<h1 class="mb-2 text-nowrap font-bold">Your P{data.currentrace.pxx} Pick:</h1> <h1 class="mb-2 text-nowrap font-bold">Your P{data.currentrace.pxx} Pick:</h1>
<LazyImage {#await data.graphics then graphics}
src={graphicfallback( {#await data.drivers then drivers}
getdriver(currentpick?.pxx ?? "")?.headshot_url, <LazyImage
"driver_headshot_template", src={get_by_value(drivers, "id", currentpick?.pxx ?? "")?.headshot_url ??
)} get_driver_headshot_template(graphics)}
imgwidth={DRIVER_HEADSHOT_WIDTH} imgwidth={DRIVER_HEADSHOT_WIDTH}
imgheight={DRIVER_HEADSHOT_HEIGHT} imgheight={DRIVER_HEADSHOT_HEIGHT}
containerstyle="height: 115px; margin: auto;" containerstyle="height: 115px; margin: auto;"
imgclass="bg-transparent cursor-pointer" imgclass="bg-transparent cursor-pointer"
hoverzoom hoverzoom
onclick={create_guess_handler} onclick={create_guess_handler}
/> />
{/await}
{/await}
</div> </div>
<div class="card w-full p-2 pb-0 shadow"> <div class="card w-full p-2 pb-0 shadow">
<h1 class="mb-2 text-nowrap font-bold">Your DNF Pick:</h1> <h1 class="mb-2 text-nowrap font-bold">Your DNF Pick:</h1>
<LazyImage {#await data.graphics then graphics}
src={graphicfallback( {#await data.drivers then drivers}
getdriver(currentpick?.dnf ?? "")?.headshot_url, <LazyImage
"driver_headshot_template", src={get_by_value(drivers, "id", currentpick?.dnf ?? "")?.headshot_url ??
)} get_driver_headshot_template(graphics)}
imgwidth={DRIVER_HEADSHOT_WIDTH} imgwidth={DRIVER_HEADSHOT_WIDTH}
imgheight={DRIVER_HEADSHOT_HEIGHT} imgheight={DRIVER_HEADSHOT_HEIGHT}
containerstyle="height: 115px; margin: auto;" containerstyle="height: 115px; margin: auto;"
imgclass="bg-transparent cursor-pointer" imgclass="bg-transparent cursor-pointer"
hoverzoom hoverzoom
onclick={create_guess_handler} onclick={create_guess_handler}
/> />
{/await}
{/await}
</div> </div>
</div> </div>
{/if} {/if}
@ -197,15 +173,17 @@
Picked ({pickedusers.length}/{data.currentpickedusers.length}): Picked ({pickedusers.length}/{data.currentpickedusers.length}):
</h1> </h1>
<div class="mt-1 grid grid-cols-4 gap-x-2 gap-y-0.5"> <div class="mt-1 grid grid-cols-4 gap-x-2 gap-y-0.5">
{#each pickedusers.slice(0, 16) as user} {#await data.graphics then graphics}
<LazyImage {#each pickedusers.slice(0, 16) as user}
src={graphicfallback(user.avatar_url, "driver_headshot_template")} <LazyImage
imgwidth={AVATAR_WIDTH} src={user.avatar_url ?? get_driver_headshot_template(graphics)}
imgheight={AVATAR_HEIGHT} imgwidth={AVATAR_WIDTH}
containerstyle="height: 35px; width: 35px;" imgheight={AVATAR_HEIGHT}
imgclass="bg-surface-400 rounded-full" containerstyle="height: 35px; width: 35px;"
/> imgclass="bg-surface-400 rounded-full"
{/each} />
{/each}
{/await}
</div> </div>
</div> </div>
<div class="card w-full p-2 shadow"> <div class="card w-full p-2 shadow">
@ -213,15 +191,17 @@
Outstanding ({outstandingusers.length}/{data.currentpickedusers.length}): Outstanding ({outstandingusers.length}/{data.currentpickedusers.length}):
</h1> </h1>
<div class="mt-1 grid grid-cols-4 gap-x-0 gap-y-0.5"> <div class="mt-1 grid grid-cols-4 gap-x-0 gap-y-0.5">
{#each outstandingusers.slice(0, 16) as user} {#await data.graphics then graphics}
<LazyImage {#each outstandingusers.slice(0, 16) as user}
src={graphicfallback(user.avatar_url, "driver_headshot_template")} <LazyImage
imgwidth={AVATAR_WIDTH} src={user.avatar_url ?? get_driver_headshot_template(graphics)}
imgheight={AVATAR_HEIGHT} imgwidth={AVATAR_WIDTH}
containerstyle="height: 35px; width: 35px;" imgheight={AVATAR_HEIGHT}
imgclass="bg-surface-400 rounded-full" containerstyle="height: 35px; width: 35px;"
/> imgclass="bg-surface-400 rounded-full"
{/each} />
{/each}
{/await}
</div> </div>
</div> </div>
</div> </div>
@ -236,7 +216,7 @@
<div> <div>
<!-- Points color coding legend --> <!-- Points color coding legend -->
<!-- Use mt-3/mt-4 to account for 2x padding around the avatar. --> <!-- Use mt-3/mt-4 to account for 2x padding around the avatar. -->
<div class="mt-4 h-10"> <div class="mt-4 h-10 w-7 lg:w-36">
<div class="hidden h-5 text-sm font-bold lg:block">Points:</div> <div class="hidden h-5 text-sm font-bold lg:block">Points:</div>
<div <div
class="flex h-full flex-col overflow-hidden rounded-b-lg rounded-t-lg shadow lg:h-5 lg:flex-row lg:!rounded-l-lg lg:!rounded-r-lg lg:rounded-b-none lg:rounded-t-none" class="flex h-full flex-col overflow-hidden rounded-b-lg rounded-t-lg shadow lg:h-5 lg:flex-row lg:!rounded-l-lg lg:!rounded-r-lg lg:rounded-b-none lg:rounded-t-none"
@ -275,53 +255,57 @@
</div> </div>
</div> </div>
{#each data.raceresults as result} {#await data.races then races}
{@const race = getrace(result.race)} {#each data.raceresults as result}
{@const race = get_by_value(races, "id", result.race)}
<div <div
use:popup={race_popupsettings(race?.id ?? "Invalid")} use:popup={race_popupsettings(race?.id ?? "Invalid")}
class="card mt-2 flex h-20 w-7 flex-col !rounded-r-none bg-surface-300 p-2 shadow lg:w-36" class="card mt-2 flex h-20 w-7 flex-col !rounded-r-none bg-surface-300 p-2 shadow lg:w-36"
> >
<span class="hidden text-sm font-bold lg:block"> <span class="hidden text-sm font-bold lg:block">
{race?.step}: {race?.name} {race?.step}: {race?.name}
</span> </span>
<span class="block rotate-90 text-sm font-bold lg:hidden"> <span class="block rotate-90 text-sm font-bold lg:hidden">
{race?.name.slice(0, 8)}{(race?.name.length ?? 8) > 8 ? "." : ""} {race?.name.slice(0, 8)}{(race?.name.length ?? 8) > 8 ? "." : ""}
</span> </span>
<span class="hidden text-sm lg:block">Date: {formatdate(race?.racedate ?? "")}</span> <span class="hidden text-sm lg:block">Date: {formatdate(race?.racedate ?? "")}</span>
<span class="hidden text-sm lg:block">Guessed: P{race?.pxx}</span> <span class="hidden text-sm lg:block">Guessed: P{race?.pxx}</span>
</div>
<!-- The race result popup is triggered on click on the race -->
<div data-popup={race?.id ?? "Invalid"} class="card z-10 p-2 shadow">
<span class="font-bold">Result:</span>
<div class="mt-2 flex flex-col gap-1">
{#each result.pxxs as pxx, index}
{@const driver = getdriver(pxx)}
<div class="flex gap-2">
<span class="w-8">P{(race?.pxx ?? -100) - 3 + index}:</span>
<span class="badge w-10 p-1 text-center" style="background: {PXX_COLORS[index]};">
{driver?.code}
</span>
</div>
{/each}
{#if result.dnfs.length > 0}
<hr class="border-black" style="border-style: inset;" />
{/if}
{#each result.dnfs as dnf}
{@const driver = getdriver(dnf)}
<div class="flex gap-2">
<span class="w-8">DNF:</span>
<span class="badge w-10 p-1 text-center" style="background: {PXX_COLORS[3]};">
{driver?.code}
</span>
</div>
{/each}
</div> </div>
</div>
{/each} <!-- The race result popup is triggered on click on the race -->
<div data-popup={race?.id ?? "Invalid"} class="card z-10 p-2 shadow">
<span class="font-bold">Result:</span>
<div class="mt-2 flex flex-col gap-1">
{#await data.drivers then drivers}
{#each result.pxxs as pxx, index}
{@const driver = get_by_value(drivers, "id", pxx)}
<div class="flex gap-2">
<span class="w-8">P{(race?.pxx ?? -100) - 3 + index}:</span>
<span class="badge w-10 p-1 text-center" style="background: {PXX_COLORS[index]};">
{driver?.code}
</span>
</div>
{/each}
{#if result.dnfs.length > 0}
<hr class="border-black" style="border-style: inset;" />
{/if}
{#each result.dnfs as dnf}
{@const driver = get_by_value(drivers, "id", dnf)}
<div class="flex gap-2">
<span class="w-8">DNF:</span>
<span class="badge w-10 p-1 text-center" style="background: {PXX_COLORS[3]};">
{driver?.code}
</span>
</div>
{/each}
{/await}
</div>
</div>
{/each}
{/await}
</div> </div>
<div class="hide-scrollbar flex w-full overflow-x-scroll pb-2"> <div class="hide-scrollbar flex w-full overflow-x-scroll pb-2">
@ -337,13 +321,15 @@
> >
<!-- Avatar + name display at the top --> <!-- Avatar + name display at the top -->
<div class="mx-auto flex h-10 w-fit"> <div class="mx-auto flex h-10 w-fit">
<LazyImage {#await data.graphics then graphics}
src={graphicfallback(user.avatar_url, "driver_headshot_template")} <LazyImage
imgwidth={AVATAR_WIDTH} src={user.avatar_url ?? get_driver_headshot_template(graphics)}
imgheight={AVATAR_HEIGHT} imgwidth={AVATAR_WIDTH}
containerstyle="height: 40px; width: 40px;" imgheight={AVATAR_HEIGHT}
imgclass="bg-surface-400 rounded-full" containerstyle="height: 40px; width: 40px;"
/> imgclass="bg-surface-400 rounded-full"
/>
{/await}
<div <div
style="height: 40px; line-height: 40px;" style="height: 40px; line-height: 40px;"
class="ml-2 hidden text-nowrap text-center align-middle lg:block" class="ml-2 hidden text-nowrap text-center align-middle lg:block"
@ -352,34 +338,38 @@
</div> </div>
</div> </div>
{#each data.raceresults as result} {#await data.races then races}
{@const race = getrace(result.race)} {#await data.drivers then drivers}
{@const pick = picks.filter((pick: RacePick) => pick.race === race?.id)[0]} {#each data.raceresults as result}
{@const pxxcolor = PXX_COLORS[result.pxxs.indexOf(pick?.pxx ?? "Invalid")]} {@const race = get_by_value(races, "id", result.race)}
{@const dnfcolor = {@const pick = picks.filter((pick: RacePick) => pick.race === race?.id)[0]}
result.dnfs.indexOf(pick?.dnf ?? "Invalid") >= 0 ? PXX_COLORS[3] : PXX_COLORS[-1]} {@const pxxcolor = PXX_COLORS[result.pxxs.indexOf(pick?.pxx ?? "Invalid")]}
{@const dnfcolor =
result.dnfs.indexOf(pick?.dnf ?? "Invalid") >= 0 ? PXX_COLORS[3] : PXX_COLORS[-1]}
{#if pick} {#if pick}
<div class="mt-2 h-20 w-full border bg-surface-300 p-1 lg:p-2"> <div class="mt-2 h-20 w-full border bg-surface-300 p-1 lg:p-2">
<div class="mx-auto flex h-full w-fit flex-col justify-evenly"> <div class="mx-auto flex h-full w-fit flex-col justify-evenly">
<span <span
class="p-1 text-center text-sm rounded-container-token" class="p-1 text-center text-sm rounded-container-token"
style="background: {pxxcolor};" style="background: {pxxcolor};"
> >
{getdriver(pick?.pxx ?? "")?.code} {get_by_value(drivers, "id", pick?.pxx ?? "")?.code}
</span> </span>
<span <span
class="p-1 text-center text-sm rounded-container-token" class="p-1 text-center text-sm rounded-container-token"
style="background: {dnfcolor};" style="background: {dnfcolor};"
> >
{getdriver(pick?.dnf ?? "")?.code} {get_by_value(drivers, "id", pick?.dnf ?? "")?.code}
</span> </span>
</div> </div>
</div> </div>
{:else} {:else}
<div class="mt-2 h-20 w-full"></div> <div class="mt-2 h-20 w-full"></div>
{/if} {/if}
{/each} {/each}
{/await}
{/await}
</div> </div>
{/each} {/each}
</div> </div>