Lib: Move cards into cards/ directory

This commit is contained in:
2024-12-18 14:59:06 +01:00
parent 49112280de
commit b3629fbe95
4 changed files with 37 additions and 27 deletions

View 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>

View 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>

View 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>

View 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>