Compare commits

..

7 Commits

12 changed files with 238 additions and 407 deletions

View File

@ -31,10 +31,12 @@
driver = meta.driver;
}
const required: boolean = $derived(!driver);
const disabled: boolean = $derived(!data.admin);
// Constants
const labelwidth: string = "120px";
// Reactive state
let required: boolean = $derived(!driver);
let disabled: boolean = $derived(!data.admin);
let team_select_value: string = $state(driver?.team ?? "");
let active_value: boolean = $state(driver?.active ?? true);
</script>
@ -92,7 +94,7 @@
{#await data.teams then teams}
<Dropdown
name="team"
input_variable={team_select_value}
bind:value={team_select_value}
options={team_dropdown_options(teams)}
{labelwidth}
{disabled}

View File

@ -3,10 +3,10 @@
import { FileDropzone, getModalStore, type ModalStore } from "@skeletonlabs/skeleton";
import { Button, Card, Input } from "$lib/components";
import type { Race, SkeletonData } from "$lib/schema";
import { format } from "date-fns";
import { RACE_PICTOGRAM_HEIGHT, RACE_PICTOGRAM_WIDTH } from "$lib/config";
import { enhance } from "$app/forms";
import { get_race_pictogram_template } from "$lib/database";
import { format_date } from "$lib/date";
interface RaceCardProps {
/** Data passed from the page context */
@ -26,29 +26,34 @@
race = meta.race;
}
// Constants
const labelwidth = "80px";
const dateformat: string = "yyyy-MM-dd'T'HH:mm";
const clear_sprint = () => {
(document.getElementById("sqdate") as HTMLInputElement).value = "";
(document.getElementById("sdate") as HTMLInputElement).value = "";
};
const required: boolean = $derived(!race);
const disabled: boolean = $derived(!data.admin);
const labelwidth = "80px";
// Reactive state
let required: boolean = $derived(!race);
let disabled: boolean = $derived(!data.admin);
let name_value: string = $state(race?.name ?? "");
let step_value: string = $state(race?.step.toString() ?? "");
let pxx_value: string = $state(race?.pxx.toString() ?? "");
let sprintqualidate_value: string = $state("");
let sprintdate_value: string = $state("");
let qualidate_value: string = $state("");
let racedate_value: string = $state("");
// Dates have to be formatted to datetime-local format
const dateformat: string = "yyyy-MM-dd'T'HH:mm";
const sprintqualidate: string | undefined = $derived(
race?.sprintqualidate ? format(new Date(race.sprintqualidate), dateformat) : undefined,
);
const sprintdate: string | undefined = $derived(
race?.sprintdate ? format(new Date(race.sprintdate), dateformat) : undefined,
);
const qualidate: string | undefined = $derived(
race ? format(new Date(race.qualidate), dateformat) : undefined,
);
const racedate: string | undefined = $derived(
race ? format(new Date(race.racedate), dateformat) : undefined,
);
if (race) {
if (race.sprintqualidate && race.sprintdate) {
sprintqualidate_value = format_date(race.sprintqualidate, dateformat);
sprintdate_value = format_date(race.sprintdate, dateformat);
qualidate_value = format_date(race.qualidate, dateformat);
racedate_value = format_date(race.racedate, dateformat);
}
}
</script>
{#await data.graphics then graphics}
@ -76,7 +81,7 @@
<!-- Driver name input -->
<Input
name="name"
value={race?.name ?? ""}
bind:value={name_value}
autocomplete="off"
{labelwidth}
{disabled}
@ -86,7 +91,7 @@
</Input>
<Input
name="step"
value={race?.step ?? ""}
bind:value={step_value}
autocomplete="off"
type="number"
min={1}
@ -99,7 +104,7 @@
</Input>
<Input
name="pxx"
value={race?.pxx ?? ""}
bind:value={pxx_value}
autocomplete="off"
type="number"
min={1}
@ -115,7 +120,7 @@
<Input
id="sqdate"
name="sprintqualidate"
value={sprintqualidate ?? ""}
bind:value={sprintqualidate_value}
autocomplete="off"
type="datetime-local"
{labelwidth}
@ -126,7 +131,7 @@
<Input
id="sdate"
name="sprintdate"
value={sprintdate ?? ""}
bind:value={sprintdate_value}
autocomplete="off"
type="datetime-local"
{labelwidth}
@ -136,7 +141,7 @@
</Input>
<Input
name="qualidate"
value={qualidate ?? ""}
bind:value={qualidate_value}
autocomplete="off"
type="datetime-local"
{labelwidth}
@ -147,7 +152,7 @@
</Input>
<Input
name="racedate"
value={racedate ?? ""}
bind:value={racedate_value}
autocomplete="off"
type="datetime-local"
{labelwidth}

View File

@ -10,7 +10,6 @@
Substitution,
} from "$lib/schema";
import { get_by_value, get_driver_headshot_template } from "$lib/database";
import type { Action } from "svelte/action";
import { getModalStore, type ModalStore } from "@skeletonlabs/skeleton";
import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config";
import { driver_dropdown_options } from "$lib/dropdown";
@ -39,53 +38,52 @@
racepick = meta.racepick;
}
// This is executed on mount of the element specifying the "action"
const register_pxx_preview_handler: Action = (node: HTMLElement) => {
node.addEventListener("DropdownChange", update_pxx_preview);
};
// Await promises
let drivers: Driver[] | undefined = $state(undefined);
data.drivers.then((d: Driver[]) => (drivers = d));
// This event handler is registered to the Dropdown's <input> element through the action above.
const update_pxx_preview = async (event: Event) => {
const target: HTMLInputElement = event.target as HTMLInputElement;
let substitutions: Substitution[] | undefined = $state(undefined);
data.substitutions.then((s: Substitution[]) => (substitutions = s));
const src: string =
get_by_value<Driver>(await data.drivers, "code", target.value)?.headshot_url || "";
const img = document.getElementById("headshot_preview") as HTMLImageElement;
// Can be null if lazyimg not loaded
if (img) img.src = src;
};
const required: boolean = $derived(!racepick);
const disabled: boolean = $derived(!data.admin);
// Constants
const labelwidth: string = "60px";
const active_drivers_and_substitutes: Promise<Driver[]> = $derived.by(async () => {
// Reactive state
let required: boolean = $derived(!racepick);
let disabled: boolean = $derived(!data.admin);
let pxx_select_value: string = $state(racepick?.pxx ?? "");
let dnf_select_value: string = $state(racepick?.dnf ?? "");
let active_drivers_and_substitutes: Driver[] = $derived.by(() => {
if (!data.currentrace) return [];
let drivers: Driver[] = await data.drivers;
let substitutions: Substitution[] = await data.substitutions;
let active_and_substitutes: Driver[] = (drivers ?? []).filter(
(driver: Driver) => driver.active,
);
let active_and_substitutes: Driver[] = drivers.filter((driver: Driver) => driver.active);
substitutions
(substitutions ?? [])
.filter((substitution: Substitution) => substitution.race === data.currentrace?.id)
.forEach((substitution: Substitution) => {
const for_index = active_and_substitutes.findIndex(
(driver: Driver) => driver.id === substitution.for,
);
const sub_index = drivers.findIndex(
const sub_index = (drivers ?? []).findIndex(
(driver: Driver) => driver.id === substitution.substitute,
);
active_and_substitutes[for_index] = drivers[sub_index];
active_and_substitutes[for_index] = (drivers ?? [])[sub_index];
});
return active_and_substitutes.sort((a: Driver, b: Driver) => a.code.localeCompare(b.code));
});
let pxx_select_value: string = $state(racepick?.pxx ?? "");
let dnf_select_value: string = $state(racepick?.dnf ?? "");
// Update preview
$effect(() => {
if (!drivers) return;
const src: string = get_by_value(drivers, "id", pxx_select_value)?.headshot_url ?? "";
const img: HTMLImageElement = document.getElementById("headshot_preview") as HTMLImageElement;
if (img) img.src = src;
});
</script>
{#await Promise.all([data.graphics, data.drivers]) then [graphics, drivers]}
@ -115,33 +113,28 @@
<div class="flex flex-col gap-2">
<!-- PXX select -->
{#await active_drivers_and_substitutes then pxx_drivers}
<Dropdown
name="pxx"
input_variable={pxx_select_value}
action={register_pxx_preview_handler}
options={driver_dropdown_options(pxx_drivers)}
bind:value={pxx_select_value}
options={driver_dropdown_options(active_drivers_and_substitutes)}
{labelwidth}
{disabled}
{required}
>
P{data.currentrace?.pxx ?? "XX"}
</Dropdown>
{/await}
<!-- DNF select -->
{#await active_drivers_and_substitutes then pxx_drivers}
<Dropdown
name="dnf"
input_variable={dnf_select_value}
options={driver_dropdown_options(pxx_drivers)}
bind:value={dnf_select_value}
options={driver_dropdown_options(active_drivers_and_substitutes)}
{labelwidth}
{disabled}
{required}
>
DNF
</Dropdown>
{/await}
<!-- Save/Delete buttons -->
<div class="flex justify-end gap-2">

View File

@ -30,16 +30,28 @@
result = meta.result;
}
const required: boolean = $derived(!result);
const disabled: boolean = $derived(!data.admin);
let races: Race[] | undefined = $state(undefined);
data.races.then((r: Race[]) => (races = r));
let drivers: Driver[] | undefined = $state(undefined);
data.drivers.then((d: Driver[]) => (drivers = d));
// Constants
const labelwidth: string = "70px";
// Reactive state
let required: boolean = $derived(!result);
let disabled: boolean = $derived(!data.admin);
let race_select_value: string = $state(result?.race ?? "");
// TODO: Currentrace needs to be updated once a race is selected
// This way it doesn't update the placeholder (or the chips)...
const currentrace: Promise<Race | undefined> = $derived.by(async () =>
get_by_value(await data.races, "id", race_select_value),
let currentrace: Race | undefined = $derived(
get_by_value<Race>(races ?? [], "id", race_select_value) ?? undefined,
);
let pxxs_placeholder: string = $derived(
currentrace
? `Select P${(currentrace.pxx ?? -10) - 3} to P${(currentrace.pxx ?? -10) + 3}...`
: `Select race first...`,
);
let pxxs_input: string = $state("");
@ -48,12 +60,24 @@
let dnfs_input: string = $state("");
let dnfs_chips: string[] = $state([]);
// Set the pxxs/dnfs states once the drivers are loaded
data.drivers.then(async (drivers: Driver[]) => {
pxxs_chips =
result?.pxxs.map(
(id: string, index: number) =>
`P${(currentrace?.pxx ?? -10) + index - 3}: ${get_by_value(drivers, "id", id)?.code ?? "Invalid"}`,
) ?? [];
dnfs_chips =
result?.dnfs.map((id: string) => get_by_value(drivers, "id", id)?.code ?? "Invalid") ?? [];
});
// This is the actual data that gets sent through the form
let pxxs_ids: string[] = $state(result?.pxxs ?? []);
let dnfs_ids: string[] = $state(result?.dnfs ?? []);
const pxxs_options: Promise<AutocompleteOption<string>[]> = $derived.by(async () =>
(await data.drivers)
let pxxs_options: AutocompleteOption<string>[] = $derived.by(() =>
(drivers ?? [])
.filter((driver: Driver) => {
// Filter out all drivers that are already selected
return !pxxs_ids.includes(driver.id);
@ -68,8 +92,8 @@
}),
);
const dnfs_options: Promise<AutocompleteOption<string>[]> = $derived.by(async () =>
(await data.drivers).map((driver: Driver) => {
let dnfs_options: AutocompleteOption<string>[] = $derived.by(() =>
(drivers ?? []).map((driver: Driver) => {
return {
// NOTE: Because Skeleton displays the values inside the autocomplete input,
// we have to supply the driver code twice and manage a list of ids manually (ugh)
@ -79,15 +103,21 @@
}),
);
const pxxs_whitelist: Promise<string[]> = $derived.by(async () =>
(await data.drivers).map((driver: Driver) => {
let pxxs_whitelist: string[] = $derived.by(() =>
(drivers ?? []).map((driver: Driver) => {
return driver.code;
}),
);
const on_pxxs_chip_select = async (
event: CustomEvent<AutocompleteOption<string>>,
): Promise<void> => {
// Event handlers
const on_race_select = (event: Event): void => {
pxxs_chips = pxxs_chips.map(
(label: string, index: number) =>
`P${(currentrace?.pxx ?? -10) + index - 3}: ${label.split(" ").pop()}`,
);
};
const on_pxxs_chip_select = (event: CustomEvent<AutocompleteOption<string>>): void => {
if (disabled) return;
// Can only select 7 drivers
@ -97,33 +127,26 @@
if (pxxs_chips.some((label: string) => label.endsWith(event.detail.value))) return;
// Manage labels that are displayed
pxxs_chips.push(
`P${((await currentrace)?.pxx ?? -10) + pxxs_chips.length - 3}: ${event.detail.value}`,
);
pxxs_chips.push(`P${(currentrace?.pxx ?? -10) + pxxs_chips.length - 3}: ${event.detail.value}`);
pxxs_input = "";
// Manage ids that are submitted via form
const id: string =
get_by_value(await data.drivers, "code", event.detail.value)?.id ?? "Invalid";
const id: string = get_by_value(drivers ?? [], "code", event.detail.value)?.id ?? "Invalid";
if (!pxxs_ids.includes(id)) {
pxxs_ids.push(id);
}
};
const on_pxxs_chip_remove = async (event: CustomEvent): Promise<void> => {
const on_pxxs_chip_remove = (event: CustomEvent): void => {
pxxs_ids.splice(event.detail.chipIndex, 1);
pxxs_chips = await Promise.all(
pxxs_chips.map(
async (label: string, index: number) =>
`P${((await currentrace)?.pxx ?? -10) + index - 3}: ${label.split(" ").pop()}`,
),
pxxs_chips = pxxs_chips.map(
(label: string, index: number) =>
`P${(currentrace?.pxx ?? -10) + index - 3}: ${label.split(" ").pop()}`,
);
};
const on_dnfs_chip_select = async (
event: CustomEvent<AutocompleteOption<string>>,
): Promise<void> => {
const on_dnfs_chip_select = (event: CustomEvent<AutocompleteOption<string>>): void => {
if (disabled) return;
// Can only select a driver once
@ -134,29 +157,15 @@
dnfs_input = "";
// Manage ids that are submitted via form
const id: string =
get_by_value(await data.drivers, "code", event.detail.value)?.id ?? "Invalid";
const id: string = get_by_value(drivers ?? [], "code", event.detail.value)?.id ?? "Invalid";
if (!dnfs_ids.includes(id)) {
dnfs_ids.push(id);
}
};
const on_dnfs_chip_remove = async (event: CustomEvent): Promise<void> => {
const on_dnfs_chip_remove = (event: CustomEvent): void => {
dnfs_ids.splice(event.detail.chipIndex, 1);
};
// Set the pxxs/dnfs states once the drivers are loaded
data.drivers.then(async (drivers: Driver[]) => {
pxxs_chips = await Promise.all(
result?.pxxs.map(
async (id: string, index: number) =>
`P${((await currentrace)?.pxx ?? -10) + index - 3}: ${get_by_value(drivers, "id", id)?.code ?? "Invalid"}`,
) ?? [],
);
dnfs_chips =
result?.dnfs.map((id: string) => get_by_value(drivers, "id", id)?.code ?? "Invalid") ?? [];
});
</script>
<Card width="w-full sm:w-[512px]">
@ -179,8 +188,9 @@
{#await data.races then races}
<Dropdown
name="race"
input_variable={race_select_value}
bind:value={race_select_value}
options={race_dropdown_options(races)}
onchange={on_race_select}
{labelwidth}
{disabled}
{required}
@ -191,52 +201,44 @@
<div class="mt-2 flex flex-col gap-2">
<!-- PXXs autocomplete chips -->
{#await Promise.all([currentrace, pxxs_whitelist]) then [current, whitelist]}
<InputChip
bind:input={pxxs_input}
bind:value={pxxs_chips}
{whitelist}
whitelist={pxxs_whitelist}
allowUpperCase
placeholder="Select P{(current?.pxx ?? -10) - 3} to P{(current?.pxx ?? -10) + 3}..."
placeholder={pxxs_placeholder}
name="pxxs_codes"
{disabled}
{required}
on:remove={on_pxxs_chip_remove}
/>
{/await}
<div class="card max-h-48 w-full overflow-y-auto p-2" tabindex="-1">
{#await pxxs_options then options}
<Autocomplete
bind:input={pxxs_input}
{options}
options={pxxs_options}
denylist={pxxs_chips}
on:selection={on_pxxs_chip_select}
/>
{/await}
</div>
<!-- DNFs autocomplete chips -->
{#await pxxs_whitelist then whitelist}
<InputChip
bind:input={dnfs_input}
bind:value={dnfs_chips}
{whitelist}
whitelist={pxxs_whitelist}
allowUpperCase
placeholder="Select DNFs..."
name="dnfs_codes"
{disabled}
on:remove={on_dnfs_chip_remove}
/>
{/await}
<div class="card max-h-48 w-full overflow-y-auto p-2" tabindex="-1">
{#await dnfs_options then options}
<Autocomplete
bind:input={dnfs_input}
{options}
options={dnfs_options}
denylist={dnfs_chips}
on:selection={on_dnfs_chip_select}
/>
{/await}
</div>
<!-- Save/Delete buttons -->

View File

@ -1,8 +1,7 @@
<script lang="ts">
import { Card, Button, Dropdown } from "$lib/components";
import type { Driver, Race, SkeletonData, Substitution } from "$lib/schema";
import type { Driver, SkeletonData, Substitution } from "$lib/schema";
import { get_by_value, get_driver_headshot_template } from "$lib/database";
import type { Action } from "svelte/action";
import { getModalStore, type ModalStore } from "@skeletonlabs/skeleton";
import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config";
import { driver_dropdown_options, race_dropdown_options } from "$lib/dropdown";
@ -26,34 +25,29 @@
substitution = meta.substitution;
}
// This is executed on mount of the element specifying the "action"
const register_substitute_preview_handler: Action = (node: HTMLElement) =>
node.addEventListener("DropdownChange", update_substitute_preview);
// Await promises
let drivers: Driver[] | undefined = $state(undefined);
data.drivers.then((d: Driver[]) => (drivers = d));
// This event handler is registered to the Dropdown's <input> element through the action above.
const update_substitute_preview = async (event: Event) => {
const target: HTMLInputElement = event.target as HTMLInputElement;
const src: string =
get_by_value<Driver>(await data.drivers, "code", target.value)?.headshot_url ?? "";
const img = document.getElementById("headshot_preview") as HTMLImageElement;
// Can be null if lazyimage hasn't loaded
if (img) img.src = src;
};
const active_drivers = (drivers: Driver[]): Driver[] =>
drivers.filter((driver: Driver) => driver.active);
const inactive_drivers = (drivers: Driver[]): Driver[] =>
drivers.filter((driver: Driver) => !driver.active);
const required: boolean = $derived(!substitution);
const disabled: boolean = $derived(!data.admin);
// Constants
const labelwidth: string = "120px";
let substitute_select_value: string = $state(substitution?.substitute ?? "");
let driver_select_value: string = $state(substitution?.for ?? "");
let race_select_value: string = $state(substitution?.race ?? "");
// Reactive state
let required: boolean = $derived(!substitution);
let disabled: boolean = $derived(!data.admin);
let active_drivers: Driver[] = $derived((drivers ?? []).filter((d: Driver) => d.active));
let inactive_drivers: Driver[] = $derived((drivers ?? []).filter((d: Driver) => !d.active));
let substitute_value: string = $state(substitution?.substitute ?? "");
let driver_value: string = $state(substitution?.for ?? "");
let race_value: string = $state(substitution?.race ?? "");
// Update preview
$effect(() => {
if (!drivers) return;
const src: string = get_by_value(drivers, "id", substitute_value)?.headshot_url ?? "";
const img: HTMLImageElement = document.getElementById("headshot_preview") as HTMLImageElement;
if (img) img.src = src;
});
</script>
{#await Promise.all([data.graphics, data.drivers]) then [graphics, drivers]}
@ -82,9 +76,8 @@
<!-- Substitute select -->
<Dropdown
name="substitute"
input_variable={substitute_select_value}
action={register_substitute_preview_handler}
options={driver_dropdown_options(inactive_drivers(drivers))}
bind:value={substitute_value}
options={driver_dropdown_options(inactive_drivers)}
{labelwidth}
{disabled}
{required}
@ -95,8 +88,8 @@
<!-- Driver select -->
<Dropdown
name="for"
input_variable={driver_select_value}
options={driver_dropdown_options(active_drivers(drivers))}
bind:value={driver_value}
options={driver_dropdown_options(active_drivers)}
{labelwidth}
{disabled}
{required}
@ -108,7 +101,7 @@
{#await data.races then races}
<Dropdown
name="race"
input_variable={race_select_value}
bind:value={race_value}
options={race_dropdown_options(races)}
{labelwidth}
{disabled}

View File

@ -25,11 +25,14 @@
team = meta.team;
}
const required: boolean = $derived(!team);
const disabled: boolean = $derived(!data.admin);
// Constants
const labelwidth: string = "110px";
let colorpreview: string = $state(team?.color ?? "Invalid");
// Reactive state
let required: boolean = $derived(!team);
let disabled: boolean = $derived(!data.admin);
let name_value: string = $state(team?.name ?? "");
let color_value: string = $state(team?.color ?? "");
</script>
{#await data.graphics then graphics}
@ -57,7 +60,7 @@
<!-- Team name input -->
<Input
name="name"
value={team?.name ?? ""}
bind:value={name_value}
autocomplete="off"
{labelwidth}
{disabled}
@ -69,20 +72,17 @@
<!-- Team color input -->
<Input
name="color"
value={team?.color ?? ""}
bind:value={color_value}
autocomplete="off"
placeholder="Enter as '#XXXXXX'"
minlength={7}
maxlength={7}
oninput={(event: Event) => {
colorpreview = (event.target as HTMLInputElement).value;
}}
{labelwidth}
{disabled}
{required}
>
Color
<span class="badge ml-2 border" style="color: {colorpreview}; background: {colorpreview}">
<span class="badge ml-2 border" style="color: {color_value}; background: {color_value}">
C
</span>
</Input>

View File

@ -1,34 +1,16 @@
<script lang="ts">
import { ListBox, ListBoxItem, popup, type PopupSettings } from "@skeletonlabs/skeleton";
import type { Snippet } from "svelte";
import type { Action } from "svelte/action";
import type { HTMLInputAttributes } from "svelte/elements";
import { v4 as uuid } from "uuid";
import { type DropdownOption, LazyImage } from "$lib/components";
import type { HTMLSelectAttributes } from "svelte/elements";
import { type DropdownOption } from "$lib/components";
interface DropdownProps extends HTMLInputAttributes {
interface DropdownProps extends HTMLSelectAttributes {
children: Snippet;
/** Placeholder for the empty input element */
placeholder?: string;
/** Form name of the input element, to reference input data after form submission */
name?: string;
/** Manually set the label width, to align multiple inputs vertically. Supply value in CSS units. */
labelwidth?: string;
/** The variable to bind to the input element. Has to be a [$state] so its value can be updated with the input element's contents. */
input_variable: string;
/** Any action to bind to the input element */
action?: Action;
/** The ID of the popup to trigger. UUID by default. */
popup_id?: string;
/** The [PopupSettings] object for the popup to trigger. */
popup_settings?: PopupSettings;
value?: string;
/** The options this autocomplete component allows to choose from.
* Example: [[{ label: "Aston", value: "0" }, { label: "VCARB", value: "1" }]].
@ -38,42 +20,11 @@
let {
children,
placeholder = "",
name = "",
labelwidth = "auto",
input_variable,
action = undefined,
popup_id = uuid(),
popup_settings = {
event: "click",
target: popup_id,
placement: "bottom",
closeQuery: ".listbox-item",
},
value = $bindable(),
options,
...restProps
}: DropdownProps = $props();
/** Find the "label" of an option by its "value" */
const get_label = (value: string): string | undefined => {
return options.find((o) => o.value === value)?.label;
};
// Use an action to fill the "input" variable
// required to dispatch the custom event using $effect
let input: HTMLInputElement | undefined = undefined;
const obtain_input: Action = (node: HTMLElement) => {
input = node as HTMLInputElement;
};
// This will run everyting "input_variable" changes.
// The event is fired when the input's value is updated via JavaScript.
$effect(() => {
// Just list this so SvelteKit picks it up as dependency
input_variable;
if (input) input.dispatchEvent(new CustomEvent("DropdownChange"));
});
</script>
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
@ -83,55 +34,11 @@
>
{@render children()}
</div>
<!-- TODO: How to assign use: conditionally? I don't wan't to repeat the entire input... -->
{#if action}
<input
use:popup={popup_settings}
type="button"
autocomplete="off"
style="height: 42px; text-align: start; text-indent: 12px; border-top-left-radius: 0; border-bottom-left-radius: 0;"
use:obtain_input
use:action
onkeypress={(event: Event) => event.preventDefault()}
value={get_label(input_variable) ?? placeholder}
{...restProps}
/>
{:else}
<input
use:popup={popup_settings}
type="button"
autocomplete="off"
style="height: 42px; text-align: start; text-indent: 12px; border-top-left-radius: 0; border-bottom-left-radius: 0;"
use:obtain_input
onkeypress={(event: Event) => event.preventDefault()}
value={get_label(input_variable) ?? placeholder}
{...restProps}
/>
{/if}
</div>
<div
data-popup={popup_id}
class="card z-10 w-auto overflow-y-scroll p-2 shadow"
style="max-height: 350px;"
>
<ListBox>
<select bind:value class="!outline-none" {...restProps}>
{#each options as option}
<ListBoxItem bind:group={input_variable} {name} value={option.value}>
<div class="flex flex-nowrap">
{#if option.icon_url}
<LazyImage
src={option.icon_url}
alt=""
class="rounded"
style="height: 24px;"
imgwidth={option.icon_width ?? 0}
imgheight={option.icon_height ?? 0}
/>
{/if}
<span class="ml-2">{option.label}</span>
</div>
</ListBoxItem>
<option value={option.value} selected={value === option.value}>
{option.label}
</option>
{/each}
</ListBox>
</select>
</div>

View File

@ -8,11 +8,20 @@
/** Manually set the label width, to align multiple inputs vertically. Supply value in CSS units. */
labelwidth?: string;
/** The variable to bind to the input element. Has to be a [$state] so its value can be updated with the input element's contents. */
value?: string;
/** The type of the input element, e.g. "text". */
type?: string;
}
let { children, labelwidth = "auto", type = "text", ...restProps }: InputProps = $props();
let {
children,
labelwidth = "auto",
value = $bindable(),
type = "text",
...restProps
}: InputProps = $props();
</script>
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
@ -22,5 +31,5 @@
>
{@render children()}
</div>
<input {type} {...restProps} />
<input bind:value class="!outline-none" {type} {...restProps} />
</div>

View File

@ -1,83 +0,0 @@
<script lang="ts">
import {
Autocomplete,
popup,
type AutocompleteOption,
type PopupSettings,
} from "@skeletonlabs/skeleton";
import type { Snippet } from "svelte";
import { v4 as uuid } from "uuid";
interface SearchProps {
children: Snippet;
/** Placeholder for the empty input element */
placeholder?: string;
/** Form name of the input element, to reference input data after form submission */
name?: string;
/** Manually set the label width, to align multiple inputs vertically. Supply value in CSS units. */
labelwidth?: string;
/** The variable to bind to the input element. Has to be a [$state] so its value can be updated with the input element's contents. */
input_variable: string;
/** The ID of the input element. UUID by default. */
input_id?: string;
/** The ID of the popup to trigger. UUID by default. */
popup_id?: string;
/** The [PopupSettings] object for the popup to trigger. */
popup_settings?: PopupSettings;
/** The event handler updating the [input_variable] after selection. */
selection_handler?: (event: CustomEvent<AutocompleteOption<string>>) => void;
/** The options this autocomplete component allows to choose from.
* Example: [[{ label: "Aston", value: "0" }, { label: "VCARB", value: "1" }]].
*/
options: AutocompleteOption<string, unknown>[];
}
let {
children,
placeholder = "",
name = "",
labelwidth = "auto",
input_variable,
input_id = uuid(),
popup_id = uuid(),
popup_settings = {
event: "focus-click",
target: popup_id,
placement: "bottom",
},
selection_handler = (event: CustomEvent<AutocompleteOption<string>>): void => {
input_variable = event.detail.label;
},
options,
}: SearchProps = $props();
</script>
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
<div
class="input-group-shim select-none text-nowrap text-neutral-900"
style="width: {labelwidth};"
>
{@render children()}
</div>
<input
id={input_id}
type="search"
{placeholder}
{name}
bind:value={input_variable}
use:popup={popup_settings}
/>
</div>
<div data-popup={popup_id} class="card z-10 w-auto p-2 shadow" tabindex="-1">
<Autocomplete bind:input={input_variable} {options} on:selection={selection_handler} />
</div>

View File

@ -6,7 +6,6 @@ import Table from "./Table.svelte";
import Button from "./form/Button.svelte";
import Dropdown from "./form/Dropdown.svelte";
import Input from "./form/Input.svelte";
import Search from "./form/Search.svelte";
import Card from "./cards/Card.svelte";
import DriverCard from "./cards/DriverCard.svelte";
@ -37,7 +36,6 @@ export {
Button,
Dropdown,
Input,
Search,
// Cards
Card,

4
src/lib/date.ts Normal file
View File

@ -0,0 +1,4 @@
import { format } from "date-fns";
export const format_date = (date: string, formatstring: string): string =>
format(new Date(date), formatstring);

View File

@ -19,9 +19,9 @@
RACE_PICTOGRAM_HEIGHT,
RACE_PICTOGRAM_WIDTH,
} from "$lib/config";
import { format_date } from "$lib/date";
import type { CurrentPickedUser, RacePick } from "$lib/schema";
import { get_by_value, get_driver_headshot_template } from "$lib/database";
import { format } from "date-fns";
let { data }: { data: PageData } = $props();
@ -63,7 +63,6 @@
);
const dateformat: string = "dd.MM' 'HH:mm";
const formatdate = (date: string): string => format(new Date(date), dateformat);
const race_popupsettings = (target: string): PopupSettings => {
return {
@ -96,20 +95,20 @@
{#if data.currentrace.sprintdate}
<div class="flex gap-2">
<span class="w-12">SQuali:</span>
<span>{formatdate(data.currentrace.sprintqualidate)}</span>
<span>{format_date(data.currentrace.sprintqualidate, dateformat)}</span>
</div>
<div class="flex gap-2">
<span class="w-12">SRace:</span>
<span>{formatdate(data.currentrace.sprintdate)}</span>
<span>{format_date(data.currentrace.sprintdate, dateformat)}</span>
</div>
{/if}
<div class="flex gap-2">
<span class="w-12">Quali:</span>
<span>{formatdate(data.currentrace.qualidate)}</span>
<span>{format_date(data.currentrace.qualidate, dateformat)}</span>
</div>
<div class="flex gap-2">
<span class="w-12">Race:</span>
<span>{formatdate(data.currentrace.racedate)}</span>
<span>{format_date(data.currentrace.racedate, dateformat)}</span>
</div>
<div class="m-auto flex">
<div class="mr-1 mt-1">
@ -265,7 +264,9 @@
<span class="block rotate-90 text-sm font-bold lg:hidden">
{race?.name.slice(0, 8)}{(race?.name.length ?? 8) > 8 ? "." : ""}
</span>
<span class="hidden text-sm lg:block">Date: {formatdate(race?.racedate ?? "")}</span>
<span class="hidden text-sm lg:block">
Date: {format_date(race?.racedate ?? "", dateformat)}
</span>
<span class="hidden text-sm lg:block">Guessed: P{race?.pxx}</span>
</div>