Compare commits
15 Commits
7abe668169
...
eec143d63b
| Author | SHA1 | Date | |
|---|---|---|---|
| eec143d63b | |||
| 0642a6c0e2 | |||
| 3eab329b42 | |||
| 4f9799dcfb | |||
| e46bdc60bd | |||
| 62f4d211ac | |||
| e35d56c81c | |||
| 3a9b4d6834 | |||
| 43e8a00aeb | |||
| 614e2becc4 | |||
| 03fe027f8c | |||
| c597fff15a | |||
| 12803a7b8f | |||
| f39c1a9090 | |||
| 6a6c93d960 |
13
src/hooks.client.ts
Normal file
13
src/hooks.client.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { refresh_auth } from "$lib/pocketbase";
|
||||||
|
import type { ClientInit } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const init: ClientInit = async () => {
|
||||||
|
// Try to refresh the authStore. This is required when e.g.
|
||||||
|
// changing e-mail address. The new e-mail will show up after
|
||||||
|
// being verified, so the authStore has to be reloaded.
|
||||||
|
try {
|
||||||
|
await refresh_auth();
|
||||||
|
} catch (error) {
|
||||||
|
console.log("hooks.client.ts:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -14,7 +14,7 @@
|
|||||||
import { team_dropdown_options } from "$lib/dropdown";
|
import { team_dropdown_options } from "$lib/dropdown";
|
||||||
import { get_driver_headshot_template } from "$lib/database";
|
import { get_driver_headshot_template } from "$lib/database";
|
||||||
import { get_error_toast } from "$lib/toast";
|
import { get_error_toast } from "$lib/toast";
|
||||||
import { pb } from "$lib/pocketbase";
|
import { pb, pbUser } from "$lib/pocketbase";
|
||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import type { PageData } from "../../../routes/data/season/drivers/$types";
|
import type { PageData } from "../../../routes/data/season/drivers/$types";
|
||||||
|
|
||||||
@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
let required: boolean = $derived(!driver);
|
let required: boolean = $derived(!driver);
|
||||||
let disabled: boolean = $derived(!data.admin);
|
let disabled: boolean = $derived(!$pbUser?.admin);
|
||||||
let firstname_input_value: string = $state(driver?.firstname ?? "");
|
let firstname_input_value: string = $state(driver?.firstname ?? "");
|
||||||
let lastname_input_value: string = $state(driver?.lastname ?? "");
|
let lastname_input_value: string = $state(driver?.lastname ?? "");
|
||||||
let code_input_value: string = $state(driver?.code ?? "");
|
let code_input_value: string = $state(driver?.code ?? "");
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
import { get_race_pictogram_template } from "$lib/database";
|
import { get_race_pictogram_template } from "$lib/database";
|
||||||
import { format_date, isodatetimeformat } from "$lib/date";
|
import { format_date, isodatetimeformat } from "$lib/date";
|
||||||
import { get_error_toast } from "$lib/toast";
|
import { get_error_toast } from "$lib/toast";
|
||||||
import { pb } from "$lib/pocketbase";
|
import { pb, pbUser } from "$lib/pocketbase";
|
||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import type { PageData } from "../../../routes/data/season/races/$types";
|
import type { PageData } from "../../../routes/data/season/races/$types";
|
||||||
|
|
||||||
@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
let required: boolean = $derived(!race);
|
let required: boolean = $derived(!race);
|
||||||
let disabled: boolean = $derived(!data.admin);
|
let disabled: boolean = $derived(!$pbUser?.admin);
|
||||||
let name_value: string = $state(race?.name ?? "");
|
let name_value: string = $state(race?.name ?? "");
|
||||||
let step_value: string = $state(race?.step.toString() ?? "");
|
let step_value: string = $state(race?.step.toString() ?? "");
|
||||||
let pxx_value: string = $state(race?.pxx.toString() ?? "");
|
let pxx_value: string = $state(race?.pxx.toString() ?? "");
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config";
|
import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config";
|
||||||
import { driver_dropdown_options } from "$lib/dropdown";
|
import { driver_dropdown_options } from "$lib/dropdown";
|
||||||
import { get_error_toast } from "$lib/toast";
|
import { get_error_toast } from "$lib/toast";
|
||||||
import { pb } from "$lib/pocketbase";
|
import { pb, pbUser } from "$lib/pocketbase";
|
||||||
import type { PageData } from "../../../routes/racepicks/$types";
|
import type { PageData } from "../../../routes/racepicks/$types";
|
||||||
|
|
||||||
interface RacePickCardProps {
|
interface RacePickCardProps {
|
||||||
@ -96,7 +96,7 @@
|
|||||||
// Database actions
|
// Database actions
|
||||||
const update_racepick = (create?: boolean): (() => Promise<void>) => {
|
const update_racepick = (create?: boolean): (() => Promise<void>) => {
|
||||||
const handler = async (): Promise<void> => {
|
const handler = async (): Promise<void> => {
|
||||||
if (!data.user?.id || data.user.id === "") {
|
if (!$pbUser?.id || $pbUser.id === "") {
|
||||||
toastStore.trigger(get_error_toast("Invalid user id!"));
|
toastStore.trigger(get_error_toast("Invalid user id!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -114,7 +114,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const racepick_data = {
|
const racepick_data = {
|
||||||
user: data.user.id,
|
user: $pbUser.id,
|
||||||
race: data.currentrace.id,
|
race: data.currentrace.id,
|
||||||
pxx: pxx_select_value,
|
pxx: pxx_select_value,
|
||||||
dnf: dnf_select_value,
|
dnf: dnf_select_value,
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
import type { Driver, Race, RaceResult, Substitution } from "$lib/schema";
|
import type { Driver, Race, RaceResult, Substitution } from "$lib/schema";
|
||||||
import { get_by_value } from "$lib/database";
|
import { get_by_value } from "$lib/database";
|
||||||
import { race_dropdown_options } from "$lib/dropdown";
|
import { race_dropdown_options } from "$lib/dropdown";
|
||||||
import { pb } from "$lib/pocketbase";
|
import { pb, pbUser } from "$lib/pocketbase";
|
||||||
import { get_error_toast } from "$lib/toast";
|
import { get_error_toast } from "$lib/toast";
|
||||||
import type { PageData } from "../../../routes/data/raceresults/$types";
|
import type { PageData } from "../../../routes/data/raceresults/$types";
|
||||||
|
|
||||||
@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
let required: boolean = $derived(!result);
|
let required: boolean = $derived(!result);
|
||||||
let disabled: boolean = $derived(!data.admin); // TODO: Datelock (prevent entering future result)
|
let disabled: boolean = $derived(!$pbUser?.admin); // TODO: Datelock (prevent entering future result)
|
||||||
let race_select_value: string = $state(result?.race ?? "");
|
let race_select_value: string = $state(result?.race ?? "");
|
||||||
|
|
||||||
let currentrace: Race | undefined = $derived(
|
let currentrace: Race | undefined = $derived(
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config";
|
import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config";
|
||||||
import { driver_dropdown_options, team_dropdown_options } from "$lib/dropdown";
|
import { driver_dropdown_options, team_dropdown_options } from "$lib/dropdown";
|
||||||
import { get_error_toast } from "$lib/toast";
|
import { get_error_toast } from "$lib/toast";
|
||||||
import { pb } from "$lib/pocketbase";
|
import { pb, pbUser } from "$lib/pocketbase";
|
||||||
import type { PageData } from "../../../routes/seasonpicks/$types";
|
import type { PageData } from "../../../routes/seasonpicks/$types";
|
||||||
|
|
||||||
interface SeasonPickCardProps {
|
interface SeasonPickCardProps {
|
||||||
@ -184,7 +184,7 @@
|
|||||||
// Database actions
|
// Database actions
|
||||||
const update_seasonpick = (create?: boolean): (() => Promise<void>) => {
|
const update_seasonpick = (create?: boolean): (() => Promise<void>) => {
|
||||||
const handler = async (): Promise<void> => {
|
const handler = async (): Promise<void> => {
|
||||||
if (!data.user?.id || data.user.id === "") {
|
if (!$pbUser?.id || $pbUser.id === "") {
|
||||||
toastStore.trigger(get_error_toast("Invalid user id!"));
|
toastStore.trigger(get_error_toast("Invalid user id!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -229,7 +229,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seasonpick_data = {
|
const seasonpick_data = {
|
||||||
user: data.user.id,
|
user: $pbUser.id,
|
||||||
hottake: hottake_value,
|
hottake: hottake_value,
|
||||||
wdcwinner: wdc_value,
|
wdcwinner: wdc_value,
|
||||||
wccwinner: wcc_value,
|
wccwinner: wcc_value,
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config";
|
import { DRIVER_HEADSHOT_HEIGHT, DRIVER_HEADSHOT_WIDTH } from "$lib/config";
|
||||||
import { driver_dropdown_options, race_dropdown_options } from "$lib/dropdown";
|
import { driver_dropdown_options, race_dropdown_options } from "$lib/dropdown";
|
||||||
import { get_error_toast } from "$lib/toast";
|
import { get_error_toast } from "$lib/toast";
|
||||||
import { pb } from "$lib/pocketbase";
|
import { pb, pbUser } from "$lib/pocketbase";
|
||||||
import type { PageData } from "../../../routes/data/season/substitutions/$types";
|
import type { PageData } from "../../../routes/data/season/substitutions/$types";
|
||||||
|
|
||||||
interface SubstitutionCardProps {
|
interface SubstitutionCardProps {
|
||||||
@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
let required: boolean = $derived(!substitution);
|
let required: boolean = $derived(!substitution);
|
||||||
let disabled: boolean = $derived(!data.admin);
|
let disabled: boolean = $derived(!$pbUser?.admin);
|
||||||
let active_drivers: Driver[] = $derived((drivers ?? []).filter((d: Driver) => d.active));
|
let active_drivers: Driver[] = $derived((drivers ?? []).filter((d: Driver) => d.active));
|
||||||
let inactive_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 substitute_value: string = $state(substitution?.substitute ?? "");
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
} from "$lib/config";
|
} from "$lib/config";
|
||||||
import { get_team_banner_template, get_team_logo_template } from "$lib/database";
|
import { get_team_banner_template, get_team_logo_template } from "$lib/database";
|
||||||
import { get_error_toast } from "$lib/toast";
|
import { get_error_toast } from "$lib/toast";
|
||||||
import { pb } from "$lib/pocketbase";
|
import { pb, pbUser } from "$lib/pocketbase";
|
||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import type { PageData } from "../../../routes/data/season/teams/$types";
|
import type { PageData } from "../../../routes/data/season/teams/$types";
|
||||||
|
|
||||||
@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
let required: boolean = $derived(!team);
|
let required: boolean = $derived(!team);
|
||||||
let disabled: boolean = $derived(!data.admin);
|
let disabled: boolean = $derived(!$pbUser?.admin);
|
||||||
let name_value: string = $state(team?.name ?? "");
|
let name_value: string = $state(team?.name ?? "");
|
||||||
let color_value: string = $state(team?.color ?? "");
|
let color_value: string = $state(team?.color ?? "");
|
||||||
let banner_value: FileList | undefined = $state();
|
let banner_value: FileList | undefined = $state();
|
||||||
|
|||||||
@ -13,6 +13,9 @@
|
|||||||
|
|
||||||
/** The type of the input element, e.g. "text". */
|
/** The type of the input element, e.g. "text". */
|
||||||
type?: string;
|
type?: string;
|
||||||
|
|
||||||
|
/** An optional element at the end of the input group */
|
||||||
|
tail?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@ -20,16 +23,20 @@
|
|||||||
labelwidth = "auto",
|
labelwidth = "auto",
|
||||||
value = $bindable(),
|
value = $bindable(),
|
||||||
type = "text",
|
type = "text",
|
||||||
|
tail = undefined,
|
||||||
...restProps
|
...restProps
|
||||||
}: InputProps = $props();
|
}: InputProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
|
<div class="input-group input-group-divider grid-cols-[auto_1fr_auto]">
|
||||||
<div
|
<div
|
||||||
class="input-group-shim select-none text-nowrap border-r text-neutral-900"
|
class="input-group-shim select-none text-nowrap text-neutral-900"
|
||||||
style="width: {labelwidth};"
|
style="width: {labelwidth};"
|
||||||
>
|
>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
<input bind:value class="!outline-none" {type} {...restProps} />
|
<input bind:value class="{tail ? '!border-r' : ''} !border-l" {type} {...restProps} />
|
||||||
|
{#if tail}
|
||||||
|
{@render tail()}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import type { DropdownOption } from "./form/Dropdown";
|
|||||||
import type { TableColumn } from "./Table";
|
import type { TableColumn } from "./Table";
|
||||||
|
|
||||||
import ChequeredFlagIcon from "./svg/ChequeredFlagIcon.svelte";
|
import ChequeredFlagIcon from "./svg/ChequeredFlagIcon.svelte";
|
||||||
|
import EMailIcon from "./svg/EMailIcon.svelte";
|
||||||
import MenuDrawerIcon from "./svg/MenuDrawerIcon.svelte";
|
import MenuDrawerIcon from "./svg/MenuDrawerIcon.svelte";
|
||||||
import NameIcon from "./svg/NameIcon.svelte";
|
import NameIcon from "./svg/NameIcon.svelte";
|
||||||
import PasswordIcon from "./svg/PasswordIcon.svelte";
|
import PasswordIcon from "./svg/PasswordIcon.svelte";
|
||||||
@ -54,6 +55,7 @@ export {
|
|||||||
|
|
||||||
// SVG
|
// SVG
|
||||||
ChequeredFlagIcon,
|
ChequeredFlagIcon,
|
||||||
|
EMailIcon,
|
||||||
NameIcon,
|
NameIcon,
|
||||||
MenuDrawerIcon,
|
MenuDrawerIcon,
|
||||||
PasswordIcon,
|
PasswordIcon,
|
||||||
|
|||||||
42
src/lib/components/svg/EMailIcon.svelte
Normal file
42
src/lib/components/svg/EMailIcon.svelte
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
fill="currentColor"
|
||||||
|
class="h-4 w-4 opacity-70"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;"
|
||||||
|
transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M 75.546 78.738 H 14.455 C 6.484 78.738 0 72.254 0 64.283 V 25.716 c 0 -7.97 6.485 -14.455 14.455 -14.455 h 61.091 c 7.97 0 14.454 6.485 14.454 14.455 v 38.567 C 90 72.254 83.516 78.738 75.546 78.738 z M 14.455 15.488 c -5.64 0 -10.228 4.588 -10.228 10.228 v 38.567 c 0 5.64 4.588 10.229 10.228 10.229 h 61.091 c 5.64 0 10.228 -4.589 10.228 -10.229 V 25.716 c 0 -5.64 -4.588 -10.228 -10.228 -10.228 H 14.455 z"
|
||||||
|
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(29,29,27); fill-rule: nonzero; opacity: 1;"
|
||||||
|
transform=" matrix(1 0 0 1 0 0) "
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 11.044 25.917 C 21.848 36.445 32.652 46.972 43.456 57.5 c 2.014 1.962 5.105 -1.122 3.088 -3.088 C 35.74 43.885 24.936 33.357 14.132 22.83 C 12.118 20.867 9.027 23.952 11.044 25.917 L 11.044 25.917 z"
|
||||||
|
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(29,29,27); fill-rule: nonzero; opacity: 1;"
|
||||||
|
transform=" matrix(1 0 0 1 0 0) "
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 46.544 57.5 c 10.804 -10.527 21.608 -21.055 32.412 -31.582 c 2.016 -1.965 -1.073 -5.051 -3.088 -3.088 C 65.064 33.357 54.26 43.885 43.456 54.412 C 41.44 56.377 44.529 59.463 46.544 57.5 L 46.544 57.5 z"
|
||||||
|
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(29,29,27); fill-rule: nonzero; opacity: 1;"
|
||||||
|
transform=" matrix(1 0 0 1 0 0) "
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 78.837 64.952 c -7.189 -6.818 -14.379 -13.635 -21.568 -20.453 c -2.039 -1.933 -5.132 1.149 -3.088 3.088 c 7.189 6.818 14.379 13.635 21.568 20.453 C 77.788 69.973 80.881 66.89 78.837 64.952 L 78.837 64.952 z"
|
||||||
|
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(29,29,27); fill-rule: nonzero; opacity: 1;"
|
||||||
|
transform=" matrix(1 0 0 1 0 0) "
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 14.446 68.039 c 7.189 -6.818 14.379 -13.635 21.568 -20.453 c 2.043 -1.938 -1.048 -5.022 -3.088 -3.088 c -7.189 6.818 -14.379 13.635 -21.568 20.453 C 9.315 66.889 12.406 69.974 14.446 68.039 L 14.446 68.039 z"
|
||||||
|
style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(29,29,27); fill-rule: nonzero; opacity: 1;"
|
||||||
|
transform=" matrix(1 0 0 1 0 0) "
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@ -1,3 +1,4 @@
|
|||||||
|
import { get } from "svelte/store";
|
||||||
import { pb, pbUser } from "./pocketbase";
|
import { pb, pbUser } from "./pocketbase";
|
||||||
import type {
|
import type {
|
||||||
CurrentPickedUser,
|
CurrentPickedUser,
|
||||||
@ -158,11 +159,12 @@ export const fetch_visibleracepicks = async (
|
|||||||
export const fetch_currentracepick = async (
|
export const fetch_currentracepick = async (
|
||||||
fetch: (_: any) => Promise<Response>,
|
fetch: (_: any) => Promise<Response>,
|
||||||
): Promise<RacePick | undefined> => {
|
): Promise<RacePick | undefined> => {
|
||||||
if (!pbUser) return undefined;
|
const user: User | undefined = get(pbUser);
|
||||||
|
if (!user) return undefined;
|
||||||
|
|
||||||
const currentpickeduser: CurrentPickedUser = await pb
|
const currentpickeduser: CurrentPickedUser = await pb
|
||||||
.collection("currentpickedusers")
|
.collection("currentpickedusers")
|
||||||
.getOne(pbUser.id, { fetch: fetch });
|
.getOne(user.id, { fetch: fetch });
|
||||||
|
|
||||||
if (!currentpickeduser.picked) return undefined;
|
if (!currentpickeduser.picked) return undefined;
|
||||||
|
|
||||||
@ -203,11 +205,12 @@ export const fetch_hottakes = async (fetch: (_: any) => Promise<Response>): Prom
|
|||||||
export const fetch_currentseasonpick = async (
|
export const fetch_currentseasonpick = async (
|
||||||
fetch: (_: any) => Promise<Response>,
|
fetch: (_: any) => Promise<Response>,
|
||||||
): Promise<SeasonPick | undefined> => {
|
): Promise<SeasonPick | undefined> => {
|
||||||
if (!pbUser) return undefined;
|
const user: User | undefined = get(pbUser);
|
||||||
|
if (!user) return undefined;
|
||||||
|
|
||||||
const seasonpickeduser: CurrentPickedUser = await pb
|
const seasonpickeduser: CurrentPickedUser = await pb
|
||||||
.collection("seasonpickedusers")
|
.collection("seasonpickedusers")
|
||||||
.getOne(pbUser.id, { fetch: fetch });
|
.getOne(user.id, { fetch: fetch });
|
||||||
|
|
||||||
if (!seasonpickeduser.picked) return undefined;
|
if (!seasonpickeduser.picked) return undefined;
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,16 @@
|
|||||||
import Pocketbase, { type AuthRecord, type RecordModel, type RecordSubscription } from "pocketbase";
|
import Pocketbase, { type RecordModel, type RecordSubscription } from "pocketbase";
|
||||||
import type { Graphic, User } from "$lib/schema";
|
import type { Graphic, User } from "$lib/schema";
|
||||||
import { env } from "$env/dynamic/public";
|
import { env } from "$env/dynamic/public";
|
||||||
import { invalidate } from "$app/navigation";
|
import { invalidate } from "$app/navigation";
|
||||||
|
import { get, writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
export let pb = new Pocketbase(env.PUBLIC_PBURL || "http://192.168.86.50:8090");
|
export let pb = new Pocketbase(env.PUBLIC_PBURL || "http://192.168.86.50:8090");
|
||||||
export let pbUser: User | undefined = undefined;
|
|
||||||
|
|
||||||
const update_user = async (record: AuthRecord): Promise<void> => {
|
// Keep this in a writable store, because this is basically a $state.
|
||||||
if (!record) {
|
// We can't use $state in non-component files though.
|
||||||
pbUser = undefined;
|
export let pbUser: Writable<User | undefined> = writable(undefined);
|
||||||
console.log("Returning with pbUser = undefined");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const update_user = async (record: RecordModel): Promise<void> => {
|
||||||
let avatar_url: string;
|
let avatar_url: string;
|
||||||
if (record.avatar) {
|
if (record.avatar) {
|
||||||
avatar_url = pb.files.getURL(record, record.avatar);
|
avatar_url = pb.files.getURL(record, record.avatar);
|
||||||
@ -23,26 +21,55 @@ const update_user = async (record: AuthRecord): Promise<void> => {
|
|||||||
avatar_url = pb.files.getURL(driver_headshot_template, driver_headshot_template.file);
|
avatar_url = pb.files.getURL(driver_headshot_template, driver_headshot_template.file);
|
||||||
}
|
}
|
||||||
|
|
||||||
pbUser = {
|
pbUser.set({
|
||||||
id: record.id,
|
id: record.id,
|
||||||
|
verified: record.verified,
|
||||||
username: record.username,
|
username: record.username,
|
||||||
firstname: record.firstname,
|
firstname: record.firstname,
|
||||||
|
email: record.email ?? "",
|
||||||
avatar: record.avatar,
|
avatar: record.avatar,
|
||||||
avatar_url: avatar_url,
|
avatar_url: avatar_url,
|
||||||
admin: record.admin,
|
admin: record.admin,
|
||||||
} as User;
|
} as User);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the pbUser object when authStore changes (e.g. after logging in)
|
// Update the pbUser object when authStore changes (e.g. after logging in)
|
||||||
pb.authStore.onChange(async () => {
|
pb.authStore.onChange(async () => {
|
||||||
|
if (!pb.authStore.isValid) {
|
||||||
|
console.log("pb.authStore is invalid: Setting pbUser to undefined");
|
||||||
|
pbUser.set(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!pb.authStore.record) {
|
||||||
|
console.log("pb.authStore.record is null: Setting pbUser to undefined");
|
||||||
|
pbUser.set(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await update_user(pb.authStore.record);
|
await update_user(pb.authStore.record);
|
||||||
|
|
||||||
// TODO: If the user has not chosen an avatar,
|
|
||||||
// the page keeps displaying the "Login" button (wtf)
|
|
||||||
console.log("Updating pbUser...");
|
console.log("Updating pbUser...");
|
||||||
console.dir(pbUser, { depth: null });
|
console.dir(get(pbUser), { depth: null });
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
export const clear_auth = (): void => {
|
||||||
|
console.log("Cleared pb.authStore");
|
||||||
|
pb.authStore.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refresh_auth = async (): Promise<void> => {
|
||||||
|
if (pb.authStore.isValid) {
|
||||||
|
console.log("Refreshed pb.authStore");
|
||||||
|
await pb.collection("users").authRefresh();
|
||||||
|
} else {
|
||||||
|
console.log("pb.autStore is invalid: Did not refresh pb.authStore");
|
||||||
|
pb.authStore.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to PocketBase realtime collections
|
||||||
|
*/
|
||||||
export const subscribe = (collections: string[]) => {
|
export const subscribe = (collections: string[]) => {
|
||||||
collections.forEach((collection: string) => {
|
collections.forEach((collection: string) => {
|
||||||
pb.collection(collection).subscribe("*", (event: RecordSubscription<RecordModel>) => {
|
pb.collection(collection).subscribe("*", (event: RecordSubscription<RecordModel>) => {
|
||||||
@ -51,6 +78,9 @@ export const subscribe = (collections: string[]) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from PocketBase realtime collections
|
||||||
|
*/
|
||||||
export const unsubscribe = (collections: string[]) => {
|
export const unsubscribe = (collections: string[]) => {
|
||||||
collections.forEach((collection: string) => {
|
collections.forEach((collection: string) => {
|
||||||
pb.collection(collection).unsubscribe("*");
|
pb.collection(collection).unsubscribe("*");
|
||||||
|
|||||||
@ -11,8 +11,10 @@ export interface Graphic {
|
|||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
|
verified: boolean;
|
||||||
username: string;
|
username: string;
|
||||||
firstname: string;
|
firstname: string;
|
||||||
|
email?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
avatar_url?: string;
|
avatar_url?: string;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
|
|||||||
@ -1,10 +1,28 @@
|
|||||||
import type { ToastSettings } from "@skeletonlabs/skeleton";
|
import type { ToastSettings } from "@skeletonlabs/skeleton";
|
||||||
|
|
||||||
export const get_error_toast = (message: string): ToastSettings => {
|
export const get_info_toast = (message: string, timeout: number = 2000): ToastSettings => {
|
||||||
return {
|
return {
|
||||||
message,
|
message,
|
||||||
hideDismiss: true,
|
hideDismiss: true,
|
||||||
timeout: 2000,
|
timeout,
|
||||||
|
background: "variant-filled-tertiary",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const get_warning_toast = (message: string, timeout: number = 2000): ToastSettings => {
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
hideDismiss: true,
|
||||||
|
timeout,
|
||||||
|
background: "variant-filled-secondary",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const get_error_toast = (message: string, timeout: number = 2000): ToastSettings => {
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
hideDismiss: true,
|
||||||
|
timeout,
|
||||||
background: "variant-filled-primary",
|
background: "variant-filled-primary",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
RacePickCard,
|
RacePickCard,
|
||||||
RaceResultCard,
|
RaceResultCard,
|
||||||
SeasonPickCard,
|
SeasonPickCard,
|
||||||
|
EMailIcon,
|
||||||
} from "$lib/components";
|
} from "$lib/components";
|
||||||
import { get_avatar_preview_event_handler } from "$lib/image";
|
import { get_avatar_preview_event_handler } from "$lib/image";
|
||||||
import {
|
import {
|
||||||
@ -37,13 +38,16 @@
|
|||||||
type ModalComponent,
|
type ModalComponent,
|
||||||
type ToastStore,
|
type ToastStore,
|
||||||
getToastStore,
|
getToastStore,
|
||||||
|
SlideToggle,
|
||||||
} from "@skeletonlabs/skeleton";
|
} from "@skeletonlabs/skeleton";
|
||||||
import { computePosition, autoUpdate, offset, shift, flip, arrow } from "@floating-ui/dom";
|
import { computePosition, autoUpdate, offset, shift, flip, arrow } from "@floating-ui/dom";
|
||||||
import { invalidate } from "$app/navigation";
|
import { invalidate } from "$app/navigation";
|
||||||
import { get_error_toast } from "$lib/toast";
|
import { get_error_toast, get_info_toast, get_warning_toast } from "$lib/toast";
|
||||||
import { pb, subscribe, unsubscribe } from "$lib/pocketbase";
|
import { clear_auth, pb, pbUser, refresh_auth, subscribe, unsubscribe } from "$lib/pocketbase";
|
||||||
import { AVATAR_HEIGHT, AVATAR_WIDTH } from "$lib/config";
|
import { AVATAR_HEIGHT, AVATAR_WIDTH } from "$lib/config";
|
||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
|
import type { User } from "$lib/schema";
|
||||||
|
import type { RecordModel } from "pocketbase";
|
||||||
|
|
||||||
let { data, children }: { data: LayoutData; children: Snippet } = $props();
|
let { data, children }: { data: LayoutData; children: Snippet } = $props();
|
||||||
|
|
||||||
@ -136,32 +140,83 @@
|
|||||||
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
|
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow });
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
let username_value: string = $state(data.user?.username ?? "");
|
let username_value: string = $state($pbUser?.username ?? "");
|
||||||
let firstname_value: string = $state(data.user?.firstname ?? "");
|
let firstname_value: string = $state($pbUser?.firstname ?? "");
|
||||||
|
let email_value: string = $state($pbUser?.email ?? "");
|
||||||
let password_value: string = $state("");
|
let password_value: string = $state("");
|
||||||
let avatar_value: FileList | undefined = $state();
|
let avatar_value: FileList | undefined = $state();
|
||||||
|
|
||||||
|
let registration_mode: boolean = $state(false);
|
||||||
|
|
||||||
|
// Add "Enter" event listeners for login/register text inputs
|
||||||
|
const enter_handler = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
// Cancel the default action, if needed
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
registration_mode ? update_profile(true) : login();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Database actions
|
// Database actions
|
||||||
const login = async (): Promise<void> => {
|
const login = async (): Promise<void> => {
|
||||||
|
if (!username_value || username_value.trim() === "") {
|
||||||
|
toastStore.trigger(get_error_toast("Please enter your username!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!password_value || password_value.trim() === "") {
|
||||||
|
toastStore.trigger(get_error_toast("Please enter your password!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pb.collection("users").authWithPassword(username_value, password_value);
|
await pb.collection("users").authWithPassword(username_value, password_value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toastStore.trigger(get_error_toast("" + error));
|
toastStore.trigger(get_error_toast("" + error));
|
||||||
}
|
}
|
||||||
|
|
||||||
await invalidate("data:user");
|
await invalidate("data:user");
|
||||||
drawerStore.close();
|
drawerStore.close();
|
||||||
|
username_value = $pbUser?.username ?? "";
|
||||||
|
firstname_value = $pbUser?.firstname ?? "";
|
||||||
|
email_value = $pbUser?.email ?? "";
|
||||||
password_value = "";
|
password_value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = async (): Promise<void> => {
|
const logout = async (): Promise<void> => {
|
||||||
pb.authStore.clear();
|
clear_auth();
|
||||||
|
|
||||||
await invalidate("data:user");
|
await invalidate("data:user");
|
||||||
drawerStore.close();
|
drawerStore.close();
|
||||||
username_value = "";
|
username_value = "";
|
||||||
firstname_value = "";
|
firstname_value = "";
|
||||||
|
email_value = "";
|
||||||
password_value = "";
|
password_value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const forgot_password = async (): Promise<void> => {
|
||||||
|
if (!username_value || username_value.trim() === "") {
|
||||||
|
toastStore.trigger(get_error_toast("Please enter a username!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user: RecordModel = await pb
|
||||||
|
.collection("users")
|
||||||
|
.getFirstListItem(`username="${username_value}"`);
|
||||||
|
|
||||||
|
if (!user.email) {
|
||||||
|
toastStore.trigger(get_error_toast("You did not set a recovery e-mail address!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await pb.collection("users").requestPasswordReset(user.email);
|
||||||
|
toastStore.trigger(get_info_toast("Check your inbox!"));
|
||||||
|
} catch (error) {
|
||||||
|
toastStore.trigger(get_error_toast("" + error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const update_profile = (create?: boolean): (() => Promise<void>) => {
|
const update_profile = (create?: boolean): (() => Promise<void>) => {
|
||||||
const handler = async (): Promise<void> => {
|
const handler = async (): Promise<void> => {
|
||||||
// Avatar handling
|
// Avatar handling
|
||||||
@ -201,6 +256,10 @@
|
|||||||
toastStore.trigger(get_error_toast("Please enter your first name!"));
|
toastStore.trigger(get_error_toast("Please enter your first name!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!email_value || email_value.trim() === "") {
|
||||||
|
toastStore.trigger(get_error_toast("Please enter your e-mail address!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!password_value || password_value.trim() === "") {
|
if (!password_value || password_value.trim() === "") {
|
||||||
toastStore.trigger(get_error_toast("Please enter a password!"));
|
toastStore.trigger(get_error_toast("Please enter a password!"));
|
||||||
return;
|
return;
|
||||||
@ -209,24 +268,44 @@
|
|||||||
await pb.collection("users").create({
|
await pb.collection("users").create({
|
||||||
username: username_value.trim(),
|
username: username_value.trim(),
|
||||||
firstname: firstname_value.trim(),
|
firstname: firstname_value.trim(),
|
||||||
|
email: email_value.trim(),
|
||||||
|
emailVisibility: true,
|
||||||
password: password_value.trim(),
|
password: password_value.trim(),
|
||||||
passwordConfirm: password_value.trim(), // lol
|
passwordConfirm: password_value.trim(), // lol
|
||||||
admin: false,
|
admin: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await pb.collection("users").requestVerification(email_value.trim());
|
||||||
|
toastStore.trigger(get_info_toast("Check your inbox!"));
|
||||||
|
|
||||||
|
// Just in case
|
||||||
|
clear_auth();
|
||||||
|
|
||||||
await login();
|
await login();
|
||||||
} else {
|
} else {
|
||||||
if (!data.user?.id || data.user.id === "") {
|
if (!$pbUser?.id || $pbUser.id === "") {
|
||||||
toastStore.trigger(get_error_toast("Invalid user id!"));
|
toastStore.trigger(get_error_toast("Invalid user id!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await pb.collection("users").update(data.user.id, {
|
await pb.collection("users").update($pbUser.id, {
|
||||||
username: username_value.trim().length > 0 ? username_value.trim() : data.user.username,
|
username: username_value.trim().length > 0 ? username_value.trim() : $pbUser.username,
|
||||||
firstname:
|
firstname:
|
||||||
firstname_value.trim().length > 0 ? firstname_value.trim() : data.user.firstname,
|
firstname_value.trim().length > 0 ? firstname_value.trim() : $pbUser.firstname,
|
||||||
avatar: avatar_avif,
|
avatar: avatar_avif,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (email_value && email_value.trim() !== $pbUser.email) {
|
||||||
|
await pb.collection("users").requestEmailChange(email_value.trim());
|
||||||
|
|
||||||
|
// When changing the email address, the auth token is invalidated
|
||||||
|
await logout();
|
||||||
|
toastStore.trigger(get_info_toast("Check your inbox!"));
|
||||||
|
toastStore.trigger(
|
||||||
|
get_warning_toast("Please login AFTER confirming your e-mail address!", 5000),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
drawerStore.close();
|
drawerStore.close();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -322,7 +401,22 @@
|
|||||||
<!-- Login Drawer -->
|
<!-- Login Drawer -->
|
||||||
<!-- Login Drawer -->
|
<!-- Login Drawer -->
|
||||||
<div class="flex flex-col gap-2 p-2 pt-3">
|
<div class="flex flex-col gap-2 p-2 pt-3">
|
||||||
<h4 class="h4 select-none">Enter Username and Password</h4>
|
<div class="flex">
|
||||||
|
<h4 class="h4 select-none text-nowrap align-middle font-bold" style="line-height: 32px;">
|
||||||
|
Login or Register
|
||||||
|
</h4>
|
||||||
|
<div class="w-full"></div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<span class="align-middle" style="line-height: 32px;">Login</span>
|
||||||
|
<SlideToggle
|
||||||
|
name="registrationmode"
|
||||||
|
background="bg-tertiary-500"
|
||||||
|
active="bg-tertiary-500"
|
||||||
|
bind:checked={registration_mode}
|
||||||
|
/>
|
||||||
|
<span class="align-middle" style="line-height: 32px;">Register</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Input
|
<Input
|
||||||
bind:value={username_value}
|
bind:value={username_value}
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
@ -330,36 +424,77 @@
|
|||||||
minlength={3}
|
minlength={3}
|
||||||
maxlength={10}
|
maxlength={10}
|
||||||
required
|
required
|
||||||
|
onkeypress={enter_handler}
|
||||||
>
|
>
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
</Input>
|
</Input>
|
||||||
<Input
|
<div
|
||||||
bind:value={firstname_value}
|
class="{registration_mode
|
||||||
placeholder="First Name (leave empty for login)"
|
? ''
|
||||||
autocomplete="off"
|
: 'mt-[-8px] h-0'} overflow-hidden transition-all duration-150 ease-out"
|
||||||
>
|
>
|
||||||
<NameIcon />
|
<Input
|
||||||
</Input>
|
bind:value={firstname_value}
|
||||||
|
placeholder="First Name"
|
||||||
|
autocomplete="off"
|
||||||
|
tabindex={registration_mode ? 0 : -1}
|
||||||
|
onkeypress={enter_handler}
|
||||||
|
>
|
||||||
|
<NameIcon />
|
||||||
|
</Input>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="{registration_mode
|
||||||
|
? ''
|
||||||
|
: 'mt-[-8px] h-0'} overflow-hidden transition-all duration-150 ease-out"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="login_email"
|
||||||
|
type="email"
|
||||||
|
bind:value={email_value}
|
||||||
|
placeholder="E-Mail"
|
||||||
|
autocomplete="email"
|
||||||
|
tabindex={registration_mode ? 0 : -1}
|
||||||
|
onkeypress={enter_handler}
|
||||||
|
>
|
||||||
|
<EMailIcon />
|
||||||
|
</Input>
|
||||||
|
</div>
|
||||||
<Input
|
<Input
|
||||||
|
id="login_password"
|
||||||
bind:value={password_value}
|
bind:value={password_value}
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
|
onkeypress={enter_handler}
|
||||||
>
|
>
|
||||||
<PasswordIcon />
|
<PasswordIcon />
|
||||||
</Input>
|
</Input>
|
||||||
<div class="flex justify-end gap-2">
|
<div
|
||||||
<Button onclick={login} color="tertiary" shadow>Login</Button>
|
class="{!registration_mode
|
||||||
<Button onclick={update_profile(true)} color="tertiary" shadow>Register</Button>
|
? ''
|
||||||
|
: 'mt-[-8px] h-0'} flex w-full gap-2 overflow-hidden transition-all duration-150 ease-out"
|
||||||
|
>
|
||||||
|
<Button onclick={forgot_password} color="primary" width="w-full">Forgot Password</Button>
|
||||||
|
<Button onclick={login} color="tertiary" width="w-full" shadow>Login</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="{registration_mode
|
||||||
|
? ''
|
||||||
|
: 'mt-[-8px] h-0'} w-full overflow-hidden transition-all duration-150 ease-out"
|
||||||
|
>
|
||||||
|
<Button onclick={update_profile(true)} color="tertiary" width="w-full" shadow>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if $drawerStore.id === "profile_drawer" && data.user}
|
{:else if $drawerStore.id === "profile_drawer" && pbUser}
|
||||||
<!-- Profile Drawer -->
|
<!-- Profile Drawer -->
|
||||||
<!-- Profile Drawer -->
|
<!-- Profile Drawer -->
|
||||||
<!-- Profile Drawer -->
|
<!-- Profile Drawer -->
|
||||||
<div class="flex flex-col gap-2 p-2 pt-3">
|
<div class="flex flex-col gap-2 p-2 pt-3">
|
||||||
<h4 class="h4 select-none">Edit Profile</h4>
|
<h4 class="h4 select-none align-middle font-bold" style="line-height: 32px;">Edit Profile</h4>
|
||||||
<Input
|
<Input
|
||||||
bind:value={username_value}
|
bind:value={username_value}
|
||||||
maxlength={10}
|
maxlength={10}
|
||||||
@ -371,6 +506,19 @@
|
|||||||
<Input bind:value={firstname_value} placeholder="First Name" autocomplete="off">
|
<Input bind:value={firstname_value} placeholder="First Name" autocomplete="off">
|
||||||
<NameIcon />
|
<NameIcon />
|
||||||
</Input>
|
</Input>
|
||||||
|
<Input bind:value={email_value} placeholder="E-Mail" autocomplete="email">
|
||||||
|
<EMailIcon />
|
||||||
|
{#snippet tail()}
|
||||||
|
{#if $pbUser}
|
||||||
|
<div
|
||||||
|
class="input-group-shim select-none text-nowrap text-neutral-900
|
||||||
|
{$pbUser.verified ? 'bg-tertiary-500' : 'bg-primary-500'}"
|
||||||
|
>
|
||||||
|
{$pbUser.verified ? "Verified" : "Not Verified"}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</Input>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
name="avatar"
|
name="avatar"
|
||||||
bind:files={avatar_value}
|
bind:files={avatar_value}
|
||||||
@ -381,8 +529,10 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</FileDropzone>
|
</FileDropzone>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button onclick={update_profile()} color="secondary" shadow>Save Changes</Button>
|
<Button onclick={update_profile()} color="secondary" width="w-full" shadow>
|
||||||
<Button onclick={logout} color="primary" shadow>Logout</Button>
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
<Button onclick={logout} color="primary" width="w-full" shadow>Logout</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -431,14 +581,14 @@
|
|||||||
activate={$page.url.pathname.startsWith("/data")}>Data</Button
|
activate={$page.url.pathname.startsWith("/data")}>Data</Button
|
||||||
>
|
>
|
||||||
|
|
||||||
{#if !data.user}
|
{#if !$pbUser}
|
||||||
<!-- Login drawer -->
|
<!-- Login drawer -->
|
||||||
<Button color="primary" onclick={login_drawer}>Login</Button>
|
<Button color="primary" onclick={login_drawer}>Login</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Profile drawer -->
|
<!-- Profile drawer -->
|
||||||
<Avatar
|
<Avatar
|
||||||
id="user_avatar_preview"
|
id="user_avatar_preview"
|
||||||
src={data.user.avatar_url}
|
src={$pbUser?.avatar_url}
|
||||||
rounded="rounded-full"
|
rounded="rounded-full"
|
||||||
width="w-10"
|
width="w-10"
|
||||||
background="bg-primary-50"
|
background="bg-primary-50"
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { fetch_graphics } from "$lib/fetch";
|
import { fetch_graphics } from "$lib/fetch";
|
||||||
import { pbUser } from "$lib/pocketbase";
|
import { pbUser } from "$lib/pocketbase";
|
||||||
|
import { get } from "svelte/store";
|
||||||
import type { LayoutLoad } from "./$types";
|
import type { LayoutLoad } from "./$types";
|
||||||
|
import type { User } from "$lib/schema";
|
||||||
|
|
||||||
// This makes the page client-side rendered
|
// This makes the page client-side rendered
|
||||||
export const ssr = false;
|
export const ssr = false;
|
||||||
@ -11,12 +13,14 @@ export const ssr = false;
|
|||||||
// It will populate the "user" attribute of each page's "data" object,
|
// It will populate the "user" attribute of each page's "data" object,
|
||||||
// so each page has access to the current user (or knows if no one is signed in).
|
// so each page has access to the current user (or knows if no one is signed in).
|
||||||
export const load: LayoutLoad = async ({ fetch, depends }) => {
|
export const load: LayoutLoad = async ({ fetch, depends }) => {
|
||||||
depends("data:graphics", "data:user");
|
depends("data:graphics");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
// NOTE: Don't do this! The user object will be updated after this, so it will be undefined!
|
||||||
|
//
|
||||||
// User information (synchronous)
|
// User information (synchronous)
|
||||||
user: pbUser,
|
// user: get(pbUser),
|
||||||
admin: pbUser?.admin ?? false,
|
// admin: get(pbUser)?.admin ?? false,
|
||||||
|
|
||||||
// Return static data
|
// Return static data
|
||||||
graphics: await fetch_graphics(fetch),
|
graphics: await fetch_graphics(fetch),
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
import { format_date, shortdatetimeformat } from "$lib/date";
|
import { format_date, shortdatetimeformat } from "$lib/date";
|
||||||
import type { CurrentPickedUser, RacePick } from "$lib/schema";
|
import type { CurrentPickedUser, RacePick } from "$lib/schema";
|
||||||
import { get_by_value, get_driver_headshot_template } from "$lib/database";
|
import { get_by_value, get_driver_headshot_template } from "$lib/database";
|
||||||
|
import { pbUser } from "$lib/pocketbase";
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
@ -76,9 +77,8 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-2 gap-2 lg:mx-auto lg:w-fit {data.user
|
class="grid grid-cols-2 gap-2 lg:mx-auto lg:w-fit
|
||||||
? 'lg:grid-cols-6'
|
{pbUser ? 'lg:grid-cols-6' : 'lg:grid-cols-4'}"
|
||||||
: 'lg:grid-cols-4'}"
|
|
||||||
>
|
>
|
||||||
<!-- Show information about the next race -->
|
<!-- Show information about the next race -->
|
||||||
<div class="card flex w-full min-w-40 flex-col p-2 shadow lg:max-w-40">
|
<div class="card flex w-full min-w-40 flex-col p-2 shadow lg:max-w-40">
|
||||||
@ -124,7 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Only show the userguess if signed in -->
|
<!-- Only show the userguess if signed in -->
|
||||||
{#if data.user}
|
{#if pbUser}
|
||||||
<!-- PXX pick -->
|
<!-- PXX pick -->
|
||||||
<div class="card w-full min-w-40 p-2 pb-0 shadow lg:max-w-40">
|
<div class="card w-full min-w-40 p-2 pb-0 shadow lg:max-w-40">
|
||||||
<h1 class="mb-2 text-nowrap font-bold">Your P{data.currentrace.pxx} Pick:</h1>
|
<h1 class="mb-2 text-nowrap font-bold">Your P{data.currentrace.pxx} Pick:</h1>
|
||||||
@ -309,10 +309,8 @@
|
|||||||
{@const picks = racepicks.filter((pick: RacePick) => pick.user === user.id)}
|
{@const picks = racepicks.filter((pick: RacePick) => pick.user === user.id)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="card ml-1 mt-2 w-full min-w-12 overflow-hidden py-2 shadow lg:ml-2 lg:min-w-40 {data.user &&
|
class="card ml-1 mt-2 w-full min-w-12 overflow-hidden py-2 shadow lg:ml-2 lg:min-w-40
|
||||||
data.user.username === user.username
|
{$pbUser && $pbUser.username === user.username ? 'bg-primary-300' : ''}"
|
||||||
? 'bg-primary-300'
|
|
||||||
: ''}"
|
|
||||||
>
|
>
|
||||||
<!-- Avatar + name display at the top -->
|
<!-- Avatar + name display at the top -->
|
||||||
<div class="mx-auto flex h-10 w-fit">
|
<div class="mx-auto flex h-10 w-fit">
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
type ModalStore,
|
type ModalStore,
|
||||||
} from "@skeletonlabs/skeleton";
|
} from "@skeletonlabs/skeleton";
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
import type { Driver, Hottake, SeasonPick, SeasonPickedUser } from "$lib/schema";
|
import type { Driver, Hottake, SeasonPick, SeasonPickedUser, User } from "$lib/schema";
|
||||||
import { ChequeredFlagIcon, LazyImage } from "$lib/components";
|
import { ChequeredFlagIcon, LazyImage } from "$lib/components";
|
||||||
import {
|
import {
|
||||||
get_by_value,
|
get_by_value,
|
||||||
@ -23,6 +23,7 @@
|
|||||||
TEAM_BANNER_WIDTH,
|
TEAM_BANNER_WIDTH,
|
||||||
} from "$lib/config";
|
} from "$lib/config";
|
||||||
import Countdown from "$lib/components/Countdown.svelte";
|
import Countdown from "$lib/components/Countdown.svelte";
|
||||||
|
import { pbUser } from "$lib/pocketbase";
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
@ -69,12 +70,12 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-2 gap-2 lg:mx-auto lg:w-fit {data.user
|
class="grid grid-cols-2 gap-2 lg:mx-auto lg:w-fit {pbUser
|
||||||
? 'lg:grid-cols-5 2xl:grid-cols-10'
|
? 'lg:grid-cols-5 2xl:grid-cols-10'
|
||||||
: 'lg:grid-cols-2 2xl:grid-cols-2'}"
|
: 'lg:grid-cols-2 2xl:grid-cols-2'}"
|
||||||
>
|
>
|
||||||
<!-- Only show the stuff if signed in -->
|
<!-- Only show the stuff if signed in -->
|
||||||
{#if data.user}
|
{#if $pbUser}
|
||||||
{@const teamwinners = data.seasonpick
|
{@const teamwinners = data.seasonpick
|
||||||
? data.seasonpick.teamwinners
|
? data.seasonpick.teamwinners
|
||||||
.map((id: string) => get_by_value(drivers, "id", id) as Driver)
|
.map((id: string) => get_by_value(drivers, "id", id) as Driver)
|
||||||
@ -345,10 +346,8 @@
|
|||||||
: [undefined]}
|
: [undefined]}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="card ml-1 mt-2 w-full min-w-[9.5rem] overflow-hidden py-2 shadow lg:ml-2 {data.user &&
|
class="card ml-1 mt-2 w-full min-w-[9.5rem] overflow-hidden py-2 shadow lg:ml-2
|
||||||
data.user.username === user.username
|
{$pbUser && $pbUser.username === user.username ? 'bg-primary-300' : ''}"
|
||||||
? 'bg-primary-300'
|
|
||||||
: ''}"
|
|
||||||
>
|
>
|
||||||
<!-- Avatar + name display at the top -->
|
<!-- Avatar + name display at the top -->
|
||||||
<div class="mx-auto flex h-10 w-fit">
|
<div class="mx-auto flex h-10 w-fit">
|
||||||
|
|||||||
Reference in New Issue
Block a user