Skeleton: Use writable store for pbUser object
This commit is contained in:
7
src/hooks.client.ts
Normal file
7
src/hooks.client.ts
Normal 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();
|
||||||
|
};
|
@ -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 ?? "");
|
||||||
|
@ -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() ?? "");
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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 ?? "");
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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"
|
||||||
|
@ -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">
|
||||||
|
@ -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">
|
||||||
|
Reference in New Issue
Block a user