Lib: Move cards into cards/ directory
This commit is contained in:
159
src/lib/components/cards/DriverCard.svelte
Normal file
159
src/lib/components/cards/DriverCard.svelte
Normal file
@ -0,0 +1,159 @@
|
||||
<script lang="ts">
|
||||
import { get_image_preview_event_handler } from "$lib/image";
|
||||
import { FileDropzone, SlideToggle } from "@skeletonlabs/skeleton";
|
||||
import { Button, Input, LazyCard, LazyDropdown, type LazyDropdownOption } from "$lib/components";
|
||||
import type { Driver } from "$lib/schema";
|
||||
import {
|
||||
DRIVER_CARD_ASPECT_HEIGHT,
|
||||
DRIVER_CARD_ASPECT_WIDTH,
|
||||
DRIVER_HEADSHOT_HEIGHT,
|
||||
DRIVER_HEADSHOT_WIDTH,
|
||||
} from "$lib/config";
|
||||
|
||||
interface DriverCardProps {
|
||||
/** The [Driver] object used to prefill values. */
|
||||
driver?: Driver | undefined;
|
||||
|
||||
/** Disable all inputs if [true] */
|
||||
disable_inputs?: boolean;
|
||||
|
||||
/** Require all inputs if [true] */
|
||||
require_inputs?: boolean;
|
||||
|
||||
/** The [src] of the driver headshot template preview */
|
||||
headshot_template?: string;
|
||||
|
||||
/** The value this component's team select dropdown will bind to */
|
||||
team_select_value: string;
|
||||
|
||||
/** The options this component's team select dropdown will display */
|
||||
team_select_options: LazyDropdownOption[];
|
||||
|
||||
/** The value this component's active switch will bind to */
|
||||
active_value: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
driver = undefined,
|
||||
disable_inputs = false,
|
||||
require_inputs = false,
|
||||
headshot_template = undefined,
|
||||
team_select_value,
|
||||
team_select_options,
|
||||
active_value,
|
||||
}: DriverCardProps = $props();
|
||||
</script>
|
||||
|
||||
<LazyCard
|
||||
cardwidth={DRIVER_CARD_ASPECT_WIDTH}
|
||||
cardheight={DRIVER_CARD_ASPECT_HEIGHT}
|
||||
imgsrc={driver?.headshot_url ?? headshot_template}
|
||||
imgwidth={DRIVER_HEADSHOT_WIDTH}
|
||||
imgheight={DRIVER_HEADSHOT_HEIGHT}
|
||||
imgid="update_driver_headshot_preview_{driver?.id ?? 'create'}"
|
||||
>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<!-- This is also disabled, because the ID should only be -->
|
||||
<!-- "leaked" to users that are allowed to use the inputs -->
|
||||
{#if driver && !disable_inputs}
|
||||
<input name="id" type="hidden" value={driver.id} />
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- Driver name input -->
|
||||
<Input
|
||||
id="driver_first_name_{driver?.id ?? 'create'}"
|
||||
name="firstname"
|
||||
value={driver?.firstname ?? ""}
|
||||
autocomplete="off"
|
||||
labelwidth="120px"
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}
|
||||
>First Name
|
||||
</Input>
|
||||
<Input
|
||||
id="driver_last_name_{driver?.id ?? 'create'}"
|
||||
name="lastname"
|
||||
value={driver?.lastname ?? ""}
|
||||
autocomplete="off"
|
||||
labelwidth="120px"
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}
|
||||
>Last Name
|
||||
</Input>
|
||||
<Input
|
||||
id="driver_code_{driver?.id ?? 'create'}"
|
||||
name="code"
|
||||
value={driver?.code ?? ""}
|
||||
autocomplete="off"
|
||||
minlength={3}
|
||||
maxlength={3}
|
||||
labelwidth="120px"
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}
|
||||
>Driver Code
|
||||
</Input>
|
||||
|
||||
<!-- Driver team input -->
|
||||
<LazyDropdown
|
||||
name="team"
|
||||
input_variable={team_select_value}
|
||||
options={team_select_options}
|
||||
labelwidth="120px"
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}
|
||||
>Team
|
||||
</LazyDropdown>
|
||||
|
||||
<!-- Headshot upload -->
|
||||
<FileDropzone
|
||||
name="headshot"
|
||||
id="driver_headshot_{driver?.id ?? 'create'}"
|
||||
onchange={get_image_preview_event_handler(
|
||||
`update_driver_headshot_preview_${driver?.id ?? "create"}`,
|
||||
)}
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}
|
||||
>
|
||||
<svelte:fragment slot="message"><b>Upload Headshot</b></svelte:fragment>
|
||||
</FileDropzone>
|
||||
|
||||
<!-- Save/Delete buttons -->
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<div class="mr-auto">
|
||||
<SlideToggle
|
||||
name="active"
|
||||
background="bg-primary-500"
|
||||
active="bg-tertiary-500"
|
||||
bind:checked={active_value}
|
||||
disabled={disable_inputs}
|
||||
/>
|
||||
</div>
|
||||
{#if driver}
|
||||
<Button
|
||||
formaction="?/update_driver"
|
||||
color="secondary"
|
||||
disabled={disable_inputs}
|
||||
submit
|
||||
width="w-1/2"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
submit
|
||||
disabled={disable_inputs}
|
||||
formaction="?/delete_driver"
|
||||
width="w-1/2"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
{:else}
|
||||
<Button formaction="?/create_driver" color="tertiary" submit width="w-full"
|
||||
>Create Driver</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</LazyCard>
|
||||
77
src/lib/components/cards/LazyCard.svelte
Normal file
77
src/lib/components/cards/LazyCard.svelte
Normal file
@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import LazyImage from "../LazyImage.svelte";
|
||||
import { lazyload } from "$lib/lazyload";
|
||||
|
||||
interface CardProps {
|
||||
children: Snippet;
|
||||
|
||||
/** The URL for a possible header image. Leave undefined for no header image. Set to empty string for an image not yet loaded. */
|
||||
imgsrc?: string | undefined;
|
||||
|
||||
/** The id of the header image element for JS access. */
|
||||
imgid?: string | undefined;
|
||||
|
||||
/** The aspect ratio width used to reserve image space (while its loading) */
|
||||
imgwidth: number;
|
||||
|
||||
/** The aspect ratio height used to reserve image space (while its loading) */
|
||||
imgheight: number;
|
||||
|
||||
/** Hide the header image element. It can be shown by removing the "hidden" property using JS and the imgid. */
|
||||
imghidden?: boolean;
|
||||
|
||||
/** The aspect ratio width used to reserve card space (while its loading) */
|
||||
cardwidth: number;
|
||||
|
||||
/** The aspect ratio height used to reserve card space (while its loading) */
|
||||
cardheight: number;
|
||||
}
|
||||
|
||||
let {
|
||||
children,
|
||||
imgsrc = undefined,
|
||||
imgid = undefined,
|
||||
imgwidth,
|
||||
imgheight,
|
||||
imghidden = false,
|
||||
cardwidth,
|
||||
cardheight,
|
||||
...restProps
|
||||
}: CardProps = $props();
|
||||
|
||||
let load: boolean = $state(false);
|
||||
|
||||
const lazy_visible_handler = () => {
|
||||
load = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
use:lazyload
|
||||
onLazyVisible={lazy_visible_handler}
|
||||
style="width: 100%; aspect-ratio: {cardwidth} / {cardheight};"
|
||||
>
|
||||
<div class="card w-full overflow-hidden bg-white shadow">
|
||||
<!-- Allow empty strings for images that only appear after user action -->
|
||||
{#if imgsrc !== undefined}
|
||||
<LazyImage
|
||||
id={imgid}
|
||||
src={imgsrc}
|
||||
{imgwidth}
|
||||
{imgheight}
|
||||
alt="Card header"
|
||||
draggable="false"
|
||||
class="select-none shadow"
|
||||
hidden={imghidden}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Only lazyload children, as the image is already lazy (also the image fade would break) -->
|
||||
{#if load}
|
||||
<div class="p-2" {...restProps}>
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
185
src/lib/components/cards/RaceCard.svelte
Normal file
185
src/lib/components/cards/RaceCard.svelte
Normal file
@ -0,0 +1,185 @@
|
||||
<script lang="ts">
|
||||
import { get_image_preview_event_handler } from "$lib/image";
|
||||
import { FileDropzone } from "@skeletonlabs/skeleton";
|
||||
import { Button, LazyCard, Input } from "$lib/components";
|
||||
import type { Race } from "$lib/schema";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
RACE_CARD_ASPECT_HEIGHT,
|
||||
RACE_CARD_ASPECT_WIDTH,
|
||||
RACE_PICTOGRAM_HEIGHT,
|
||||
RACE_PICTOGRAM_WIDTH,
|
||||
} from "$lib/config";
|
||||
|
||||
interface RaceCardProps {
|
||||
/** The [Race] object used to prefill values. */
|
||||
race?: Race | undefined;
|
||||
|
||||
/** Disable all inputs if [true] */
|
||||
disable_inputs?: boolean;
|
||||
|
||||
/** Require all inputs if [true] */
|
||||
require_inputs?: boolean;
|
||||
|
||||
/** The [src] of the race pictogram template preview */
|
||||
pictogram_template?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
race = undefined,
|
||||
disable_inputs = false,
|
||||
require_inputs = false,
|
||||
pictogram_template = "",
|
||||
}: RaceCardProps = $props();
|
||||
|
||||
// Dates have to be formatted to datetime-local format
|
||||
const dateformat: string = "yyyy-MM-dd'T'HH:mm";
|
||||
const sprintqualidate: string | undefined =
|
||||
race && race.sprintqualidate ? format(new Date(race.sprintqualidate), dateformat) : undefined;
|
||||
const sprintdate: string | undefined =
|
||||
race && race.sprintdate ? format(new Date(race.sprintdate), dateformat) : undefined;
|
||||
const qualidate: string | undefined = race
|
||||
? format(new Date(race.qualidate), dateformat)
|
||||
: undefined;
|
||||
const racedate: string | undefined = race
|
||||
? format(new Date(race.racedate), dateformat)
|
||||
: undefined;
|
||||
|
||||
const clear_sprint = () => {
|
||||
const sprintquali: HTMLInputElement = document.getElementById(
|
||||
`race_sprintqualidate_${race?.id ?? "create"}`,
|
||||
) as HTMLInputElement;
|
||||
const sprint: HTMLInputElement = document.getElementById(
|
||||
`race_sprintdate_${race?.id ?? "create"}`,
|
||||
) as HTMLInputElement;
|
||||
|
||||
sprintquali.value = "";
|
||||
sprint.value = "";
|
||||
};
|
||||
|
||||
const labelwidth = "80px";
|
||||
</script>
|
||||
|
||||
<LazyCard
|
||||
cardwidth={RACE_CARD_ASPECT_WIDTH}
|
||||
cardheight={RACE_CARD_ASPECT_HEIGHT}
|
||||
imgsrc={race?.pictogram_url ?? pictogram_template}
|
||||
imgwidth={RACE_PICTOGRAM_WIDTH}
|
||||
imgheight={RACE_PICTOGRAM_HEIGHT}
|
||||
imgid="update_race_pictogram_preview_{race?.id ?? 'create'}"
|
||||
>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<!-- This is also disabled, because the ID should only be -->
|
||||
<!-- "leaked" to users that are allowed to use the inputs -->
|
||||
{#if race && !disable_inputs}
|
||||
<input name="id" type="hidden" value={race.id} />
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- Driver name input -->
|
||||
<Input
|
||||
id="race_name_{race?.id ?? 'create'}"
|
||||
name="name"
|
||||
value={race?.name ?? ""}
|
||||
autocomplete="off"
|
||||
{labelwidth}
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}>Name</Input
|
||||
>
|
||||
<Input
|
||||
id="race_step_{race?.id ?? 'create'}"
|
||||
name="step"
|
||||
value={race?.step ?? ""}
|
||||
autocomplete="off"
|
||||
{labelwidth}
|
||||
type="number"
|
||||
min={1}
|
||||
max={24}
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}>Step</Input
|
||||
>
|
||||
<Input
|
||||
id="race_pxx_{race?.id ?? 'create'}"
|
||||
name="pxx"
|
||||
value={race?.pxx ?? ""}
|
||||
autocomplete="off"
|
||||
{labelwidth}
|
||||
type="number"
|
||||
min={1}
|
||||
max={20}
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}>PXX</Input
|
||||
>
|
||||
|
||||
<!-- NOTE: Input datetime-local accepts YYYY-mm-ddTHH:MM format -->
|
||||
<Input
|
||||
id="race_sprintqualidate_{race?.id ?? 'create'}"
|
||||
name="sprintqualidate"
|
||||
value={sprintqualidate ?? ""}
|
||||
autocomplete="off"
|
||||
{labelwidth}
|
||||
type="datetime-local"
|
||||
disabled={disable_inputs}>SQuali</Input
|
||||
>
|
||||
<Input
|
||||
id="race_sprintdate_{race?.id ?? 'create'}"
|
||||
name="sprintdate"
|
||||
value={sprintdate ?? ""}
|
||||
autocomplete="off"
|
||||
{labelwidth}
|
||||
type="datetime-local"
|
||||
disabled={disable_inputs}>SRace</Input
|
||||
>
|
||||
<Input
|
||||
id="race_qualidate_{race?.id ?? 'create'}"
|
||||
name="qualidate"
|
||||
value={qualidate ?? ""}
|
||||
autocomplete="off"
|
||||
{labelwidth}
|
||||
type="datetime-local"
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}>Quali</Input
|
||||
>
|
||||
<Input
|
||||
id="race_racedate_{race?.id ?? 'create'}"
|
||||
name="racedate"
|
||||
value={racedate ?? ""}
|
||||
autocomplete="off"
|
||||
{labelwidth}
|
||||
type="datetime-local"
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}>Race</Input
|
||||
>
|
||||
|
||||
<!-- Headshot upload -->
|
||||
<FileDropzone
|
||||
name="pictogram"
|
||||
id="race_pictogram_{race?.id ?? 'create'}"
|
||||
onchange={get_image_preview_event_handler(
|
||||
`update_race_pictogram_preview_${race?.id ?? "create"}`,
|
||||
)}
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}
|
||||
>
|
||||
<svelte:fragment slot="message"><b>Upload Pictogram</b></svelte:fragment>
|
||||
</FileDropzone>
|
||||
|
||||
<!-- Save/Delete buttons -->
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button onclick={clear_sprint} color="secondary" disabled={disable_inputs}
|
||||
>Remove Sprint</Button
|
||||
>
|
||||
{#if race}
|
||||
<Button formaction="?/update_race" color="secondary" disabled={disable_inputs} submit
|
||||
>Save Changes</Button
|
||||
>
|
||||
<Button color="primary" submit disabled={disable_inputs} formaction="?/delete_race"
|
||||
>Delete</Button
|
||||
>
|
||||
{:else}
|
||||
<Button formaction="?/create_race" color="tertiary" submit>Create Race</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</LazyCard>
|
||||
158
src/lib/components/cards/SubstitutionCard.svelte
Normal file
158
src/lib/components/cards/SubstitutionCard.svelte
Normal file
@ -0,0 +1,158 @@
|
||||
<script lang="ts">
|
||||
import LazyCard from "./LazyCard.svelte";
|
||||
import { Button, LazyDropdown, type LazyDropdownOption } from "$lib/components";
|
||||
import type { Driver, Substitution } from "$lib/schema";
|
||||
import { get_by_value } from "$lib/database";
|
||||
import type { Action } from "svelte/action";
|
||||
import {
|
||||
DRIVER_HEADSHOT_HEIGHT,
|
||||
DRIVER_HEADSHOT_WIDTH,
|
||||
SUBSTITUTION_CARD_ASPECT_HEIGHT,
|
||||
SUBSTITUTION_CARD_ASPECT_WIDTH,
|
||||
} from "$lib/config";
|
||||
|
||||
interface SubstitutionCardProps {
|
||||
/** The [Substitution] object used to prefill values. */
|
||||
substitution?: Substitution | undefined;
|
||||
|
||||
/** The drivers (to display the headshot) */
|
||||
drivers: Driver[];
|
||||
|
||||
/** Disable all inputs if [true] */
|
||||
disable_inputs?: boolean;
|
||||
|
||||
/** Require all inputs if [true] */
|
||||
require_inputs?: boolean;
|
||||
|
||||
/** The [src] of the driver headshot template preview */
|
||||
headshot_template?: string;
|
||||
|
||||
/** The value this component's substitute select dropdown will bind to */
|
||||
substitute_select_value: string;
|
||||
|
||||
/** The value this component's driver select dropdown will bind to */
|
||||
driver_select_value: string;
|
||||
|
||||
/** The value this component's race select dropdown will bind to */
|
||||
race_select_value: string;
|
||||
|
||||
/** The options this component's substitute/driver select dropdowns will display */
|
||||
driver_select_options: LazyDropdownOption[];
|
||||
|
||||
/** The options this component's race select dropdown will display */
|
||||
race_select_options: LazyDropdownOption[];
|
||||
}
|
||||
|
||||
let {
|
||||
substitution = undefined,
|
||||
drivers,
|
||||
disable_inputs = false,
|
||||
require_inputs = false,
|
||||
headshot_template = "",
|
||||
substitute_select_value,
|
||||
driver_select_value,
|
||||
race_select_value,
|
||||
driver_select_options,
|
||||
race_select_options,
|
||||
}: SubstitutionCardProps = $props();
|
||||
|
||||
// This action is used on the <Dropdown> element.
|
||||
// It will trigger once the Dropdown's <input> elements is mounted.
|
||||
// This way we'll receive a reference to the object so we can register our event handler.
|
||||
const register_substitute_preview_handler: Action = (node: HTMLElement) => {
|
||||
node.addEventListener("DropdownChange", update_substitute_preview);
|
||||
};
|
||||
|
||||
// This event handler is registered to the Dropdown's <input> element through the action above.
|
||||
const update_substitute_preview = (event: Event) => {
|
||||
const target: HTMLInputElement = event.target as HTMLInputElement;
|
||||
|
||||
// The option "label" gets put into the Dropdown's input value,
|
||||
// so we need to lookup the driver by "code".
|
||||
const src: string = get_by_value(drivers, "code", target.value)?.headshot_url || "";
|
||||
if (src) {
|
||||
const preview: HTMLImageElement = document.getElementById(
|
||||
`update_substitution_headshot_preview_${substitution?.id ?? "create"}`,
|
||||
) as HTMLImageElement;
|
||||
|
||||
if (preview) preview.src = src;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<LazyCard
|
||||
cardwidth={SUBSTITUTION_CARD_ASPECT_WIDTH}
|
||||
cardheight={SUBSTITUTION_CARD_ASPECT_HEIGHT}
|
||||
imgsrc={get_by_value(drivers, "id", substitution?.substitute ?? "")?.headshot_url ??
|
||||
headshot_template}
|
||||
imgwidth={DRIVER_HEADSHOT_WIDTH}
|
||||
imgheight={DRIVER_HEADSHOT_HEIGHT}
|
||||
imgid="update_substitution_headshot_preview_{substitution?.id ?? 'create'}"
|
||||
>
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<!-- This is also disabled, because the ID should only be -->
|
||||
<!-- "leaked" to users that are allowed to use the inputs -->
|
||||
{#if substitution && !disable_inputs}
|
||||
<input name="id" type="hidden" value={substitution.id} />
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<!-- Substitute select -->
|
||||
<LazyDropdown
|
||||
name="substitute"
|
||||
input_variable={substitute_select_value}
|
||||
action={register_substitute_preview_handler}
|
||||
options={driver_select_options}
|
||||
labelwidth="120px"
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}
|
||||
>
|
||||
Substitute
|
||||
</LazyDropdown>
|
||||
|
||||
<!-- Driver select -->
|
||||
<LazyDropdown
|
||||
name="for"
|
||||
input_variable={driver_select_value}
|
||||
options={driver_select_options}
|
||||
labelwidth="120px"
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}
|
||||
>For
|
||||
</LazyDropdown>
|
||||
|
||||
<!-- Race select -->
|
||||
<LazyDropdown
|
||||
name="race"
|
||||
input_variable={race_select_value}
|
||||
options={race_select_options}
|
||||
labelwidth="120px"
|
||||
disabled={disable_inputs}
|
||||
required={require_inputs}
|
||||
>Race
|
||||
</LazyDropdown>
|
||||
|
||||
<!-- Save/Delete buttons -->
|
||||
<div class="flex justify-end gap-2">
|
||||
{#if substitution}
|
||||
<Button
|
||||
formaction="?/update_substitution"
|
||||
color="secondary"
|
||||
disabled={disable_inputs}
|
||||
submit>Save Changes</Button
|
||||
>
|
||||
<Button
|
||||
color="primary"
|
||||
submit
|
||||
disabled={disable_inputs}
|
||||
formaction="?/delete_substitution">Delete</Button
|
||||
>
|
||||
{:else}
|
||||
<Button formaction="?/create_substitution" color="tertiary" submit
|
||||
>Create Substitution</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</LazyCard>
|
||||
Reference in New Issue
Block a user