Compare commits

..

4 Commits

7 changed files with 49 additions and 93 deletions

View File

@ -139,6 +139,11 @@
help = "Serve Formula 11 (Prod)"; help = "Serve Formula 11 (Prod)";
command = "npm run build && npm run preview -- --host --port 5173"; command = "npm run build && npm run preview -- --host --port 5173";
} }
{
name = "check";
help = "Continuously monitor for SvelteKit issues";
command = "svelte-check --watch";
}
]; ];
}; };
}); });

View File

@ -1,77 +0,0 @@
<script lang="ts">
import type { Snippet } from "svelte";
import { LazyImage } from "$lib/components";
import { lazyload } from "$lib/lazyload";
interface LazyCardProps {
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
}: LazyCardProps = $props();
let load: boolean = $state(false);
const lazy_visible_handler = () => {
load = true;
};
</script>
<div
use:lazyload
onLazyVisible={lazy_visible_handler}
style="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

@ -9,7 +9,6 @@ import Search from "./form/Search.svelte";
import Card from "./cards/Card.svelte"; import Card from "./cards/Card.svelte";
import DriverCard from "./cards/DriverCard.svelte"; import DriverCard from "./cards/DriverCard.svelte";
import LazyCard from "./cards/LazyCard.svelte";
import RaceCard from "./cards/RaceCard.svelte"; import RaceCard from "./cards/RaceCard.svelte";
import SubstitutionCard from "./cards/SubstitutionCard.svelte"; import SubstitutionCard from "./cards/SubstitutionCard.svelte";
import TeamCard from "./cards/TeamCard.svelte"; import TeamCard from "./cards/TeamCard.svelte";
@ -18,6 +17,7 @@ import type { DropdownOption } from "./form/Dropdown";
import type { TableColumn } from "./Table"; import type { TableColumn } from "./Table";
import MenuDrawerIcon from "./svg/MenuDrawerIcon.svelte"; import MenuDrawerIcon from "./svg/MenuDrawerIcon.svelte";
import NameIcon from "./svg/NameIcon.svelte";
import PasswordIcon from "./svg/PasswordIcon.svelte"; import PasswordIcon from "./svg/PasswordIcon.svelte";
import UserIcon from "./svg/UserIcon.svelte"; import UserIcon from "./svg/UserIcon.svelte";
@ -36,7 +36,6 @@ export {
// Cards // Cards
Card, Card,
DriverCard, DriverCard,
LazyCard,
RaceCard, RaceCard,
SubstitutionCard, SubstitutionCard,
TeamCard, TeamCard,
@ -46,6 +45,7 @@ export {
type TableColumn, type TableColumn,
// SVG // SVG
NameIcon,
MenuDrawerIcon, MenuDrawerIcon,
PasswordIcon, PasswordIcon,
UserIcon, UserIcon,

View File

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 411 511.71"
fill="currentColor"
class="h-4 w-4 opacity-70"
>
<path
d="M69.04 126.32h70.44L76.02 0h40.4l63.25 126.32h49.54L292.22.01h40.16l-62.87 126.31h72.46c37.9 0 69.03 31.13 69.03 69.03v247.33c0 37.9-31.13 69.03-69.03 69.03H69.04C31.07 511.71 0 480.64 0 442.68V195.35c0-37.96 31.08-69.03 69.04-69.03zm36.57 231.81L89.13 334.2c-.58-.79-.94-2.51-1.08-5.17h-.43v29.1H66.06v-67.37h20.27l16.48 23.94c.58.78.94 2.5 1.08 5.17h.43v-29.11h21.57v67.37h-20.28zm49.53 0H132.4l17.46-67.37h33.31l17.46 67.37h-22.75l-2.48-10.67h-17.77l-2.49 10.67zm9.53-46.68-4.42 18.87h12.42l-4.31-18.87h-3.69zm62.31 46.68h-22.53l4.1-67.37h28.13l8.41 34.28h.75l8.41-34.28h28.13l4.1 67.37h-22.52l-1.31-32.66h-.75l-8.19 32.66h-16.49l-8.3-32.66h-.64l-1.3 32.66zm113.11-25.44h-21.55v10.71h26.41v14.73h-47.97v-67.37h47.42l-2.68 14.74h-23.18v11.57h21.55v15.62zM154.5 437.39h102v17.27h-102v-17.27zm-53.72-44.49h209.43v17.27H100.78V392.9zm104.07-217.74 12.62-25.3H69.04c-25.03 0-45.5 20.47-45.5 45.49v247.33c0 24.96 20.53 45.49 45.5 45.49h272.93c24.97 0 45.49-20.53 45.49-45.49V195.35c0-25.02-20.47-45.49-45.49-45.49h-84.2l-20.65 41.39c5.41 7 8.62 15.77 8.62 25.29 0 22.86-18.53 41.39-41.39 41.39-22.85 0-41.39-18.53-41.39-41.39s18.54-41.39 41.39-41.39l.5.01z"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -7,6 +7,7 @@ export interface Graphic {
export interface User { export interface User {
id: string; id: string;
username: string; username: string;
firstname: string;
avatar: string; avatar: string;
avatar_url?: string; avatar_url?: string;
admin: boolean; admin: boolean;

View File

@ -16,6 +16,7 @@
TeamCard, TeamCard,
RaceCard, RaceCard,
SubstitutionCard, SubstitutionCard,
NameIcon,
} from "$lib/components"; } from "$lib/components";
import { get_avatar_preview_event_handler } from "$lib/image"; import { get_avatar_preview_event_handler } from "$lib/image";
@ -151,13 +152,15 @@
<!-- 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" width="w-full" <Button href="/data/raceresults" onclick={close_drawer} color="surface" width="w-full">
>Race Results Race Results
</Button>
<Button href="/data/season/teams" onclick={close_drawer} color="surface" width="w-full">
Season
</Button>
<Button href="/data/users" onclick={close_drawer} color="surface" width="w-full">
Users
</Button> </Button>
<Button href="/data/season/teams" onclick={close_drawer} color="surface" width="w-full"
>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 -->
@ -168,22 +171,26 @@
<form method="POST" class="contents"> <form method="POST" class="contents">
<!-- Supply the pathname so the form can redirect to the current page. --> <!-- Supply the pathname so the form can redirect to the current page. -->
<input type="hidden" name="redirect_url" value={$page.url.pathname} /> <input type="hidden" name="redirect_url" value={$page.url.pathname} />
<Input name="username" placeholder="Username" autocomplete="username" required <Input name="username" placeholder="Username" autocomplete="username" required>
><UserIcon /> <UserIcon />
</Input> </Input>
<Input name="password" type="password" placeholder="Password" autocomplete="off" required <Input name="firstname" placeholder="First Name (leave empty for login)" autocomplete="off">
><PasswordIcon /> <NameIcon />
</Input>
<Input name="password" type="password" placeholder="Password" autocomplete="off" required>
<PasswordIcon />
</Input> </Input>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<Button formaction="/profile?/login" onclick={close_drawer} color="tertiary" submit <Button formaction="/profile?/login" onclick={close_drawer} color="tertiary" submit>
>Login Login
</Button> </Button>
<Button <Button
formaction="/profile?/create_profile" formaction="/profile?/create_profile"
onclick={close_drawer} onclick={close_drawer}
color="tertiary" color="tertiary"
submit submit
>Register >
Register
</Button> </Button>
</div> </div>
</form> </form>
@ -206,6 +213,14 @@
> >
<UserIcon /> <UserIcon />
</Input> </Input>
<Input
name="firstname"
value={data.user.firstname}
placeholder="First Name"
autocomplete="off"
>
<NameIcon />
</Input>
<FileDropzone <FileDropzone
name="avatar" name="avatar"
onchange={get_avatar_preview_event_handler("user_avatar_preview")} onchange={get_avatar_preview_event_handler("user_avatar_preview")}

View File

@ -7,11 +7,12 @@ import { AVATAR_HEIGHT, AVATAR_WIDTH } from "$lib/config";
export const actions = { export const actions = {
create_profile: async ({ request, locals }): Promise<void> => { create_profile: async ({ request, locals }): Promise<void> => {
const data: FormData = form_data_clean(await request.formData()); const data: FormData = form_data_clean(await request.formData());
form_data_ensure_keys(data, ["username", "password", "redirect_url"]); form_data_ensure_keys(data, ["username", "firstname", "password", "redirect_url"]);
// Confirm password lol // Confirm password lol
await locals.pb.collection("users").create({ await locals.pb.collection("users").create({
username: data.get("username")?.toString(), username: data.get("username")?.toString(),
firstname: data.get("firstname")?.toString(),
password: data.get("password")?.toString(), password: data.get("password")?.toString(),
passwordConfirm: data.get("password")?.toString(), passwordConfirm: data.get("password")?.toString(),
admin: false, admin: false,
@ -22,6 +23,7 @@ export const actions = {
.collection("users") .collection("users")
.authWithPassword(data.get("username")?.toString(), data.get("password")?.toString()); .authWithPassword(data.get("username")?.toString(), data.get("password")?.toString());
// The current page is sent with the form, redirect to that page
redirect(303, data.get("redirect_url")?.toString() ?? "/"); redirect(303, data.get("redirect_url")?.toString() ?? "/");
}, },