Compare commits

..

6 Commits

19 changed files with 372 additions and 198 deletions

View File

@ -27,13 +27,13 @@ export const handle: Handle = async ({ event, resolve }) => {
event.locals.pb.authStore.model.avatar, event.locals.pb.authStore.model.avatar,
); );
} else { } else {
// Fill in the driver_template URL if no avatar chosen // Fill in the driver_headshot_template URL if no avatar chosen
const driver_template: Graphic = await event.locals.pb const driver_headshot_template: Graphic = await event.locals.pb
.collection("graphics") .collection("graphics")
.getFirstListItem('name="driver_template"'); .getFirstListItem('name="driver_headshot_template"');
event.locals.user.avatar_url = event.locals.pb.files.getURL( event.locals.user.avatar_url = event.locals.pb.files.getURL(
driver_template, driver_headshot_template,
driver_template.file, driver_headshot_template.file,
); );
} }

View File

@ -0,0 +1,44 @@
<script lang="ts">
import { type TableColumn } from "$lib/components";
interface TableProps {
/** The data that is displayed inside the table. Any array of arbitrary key-value objects. */
data: any[];
/** The columns the table should have. */
columns: TableColumn[];
}
let { data, columns }: TableProps = $props();
</script>
<div class="table-container bg-white shadow">
<table class="table table-interactive table-compact bg-white">
<thead>
<tr class="bg-white">
{#each columns as col}
<th>{col.label}</th>
{/each}
</tr>
</thead>
<tbody>
{#each data as row}
<tr>
{#each columns as col}
{#if col.valuefun}
<td>{@html col.valuefun(row[col.data_value_name])}</td>
{:else}
<td>{row[col.data_value_name]}</td>
{/if}
{/each}
</tr>
{/each}
</tbody>
<!-- <tfoot> -->
<!-- <tr> -->
<!-- <th colspan="3">Calculated Total Weight</th> -->
<!-- <td>{totalWeight}</td> -->
<!-- </tr> -->
<!-- </tfoot> -->
</table>
</div>

View File

@ -0,0 +1,10 @@
export interface TableColumn {
/** The name of the property containing the value. */
data_value_name: string;
/** The columnname for this property. */
label: string;
/** Any function to further customize the displayed value. May return HTML. */
valuefun?: (value: any) => string;
}

View File

@ -1,93 +0,0 @@
<script lang="ts">
import { get_image_preview_event_handler } from "$lib/image";
import { FileDropzone } from "@skeletonlabs/skeleton";
import Button from "./Button.svelte";
import type { Team } from "$lib/schema";
import Input from "./Input.svelte";
import {
TEAM_CARD_ASPECT_HEIGHT,
TEAM_CARD_ASPECT_WIDTH,
TEAM_LOGO_HEIGHT,
TEAM_LOGO_WIDTH,
} from "$lib/config";
import LazyCard from "./LazyCard.svelte";
interface TeamCardProps {
/** The [Team] object used to prefill values. */
team?: Team | undefined;
/** Disable all inputs if [true] */
disable_inputs?: boolean;
/** Require all inputs if [true] */
require_inputs?: boolean;
/** The [src] of the team logo template preview */
logo_template?: string;
}
let {
team = undefined,
disable_inputs = false,
require_inputs = false,
logo_template = "",
}: TeamCardProps = $props();
</script>
<LazyCard
cardwidth={TEAM_CARD_ASPECT_WIDTH}
cardheight={TEAM_CARD_ASPECT_HEIGHT}
imgsrc={team?.logo_url ?? logo_template}
imgwidth={TEAM_LOGO_WIDTH}
imgheight={TEAM_LOGO_HEIGHT}
imgid="update_team_logo_preview_{team?.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 team && !disable_inputs}
<input name="id" type="hidden" value={team.id} />
{/if}
<div class="flex flex-col gap-2">
<!-- Team name input -->
<Input
id="team_name_{team?.id ?? 'create'}"
name="name"
value={team?.name ?? ""}
autocomplete="off"
disabled={disable_inputs}
required={require_inputs}
>
Name
</Input>
<!-- Logo upload -->
<FileDropzone
name="logo"
id="team_logo_{team?.id ?? 'create'}"
onchange={get_image_preview_event_handler(
`update_team_logo_preview_${team?.id ?? "create"}`,
)}
disabled={disable_inputs}
required={require_inputs}
>
<svelte:fragment slot="message"><b>Upload Logo</b> or Drag and Drop</svelte:fragment>
</FileDropzone>
<!-- Save/Delete buttons -->
<div class="flex justify-end gap-2">
{#if team}
<Button formaction="?/update_team" color="secondary" disabled={disable_inputs} submit>
Save Changes
</Button>
<Button color="primary" submit disabled={disable_inputs} formaction="?/delete_team">
Delete
</Button>
{:else}
<Button formaction="?/create_team" color="tertiary" submit>Create Team</Button>
{/if}
</div>
</div>
</form>
</LazyCard>

View File

@ -1,11 +1,8 @@
<script lang="ts"> <script lang="ts">
import { get_image_preview_event_handler } from "$lib/image"; import { get_image_preview_event_handler } from "$lib/image";
import { FileDropzone, SlideToggle } from "@skeletonlabs/skeleton"; import { FileDropzone, SlideToggle } from "@skeletonlabs/skeleton";
import LazyCard from "./LazyCard.svelte"; import { Button, Input, LazyCard, LazyDropdown, type LazyDropdownOption } from "$lib/components";
import Button from "./Button.svelte";
import type { Driver } from "$lib/schema"; import type { Driver } from "$lib/schema";
import Input from "./Input.svelte";
import LazyDropdown, { type LazyDropdownOption } from "./LazyDropdown.svelte";
import { import {
DRIVER_CARD_ASPECT_HEIGHT, DRIVER_CARD_ASPECT_HEIGHT,
DRIVER_CARD_ASPECT_WIDTH, DRIVER_CARD_ASPECT_WIDTH,
@ -118,7 +115,7 @@
disabled={disable_inputs} disabled={disable_inputs}
required={require_inputs} required={require_inputs}
> >
<svelte:fragment slot="message"><b>Upload Headshot</b> or Drag and Drop</svelte:fragment> <svelte:fragment slot="message"><b>Upload Headshot</b></svelte:fragment>
</FileDropzone> </FileDropzone>
<!-- Save/Delete buttons --> <!-- Save/Delete buttons -->
@ -133,14 +130,28 @@
/> />
</div> </div>
{#if driver} {#if driver}
<Button formaction="?/update_driver" color="secondary" disabled={disable_inputs} submit <Button
>Save Changes</Button formaction="?/update_driver"
color="secondary"
disabled={disable_inputs}
submit
width="w-1/2"
> >
<Button color="primary" submit disabled={disable_inputs} formaction="?/delete_driver" Save
>Delete</Button </Button>
<Button
color="primary"
submit
disabled={disable_inputs}
formaction="?/delete_driver"
width="w-1/2"
> >
Delete
</Button>
{:else} {:else}
<Button formaction="?/create_driver" color="tertiary" submit>Create Driver</Button> <Button formaction="?/create_driver" color="tertiary" submit width="w-full"
>Create Driver</Button
>
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import LazyImage from "./LazyImage.svelte"; import LazyImage from "../LazyImage.svelte";
import { lazyload } from "$lib/lazyload"; import { lazyload } from "$lib/lazyload";
interface CardProps { interface CardProps {

View File

@ -1,10 +1,8 @@
<script lang="ts"> <script lang="ts">
import { get_image_preview_event_handler } from "$lib/image"; import { get_image_preview_event_handler } from "$lib/image";
import { FileDropzone } from "@skeletonlabs/skeleton"; import { FileDropzone } from "@skeletonlabs/skeleton";
import LazyCard from "./LazyCard.svelte"; import { Button, LazyCard, Input } from "$lib/components";
import Button from "./Button.svelte";
import type { Race } from "$lib/schema"; import type { Race } from "$lib/schema";
import Input from "./Input.svelte";
import { format } from "date-fns"; import { format } from "date-fns";
import { import {
RACE_CARD_ASPECT_HEIGHT, RACE_CARD_ASPECT_HEIGHT,
@ -58,6 +56,8 @@
sprintquali.value = ""; sprintquali.value = "";
sprint.value = ""; sprint.value = "";
}; };
const labelwidth = "80px";
</script> </script>
<LazyCard <LazyCard
@ -82,7 +82,7 @@
name="name" name="name"
value={race?.name ?? ""} value={race?.name ?? ""}
autocomplete="off" autocomplete="off"
labelwidth="120px" {labelwidth}
disabled={disable_inputs} disabled={disable_inputs}
required={require_inputs}>Name</Input required={require_inputs}>Name</Input
> >
@ -91,7 +91,7 @@
name="step" name="step"
value={race?.step ?? ""} value={race?.step ?? ""}
autocomplete="off" autocomplete="off"
labelwidth="120px" {labelwidth}
type="number" type="number"
min={1} min={1}
max={24} max={24}
@ -103,7 +103,7 @@
name="pxx" name="pxx"
value={race?.pxx ?? ""} value={race?.pxx ?? ""}
autocomplete="off" autocomplete="off"
labelwidth="120px" {labelwidth}
type="number" type="number"
min={1} min={1}
max={20} max={20}
@ -117,25 +117,25 @@
name="sprintqualidate" name="sprintqualidate"
value={sprintqualidate ?? ""} value={sprintqualidate ?? ""}
autocomplete="off" autocomplete="off"
labelwidth="120px" {labelwidth}
type="datetime-local" type="datetime-local"
disabled={disable_inputs}>Sprint Quali</Input disabled={disable_inputs}>SQuali</Input
> >
<Input <Input
id="race_sprintdate_{race?.id ?? 'create'}" id="race_sprintdate_{race?.id ?? 'create'}"
name="sprintdate" name="sprintdate"
value={sprintdate ?? ""} value={sprintdate ?? ""}
autocomplete="off" autocomplete="off"
labelwidth="120px" {labelwidth}
type="datetime-local" type="datetime-local"
disabled={disable_inputs}>Sprint</Input disabled={disable_inputs}>SRace</Input
> >
<Input <Input
id="race_qualidate_{race?.id ?? 'create'}" id="race_qualidate_{race?.id ?? 'create'}"
name="qualidate" name="qualidate"
value={qualidate ?? ""} value={qualidate ?? ""}
autocomplete="off" autocomplete="off"
labelwidth="120px" {labelwidth}
type="datetime-local" type="datetime-local"
disabled={disable_inputs} disabled={disable_inputs}
required={require_inputs}>Quali</Input required={require_inputs}>Quali</Input
@ -145,10 +145,10 @@
name="racedate" name="racedate"
value={racedate ?? ""} value={racedate ?? ""}
autocomplete="off" autocomplete="off"
labelwidth="120px" {labelwidth}
type="datetime-local" type="datetime-local"
disabled={disable_inputs} disabled={disable_inputs}
required={require_inputs}>Sprint Quali</Input required={require_inputs}>Race</Input
> >
<!-- Headshot upload --> <!-- Headshot upload -->
@ -161,7 +161,7 @@
disabled={disable_inputs} disabled={disable_inputs}
required={require_inputs} required={require_inputs}
> >
<svelte:fragment slot="message"><b>Upload Pictogram</b> or Drag and Drop</svelte:fragment> <svelte:fragment slot="message"><b>Upload Pictogram</b></svelte:fragment>
</FileDropzone> </FileDropzone>
<!-- Save/Delete buttons --> <!-- Save/Delete buttons -->

View File

@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import LazyCard from "./LazyCard.svelte"; import LazyCard from "./LazyCard.svelte";
import Button from "./Button.svelte"; import { Button, LazyDropdown, type LazyDropdownOption } from "$lib/components";
import type { Driver, Substitution } from "$lib/schema"; import type { Driver, Substitution } from "$lib/schema";
import { get_by_value } from "$lib/database"; import { get_by_value } from "$lib/database";
import LazyDropdown, { type LazyDropdownOption } from "./LazyDropdown.svelte";
import type { Action } from "svelte/action"; import type { Action } from "svelte/action";
import { import {
DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_HEIGHT,

View File

@ -0,0 +1,160 @@
<script lang="ts">
import { get_image_preview_event_handler } from "$lib/image";
import { FileDropzone } from "@skeletonlabs/skeleton";
import { Button, Input, LazyImage } from "$lib/components";
import type { Team } from "$lib/schema";
import {
TEAM_CARD_ASPECT_HEIGHT,
TEAM_CARD_ASPECT_WIDTH,
TEAM_BANNER_HEIGHT,
TEAM_BANNER_WIDTH,
TEAM_LOGO_WIDTH,
TEAM_LOGO_HEIGHT,
} from "$lib/config";
import LazyCard from "./LazyCard.svelte";
interface TeamCardProps {
/** The [Team] object used to prefill values. */
team?: Team | undefined;
/** Disable all inputs if [true] */
disable_inputs?: boolean;
/** Require all inputs if [true] */
require_inputs?: boolean;
/** The [src] of the team banner template preview */
banner_template?: string;
/** The [src] of the team logo template preview */
logo_template?: string;
}
let {
team = undefined,
disable_inputs = false,
require_inputs = false,
banner_template = "",
logo_template = "",
}: TeamCardProps = $props();
const labelwidth: string = "110px";
let colorpreview: string = $state(team?.color ?? "white");
</script>
<LazyCard
cardwidth={TEAM_CARD_ASPECT_WIDTH}
cardheight={TEAM_CARD_ASPECT_HEIGHT}
imgsrc={team?.banner_url ?? banner_template}
imgwidth={TEAM_BANNER_WIDTH}
imgheight={TEAM_BANNER_HEIGHT}
imgid="update_team_banner_preview_{team?.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 team && !disable_inputs}
<input name="id" type="hidden" value={team.id} />
{/if}
<div class="flex flex-col gap-2">
<!-- Team name input -->
<Input
id="team_name_{team?.id ?? 'create'}"
name="name"
value={team?.name ?? ""}
{labelwidth}
autocomplete="off"
disabled={disable_inputs}
required={require_inputs}
>
Name
</Input>
<!-- Team color input -->
<Input
id="team_color_{team?.id ?? 'create'}"
name="color"
value={team?.color ?? ""}
{labelwidth}
autocomplete="off"
disabled={disable_inputs}
required={require_inputs}
onchange={(event: Event) => {
colorpreview = (event.target as HTMLInputElement).value;
}}
>
Color
<span class="badge ml-2 border" style="color: {colorpreview}; background: {colorpreview}"
>C</span
>
</Input>
<!-- Banner upload -->
<FileDropzone
name="banner"
id="team_banner_{team?.id ?? 'create'}"
onchange={get_image_preview_event_handler(
`update_team_banner_preview_${team?.id ?? "create"}`,
)}
disabled={disable_inputs}
required={require_inputs}
>
<svelte:fragment slot="message"><b>Upload Banner</b></svelte:fragment>
</FileDropzone>
<!-- Logo upload -->
<FileDropzone
name="logo"
id="team_logo_{team?.id ?? 'create'}"
onchange={get_image_preview_event_handler(
`update_team_logo_preview_${team?.id ?? "create"}`,
)}
disabled={disable_inputs}
required={require_inputs}
>
<svelte:fragment slot="message">
<div class="inline-flex flex-nowrap items-center gap-2">
<b>Upload Logo</b>
<LazyImage
src={team?.logo_url ?? logo_template}
imgwidth={TEAM_LOGO_WIDTH}
imgheight={TEAM_LOGO_HEIGHT}
imgstyle="width: 32px; height: 32px;"
id="update_team_logo_preview_{team?.id ?? 'create'}"
/>
</div>
</svelte:fragment>
</FileDropzone>
<!-- Save/Delete buttons -->
<div class="flex justify-end gap-2">
{#if team}
<Button
formaction="?/update_team"
color="secondary"
disabled={disable_inputs}
submit
width="w-1/2"
>
Save
</Button>
<Button
color="primary"
submit
disabled={disable_inputs}
formaction="?/delete_team"
width="w-1/2"
>
Delete
</Button>
{:else}
<Button formaction="?/create_team" color="tertiary" submit width="w-full"
>Create Team</Button
>
{/if}
</div>
</div>
</form>
</LazyCard>

View File

@ -22,8 +22,8 @@
/** Make the button act as a link. */ /** Make the button act as a link. */
href?: string | undefined; href?: string | undefined;
/** Add the "w-full" class to the button. */ /** Add a width class to the button. */
fullwidth?: boolean; width?: string;
/** Enable the button's ":hover" state manually. */ /** Enable the button's ":hover" state manually. */
activate?: boolean; activate?: boolean;
@ -40,7 +40,7 @@
color = undefined, color = undefined,
submit = false, submit = false,
href = undefined, href = undefined,
fullwidth = false, width = "w-auto",
activate = false, activate = false,
activate_href = false, activate_href = false,
trigger_popup = { event: "click", target: "invalid" }, trigger_popup = { event: "click", target: "invalid" },
@ -53,9 +53,9 @@
<form action={href} class="contents"> <form action={href} class="contents">
<button <button
type="submit" type="submit"
class="btn m-0 select-none px-2 py-2 {color ? `variant-filled-${color}` : ''} {fullwidth class="btn m-0 select-none px-2 py-2 {color
? 'w-full' ? `variant-filled-${color}`
: 'w-auto'} {activate ? 'btn-hover' : ''} {activate_href && is_at_path(href) : ''} {width} {activate ? 'btn-hover' : ''} {activate_href && is_at_path(href)
? 'btn-hover' ? 'btn-hover'
: ''}" : ''}"
draggable="false" draggable="false"
@ -65,9 +65,9 @@
{:else} {:else}
<button <button
type={submit ? "submit" : "button"} type={submit ? "submit" : "button"}
class="btn select-none px-2 py-2 {color ? `variant-filled-${color}` : ''} {fullwidth class="btn select-none px-2 py-2 {color ? `variant-filled-${color}` : ''} {width} {activate
? 'w-full' ? 'btn-hover'
: 'w-auto'} {activate ? 'btn-hover' : ''}" : ''}"
draggable="false" draggable="false"
use:popup={trigger_popup} use:popup={trigger_popup}
{...restProps}>{@render children()}</button {...restProps}>{@render children()}</button

View File

@ -4,24 +4,7 @@
import type { Action } from "svelte/action"; import type { Action } from "svelte/action";
import type { HTMLInputAttributes } from "svelte/elements"; import type { HTMLInputAttributes } from "svelte/elements";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import LazyImage from "./LazyImage.svelte"; import { type LazyDropdownOption, LazyImage } from "$lib/components";
export interface LazyDropdownOption {
/** The label displayed in the list of options. */
label: string;
/** The value assigned to the dropdown value variable */
value: string;
/** An optional icon displayed left to the label */
icon_url?: string;
/** The aspect ratio width of the optional icon */
icon_width?: number;
/** The aspect ratio height of the optional icon */
icon_height?: number;
}
interface LazyDropdownProps extends HTMLInputAttributes { interface LazyDropdownProps extends HTMLInputAttributes {
children: Snippet; children: Snippet;

View File

@ -0,0 +1,16 @@
export interface LazyDropdownOption {
/** The label displayed in the list of options. */
label: string;
/** The value assigned to the dropdown value variable */
value: string;
/** An optional icon displayed left to the label */
icon_url?: string;
/** The aspect ratio width of the optional icon */
icon_width?: number;
/** The aspect ratio height of the optional icon */
icon_height?: number;
}

View File

@ -1,14 +1,20 @@
import Button from "./Button.svelte";
import DriverCard from "./DriverCard.svelte";
import Input from "./Input.svelte";
import LazyCard from "./LazyCard.svelte";
import LazyDropdown from "./LazyDropdown.svelte";
import LazyImage from "./LazyImage.svelte"; import LazyImage from "./LazyImage.svelte";
import LoadingIndicator from "./LoadingIndicator.svelte"; import LoadingIndicator from "./LoadingIndicator.svelte";
import RaceCard from "./RaceCard.svelte"; import Table from "./Table.svelte";
import Search from "./Search.svelte";
import SubstitutionCard from "./SubstitutionCard.svelte"; import Button from "./form/Button.svelte";
import TeamCard from "./TeamCard.svelte"; import Input from "./form/Input.svelte";
import LazyDropdown from "./form/LazyDropdown.svelte";
import Search from "./form/Search.svelte";
import DriverCard from "./cards/DriverCard.svelte";
import LazyCard from "./cards/LazyCard.svelte";
import RaceCard from "./cards/RaceCard.svelte";
import SubstitutionCard from "./cards/SubstitutionCard.svelte";
import TeamCard from "./cards/TeamCard.svelte";
import type { LazyDropdownOption } from "./form/LazyDropdown";
import type { TableColumn } from "./Table";
import MenuDrawerIcon from "./svg/MenuDrawerIcon.svelte"; import MenuDrawerIcon from "./svg/MenuDrawerIcon.svelte";
import PasswordIcon from "./svg/PasswordIcon.svelte"; import PasswordIcon from "./svg/PasswordIcon.svelte";
@ -16,18 +22,27 @@ import UserIcon from "./svg/UserIcon.svelte";
export { export {
// Components // Components
Button,
DriverCard,
Input,
LazyCard,
LazyDropdown,
LazyImage, LazyImage,
LoadingIndicator, LoadingIndicator,
RaceCard, Table,
// Form
Button,
Input,
LazyDropdown,
Search, Search,
// Cards
DriverCard,
LazyCard,
RaceCard,
SubstitutionCard, SubstitutionCard,
TeamCard, TeamCard,
// Types
type LazyDropdownOption,
type TableColumn,
// SVG // SVG
MenuDrawerIcon, MenuDrawerIcon,
PasswordIcon, PasswordIcon,

View File

@ -8,8 +8,11 @@
export const AVATAR_WIDTH: number = 256; export const AVATAR_WIDTH: number = 256;
export const AVATAR_HEIGHT: number = 256; export const AVATAR_HEIGHT: number = 256;
export const TEAM_LOGO_WIDTH: number = 512; export const TEAM_BANNER_WIDTH: number = 512;
export const TEAM_LOGO_HEIGHT: number = 288; export const TEAM_BANNER_HEIGHT: number = 288;
export const TEAM_LOGO_WIDTH: number = 96;
export const TEAM_LOGO_HEIGHT: number = 96;
export const DRIVER_HEADSHOT_WIDTH: number = 512; export const DRIVER_HEADSHOT_WIDTH: number = 512;
export const DRIVER_HEADSHOT_HEIGHT: number = 512; export const DRIVER_HEADSHOT_HEIGHT: number = 512;

View File

@ -15,15 +15,18 @@ export interface User {
export interface Team { export interface Team {
id: string; id: string;
name: string; name: string;
banner: string;
banner_url?: string;
logo: string; logo: string;
logo_url?: string; logo_url?: string;
color: string;
} }
export interface Driver { export interface Driver {
id: string; id: string;
code: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
code: string;
headshot: string; headshot: string;
headshot_url?: string; headshot_url?: string;
team: string; team: string;

View File

@ -121,28 +121,32 @@
<!-- Menu Drawer --> <!-- Menu Drawer -->
<!-- Menu Drawer --> <!-- Menu Drawer -->
<div class="flex flex-col gap-2 p-2 pt-3"> <div class="flex flex-col gap-2 p-2 pt-3">
<Button href="/racepicks" onclick={close_drawer} color="surface" fullwidth>Race Picks</Button> <Button href="/racepicks" onclick={close_drawer} color="surface" width="w-full">
<Button href="/seasonpicks" onclick={close_drawer} color="surface" fullwidth Race Picks
>Season Picks
</Button> </Button>
<Button href="/leaderboard" onclick={close_drawer} color="surface" fullwidth <Button href="/seasonpicks" onclick={close_drawer} color="surface" width="w-full">
>Leaderboard Season Picks
</Button> </Button>
<Button href="/statistics" onclick={close_drawer} color="surface" fullwidth <Button href="/leaderboard" onclick={close_drawer} color="surface" width="w-full">
>Statistics Leaderboard
</Button> </Button>
<Button href="/rules" onclick={close_drawer} color="surface" fullwidth>Rules</Button> <Button href="/statistics" onclick={close_drawer} color="surface" width="w-full">
Statistics
</Button>
<Button href="/rules" onclick={close_drawer} color="surface" width="w-full">Rules</Button>
</div> </div>
{:else if $drawerStore.id === "data_drawer"} {:else if $drawerStore.id === "data_drawer"}
<!-- Data Drawer --> <!-- Data Drawer -->
<!-- Data Drawer --> <!-- Data Drawer -->
<!-- Data Drawer --> <!-- Data Drawer -->
<div class="flex flex-col gap-2 p-2 pt-3"> <div class="flex flex-col gap-2 p-2 pt-3">
<Button href="/data/raceresult" onclick={close_drawer} color="surface" fullwidth <Button href="/data/raceresult" onclick={close_drawer} color="surface" width="w-full"
>Race Results >Race Results
</Button> </Button>
<Button href="/data/season" onclick={close_drawer} color="surface" fullwidth>Season</Button> <Button href="/data/season" onclick={close_drawer} color="surface" width="w-full"
<Button href="/data/user" onclick={close_drawer} color="surface" fullwidth>Users</Button> >Season</Button
>
<Button href="/data/user" onclick={close_drawer} color="surface" width="w-full">Users</Button>
</div> </div>
{:else if $drawerStore.id === "login_drawer"} {:else if $drawerStore.id === "login_drawer"}
<!-- Login Drawer --> <!-- Login Drawer -->
@ -195,7 +199,7 @@
name="avatar" name="avatar"
onchange={get_avatar_preview_event_handler("user_avatar_preview")} onchange={get_avatar_preview_event_handler("user_avatar_preview")}
> >
<svelte:fragment slot="message"><b>Upload Avatar</b> or Drag and Drop</svelte:fragment> <svelte:fragment slot="message"><b>Upload Avatar</b></svelte:fragment>
</FileDropzone> </FileDropzone>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<Button <Button

View File

@ -12,6 +12,8 @@ import {
DRIVER_HEADSHOT_WIDTH, DRIVER_HEADSHOT_WIDTH,
RACE_PICTOGRAM_HEIGHT, RACE_PICTOGRAM_HEIGHT,
RACE_PICTOGRAM_WIDTH, RACE_PICTOGRAM_WIDTH,
TEAM_BANNER_HEIGHT,
TEAM_BANNER_WIDTH,
TEAM_LOGO_HEIGHT, TEAM_LOGO_HEIGHT,
TEAM_LOGO_WIDTH, TEAM_LOGO_WIDTH,
} from "$lib/config"; } from "$lib/config";
@ -22,16 +24,23 @@ export const actions = {
if (!locals.admin) return { unauthorized: true }; if (!locals.admin) return { unauthorized: true };
const data: FormData = form_data_clean(await request.formData()); const data: FormData = form_data_clean(await request.formData());
form_data_ensure_keys(data, ["name", "logo"]); form_data_ensure_keys(data, ["name", "banner", "logo", "color"]);
// Compress banner
const banner_avif: Blob = await image_to_avif(
await (data.get("banner") as File).arrayBuffer(),
TEAM_BANNER_WIDTH,
TEAM_BANNER_HEIGHT,
);
data.set("banner", banner_avif);
// Compress logo // Compress logo
const compressed: Blob = await image_to_avif( const logo_avif: Blob = await image_to_avif(
await (data.get("logo") as File).arrayBuffer(), await (data.get("logo") as File).arrayBuffer(),
TEAM_LOGO_WIDTH, TEAM_LOGO_WIDTH,
TEAM_LOGO_HEIGHT, TEAM_LOGO_HEIGHT,
); );
data.set("logo", logo_avif);
data.set("logo", compressed);
await locals.pb.collection("teams").create(data); await locals.pb.collection("teams").create(data);
@ -44,15 +53,24 @@ export const actions = {
const data: FormData = form_data_clean(await request.formData()); const data: FormData = form_data_clean(await request.formData());
const id: string = form_data_get_and_remove_id(data); const id: string = form_data_get_and_remove_id(data);
if (data.has("banner")) {
// Compress banner
const banner_avif: Blob = await image_to_avif(
await (data.get("banner") as File).arrayBuffer(),
TEAM_BANNER_WIDTH,
TEAM_BANNER_HEIGHT,
);
data.set("banner", banner_avif);
}
if (data.has("logo")) { if (data.has("logo")) {
// Compress logo // Compress logo
const compressed: Blob = await image_to_avif( const logo_avif: Blob = await image_to_avif(
await (data.get("logo") as File).arrayBuffer(), await (data.get("logo") as File).arrayBuffer(),
TEAM_LOGO_WIDTH, TEAM_LOGO_WIDTH,
TEAM_LOGO_HEIGHT, TEAM_LOGO_HEIGHT,
); );
data.set("logo", logo_avif);
data.set("logo", compressed);
} }
await locals.pb.collection("teams").update(id, data); await locals.pb.collection("teams").update(id, data);
@ -81,13 +99,13 @@ export const actions = {
data.set("active", data.has("active") ? "true" : "false"); data.set("active", data.has("active") ? "true" : "false");
// Compress headshot // Compress headshot
const compressed: Blob = await image_to_avif( const headshot_avif: Blob = await image_to_avif(
await (data.get("headshot") as File).arrayBuffer(), await (data.get("headshot") as File).arrayBuffer(),
DRIVER_HEADSHOT_WIDTH, DRIVER_HEADSHOT_WIDTH,
DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_HEIGHT,
); );
data.set("headshot", compressed); data.set("headshot", headshot_avif);
await locals.pb.collection("drivers").create(data); await locals.pb.collection("drivers").create(data);
@ -105,13 +123,13 @@ export const actions = {
if (data.has("headshot")) { if (data.has("headshot")) {
// Compress headshot // Compress headshot
const compressed: Blob = await image_to_avif( const headshot_avif: Blob = await image_to_avif(
await (data.get("headshot") as File).arrayBuffer(), await (data.get("headshot") as File).arrayBuffer(),
DRIVER_HEADSHOT_WIDTH, DRIVER_HEADSHOT_WIDTH,
DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_HEIGHT,
); );
data.set("headshot", compressed); data.set("headshot", headshot_avif);
} }
await locals.pb.collection("drivers").update(id, data); await locals.pb.collection("drivers").update(id, data);
@ -138,13 +156,13 @@ export const actions = {
form_data_fix_dates(data, ["sprintqualidate", "sprintdate", "qualidate", "racedate"]); form_data_fix_dates(data, ["sprintqualidate", "sprintdate", "qualidate", "racedate"]);
// Compress pictogram // Compress pictogram
const compressed: Blob = await image_to_avif( const pictogram_avif: Blob = await image_to_avif(
await (data.get("pictogram") as File).arrayBuffer(), await (data.get("pictogram") as File).arrayBuffer(),
RACE_PICTOGRAM_WIDTH, RACE_PICTOGRAM_WIDTH,
RACE_PICTOGRAM_HEIGHT, RACE_PICTOGRAM_HEIGHT,
); );
data.set("pictogram", compressed); data.set("pictogram", pictogram_avif);
await locals.pb.collection("races").create(data); await locals.pb.collection("races").create(data);
@ -164,13 +182,13 @@ export const actions = {
if (data.has("pictogram")) { if (data.has("pictogram")) {
// Compress pictogram // Compress pictogram
const compressed: Blob = await image_to_avif( const pictogram_avif: Blob = await image_to_avif(
await (data.get("pictogram") as File).arrayBuffer(), await (data.get("pictogram") as File).arrayBuffer(),
RACE_PICTOGRAM_WIDTH, RACE_PICTOGRAM_WIDTH,
RACE_PICTOGRAM_HEIGHT, RACE_PICTOGRAM_HEIGHT,
); );
data.set("pictogram", compressed); data.set("pictogram", pictogram_avif);
} }
await locals.pb.collection("races").update(id, data); await locals.pb.collection("races").update(id, data);
@ -244,6 +262,7 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
}); });
teams.map((team: Team) => { teams.map((team: Team) => {
team.banner_url = locals.pb.files.getURL(team, team.banner);
team.logo_url = locals.pb.files.getURL(team, team.logo); team.logo_url = locals.pb.files.getURL(team, team.logo);
}); });
@ -252,7 +271,7 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
const fetch_drivers = async (): Promise<Driver[]> => { const fetch_drivers = async (): Promise<Driver[]> => {
const drivers: Driver[] = await locals.pb.collection("drivers").getFullList({ const drivers: Driver[] = await locals.pb.collection("drivers").getFullList({
sort: "+lastname", sort: "+code",
fetch: fetch, fetch: fetch,
}); });