Skeleton: Use writable store for pbUser object

This commit is contained in:
2025-03-14 23:56:52 +01:00
parent 614e2becc4
commit 43e8a00aeb
13 changed files with 81 additions and 50 deletions

7
src/hooks.client.ts Normal file
View File

@ -0,0 +1,7 @@
import { refresh_auth } from "$lib/pocketbase";
import type { ClientInit } from "@sveltejs/kit";
export const init: ClientInit = async () => {
// NOTE: If the auth token is invalidated, this will block the entire page
// await refresh_auth();
};

View File

@ -43,7 +43,7 @@
// Reactive state // Reactive state
let required: boolean = $derived(!driver); let required: boolean = $derived(!driver);
let disabled: boolean = $derived(!pbUser?.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 ?? "");

View File

@ -50,7 +50,7 @@
// Reactive state // Reactive state
let required: boolean = $derived(!race); let required: boolean = $derived(!race);
let disabled: boolean = $derived(!pbUser?.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() ?? "");

View File

@ -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 (!pbUser?.id || pbUser.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: pbUser.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,

View File

@ -51,7 +51,7 @@
// Reactive state // Reactive state
let required: boolean = $derived(!result); let required: boolean = $derived(!result);
let disabled: boolean = $derived(!pbUser?.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(

View File

@ -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 (!pbUser?.id || pbUser.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: pbUser.id, user: $pbUser.id,
hottake: hottake_value, hottake: hottake_value,
wdcwinner: wdc_value, wdcwinner: wdc_value,
wccwinner: wcc_value, wccwinner: wcc_value,

View File

@ -43,7 +43,7 @@
// Reactive state // Reactive state
let required: boolean = $derived(!substitution); let required: boolean = $derived(!substitution);
let disabled: boolean = $derived(!pbUser?.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 ?? "");

View File

@ -46,7 +46,7 @@
// Reactive state // Reactive state
let required: boolean = $derived(!team); let required: boolean = $derived(!team);
let disabled: boolean = $derived(!pbUser?.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();

View File

@ -159,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;
@ -204,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;

View File

@ -2,9 +2,13 @@ import Pocketbase, { type RecordModel, type RecordSubscription } from "pocketbas
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;
// Keep this in a writable store, because this is basically a $state.
// We can't use $state in non-component files though.
export let pbUser: Writable<User | undefined> = writable(undefined);
const update_user = async (record: RecordModel): Promise<void> => { const update_user = async (record: RecordModel): Promise<void> => {
let avatar_url: string; let avatar_url: string;
@ -17,7 +21,7 @@ const update_user = async (record: RecordModel): 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,
username: record.username, username: record.username,
firstname: record.firstname, firstname: record.firstname,
@ -25,19 +29,19 @@ const update_user = async (record: RecordModel): Promise<void> => {
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) { if (!pb.authStore.isValid) {
console.log("pb.authStore is invalid: Setting pbUser to undefined"); console.log("pb.authStore is invalid: Setting pbUser to undefined");
pbUser = undefined; pbUser.set(undefined);
return; return;
} }
if (!pb.authStore.record) { if (!pb.authStore.record) {
console.log("pb.authStore.record is null: Setting pbUser to undefined"); console.log("pb.authStore.record is null: Setting pbUser to undefined");
pbUser = undefined; pbUser.set(undefined);
return; return;
} }
@ -46,9 +50,24 @@ pb.authStore.onChange(async () => {
// TODO: If the user has not chosen an avatar, // TODO: If the user has not chosen an avatar,
// the page keeps displaying the "Login" button (wtf) // 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 * Subscribe to PocketBase realtime collections
*/ */

View File

@ -42,11 +42,10 @@
} 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, get_info_toast } from "$lib/toast"; import { get_error_toast, get_info_toast, get_warning_toast } from "$lib/toast";
import { pb, pbUser, 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 { get } from "svelte/store";
let { data, children }: { data: LayoutData; children: Snippet } = $props(); let { data, children }: { data: LayoutData; children: Snippet } = $props();
@ -139,9 +138,9 @@
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(pbUser?.username ?? ""); let username_value: string = $state($pbUser?.username ?? "");
let firstname_value: string = $state(pbUser?.firstname ?? ""); let firstname_value: string = $state($pbUser?.firstname ?? "");
let email_value: string = $state(pbUser?.email ?? ""); 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();
@ -176,14 +175,15 @@
await invalidate("data:user"); await invalidate("data:user");
drawerStore.close(); drawerStore.close();
username_value = pbUser?.username ?? ""; username_value = $pbUser?.username ?? "";
firstname_value = pbUser?.firstname ?? ""; firstname_value = $pbUser?.firstname ?? "";
email_value = pbUser?.email ?? ""; 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 = "";
@ -252,29 +252,34 @@
await pb.collection("users").requestVerification(email_value.trim()); await pb.collection("users").requestVerification(email_value.trim());
toastStore.trigger(get_info_toast("Check your inbox!")); toastStore.trigger(get_info_toast("Check your inbox!"));
pb.authStore.clear(); // Just in case
clear_auth();
await login(); await login();
} else { } else {
if (!pbUser?.id || pbUser.id === "") { if (!$pbUser?.id || $pbUser.id === "") {
toastStore.trigger(get_error_toast("Invalid user id!")); toastStore.trigger(get_error_toast("Invalid user id!"));
return; return;
} }
if (email_value && email_value.trim() !== pbUser.email) { await pb.collection("users").update($pbUser.id, {
await pb.collection("users").requestEmailChange(email_value.trim()); username: username_value.trim().length > 0 ? username_value.trim() : $pbUser.username,
toastStore.trigger(get_info_toast("Check your inbox!"));
}
await pb.collection("users").update(pbUser.id, {
username: username_value.trim().length > 0 ? username_value.trim() : pbUser.username,
firstname: firstname:
firstname_value.trim().length > 0 ? firstname_value.trim() : pbUser.firstname, firstname_value.trim().length > 0 ? firstname_value.trim() : $pbUser.firstname,
avatar: avatar_avif, avatar: avatar_avif,
}); });
pb.authStore.clear(); if (email_value && email_value.trim() !== $pbUser.email) {
toastStore.trigger(get_info_toast("Please login again (sry UwU)!")); 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 again AFTER confirming the email address!"),
);
}
drawerStore.close(); drawerStore.close();
} }
} catch (error) { } catch (error) {
@ -539,14 +544,14 @@
activate={$page.url.pathname.startsWith("/data")}>Data</Button activate={$page.url.pathname.startsWith("/data")}>Data</Button
> >
{#if !pbUser} {#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={pbUser?.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"

View File

@ -310,7 +310,7 @@
<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 class="card ml-1 mt-2 w-full min-w-12 overflow-hidden py-2 shadow lg:ml-2 lg:min-w-40
{pbUser && pbUser.username === user.username ? 'bg-primary-300' : ''}" {$pbUser && $pbUser.username === user.username ? '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">

View File

@ -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,
@ -75,7 +75,7 @@
: '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 pbUser} {#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)
@ -346,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 {pbUser && class="card ml-1 mt-2 w-full min-w-[9.5rem] overflow-hidden py-2 shadow lg:ml-2
pbUser.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">