fix: don't use userStore in edit profile pages so they can be used after refresh

This commit is contained in:
Sam 2023-03-29 12:30:10 +02:00
parent ce214d2066
commit 7764f0f80c
No known key found for this signature in database
GPG key ID: B4EF20DDE721CAA1
4 changed files with 310 additions and 306 deletions

View file

@ -11,9 +11,7 @@
type Pronoun, type Pronoun,
} from "$lib/api/entities"; } from "$lib/api/entities";
import FallbackImage from "$lib/components/FallbackImage.svelte"; import FallbackImage from "$lib/components/FallbackImage.svelte";
import { userStore } from "$lib/store";
import { import {
Alert,
Button, Button,
ButtonGroup, ButtonGroup,
FormGroup, FormGroup,
@ -38,7 +36,8 @@
export let data: PageData; export let data: PageData;
if (!$userStore || $userStore.id !== data.member.user.id) { if (data.user.id !== data.member.user.id) {
addToast({ header: "Not your member", body: "You cannot edit another user's member." });
goto(`/@${data.member.user.name}/${data.member.name}`); goto(`/@${data.member.user.name}/${data.member.name}`);
} }
@ -307,161 +306,157 @@
<ErrorAlert {error} /> <ErrorAlert {error} />
{/if} {/if}
{#if !$userStore} <div class="grid">
<Alert color="danger" fade={false}>Error: No user object</Alert> <div class="row m-1">
{:else} <div class="col-md">
<div class="grid"> <h4>Avatar</h4>
<div class="row m-1"> <div class="row">
<div class="col-md"> <div class="col-md text-center">
<h4>Avatar</h4> {#if avatar === ""}
<div class="row"> <FallbackImage alt="Current avatar" urls={[]} width={200} />
<div class="col-md text-center"> {:else if avatar}
{#if avatar === ""} <img
<FallbackImage alt="Current avatar" urls={[]} width={200} /> width={200}
{:else if avatar} height={200}
<img src={avatar}
width={200} alt="New avatar"
height={200} class="rounded-circle img-fluid"
src={avatar}
alt="New avatar"
class="rounded-circle img-fluid"
/>
{:else}
<FallbackImage alt="Current avatar" urls={memberAvatars(data.member)} width={200} />
{/if}
</div>
<div class="col-md mt-2">
<input
class="form-control"
id="avatar"
type="file"
bind:files={avatar_files}
accept="image/png, image/jpeg, image/gif, image/webp"
/> />
<p class="text-muted mt-3"> {:else}
<Icon name="info-circle-fill" aria-hidden /> Only PNG, JPEG, GIF, and WebP can be used <FallbackImage alt="Current avatar" urls={memberAvatars(data.member)} width={200} />
as avatars. Avatars cannot be larger than 1 MB, and animated avatars will be made static.
</p>
<a href="" on:click={() => (avatar = "")}>Remove avatar</a>
</div>
</div>
</div>
<div class="col-md">
<div>
<FormGroup floating label="Name">
<Input bind:value={name} />
</FormGroup>
{#if !memberNameValid}
<p class="text-danger-emphasis mb-2">That member name is not valid.</p>
{/if} {/if}
</div> </div>
<div> <div class="col-md mt-2">
<FormGroup floating label="Display name"> <input
<Input bind:value={display_name} /> class="form-control"
</FormGroup> id="avatar"
</div> type="file"
<div> bind:files={avatar_files}
<FormGroup floating label="Bio ({bio.length}/{MAX_DESCRIPTION_LENGTH})"> accept="image/png, image/jpeg, image/gif, image/webp"
<textarea style="min-height: 100px;" class="form-control" bind:value={bio} /> />
</FormGroup>
<p class="text-muted mt-3"> <p class="text-muted mt-3">
<Icon name="info-circle-fill" aria-hidden /> Your bio supports limited <Icon name="info-circle-fill" aria-hidden /> Only PNG, JPEG, GIF, and WebP can be used as
<a avatars. Avatars cannot be larger than 1 MB, and animated avatars will be made static.
class="text-reset"
href="https://commonmark.org/help/"
target="_blank"
rel="noopener noreferrer">Markdown</a
>.
</p> </p>
<a href="" on:click={() => (avatar = "")}>Remove avatar</a>
</div> </div>
</div> </div>
</div> </div>
<div class="row m-1"> <div class="col-md">
<div class="col-md"> <div>
<h4>Names</h4> <FormGroup floating label="Name">
{#each names as _, index} <Input bind:value={name} />
<EditableName </FormGroup>
bind:value={names[index].value} {#if !memberNameValid}
bind:status={names[index].status} <p class="text-danger-emphasis mb-2">That member name is not valid.</p>
moveUp={() => moveName(index, true)} {/if}
moveDown={() => moveName(index, false)}
remove={() => removeName(index)}
/>
{/each}
<div class="input-group m-1">
<input type="text" class="form-control" bind:value={newName} />
<IconButton color="success" icon="plus" tooltip="Add name" click={() => addName()} />
</div>
</div> </div>
<div class="col-md"> <div>
<h4>Links</h4> <FormGroup floating label="Display name">
{#each links as _, index} <Input bind:value={display_name} />
<div class="input-group m-1"> </FormGroup>
<input type="text" class="form-control" bind:value={links[index]} /> </div>
<IconButton <div>
color="danger" <FormGroup floating label="Bio ({bio.length}/{MAX_DESCRIPTION_LENGTH})">
icon="trash3" <textarea style="min-height: 100px;" class="form-control" bind:value={bio} />
tooltip="Remove link" </FormGroup>
click={() => removeLink(index)} <p class="text-muted mt-3">
/> <Icon name="info-circle-fill" aria-hidden /> Your bio supports limited
</div> <a
{/each} class="text-reset"
<div class="input-group m-1"> href="https://commonmark.org/help/"
<input type="text" class="form-control" bind:value={newLink} /> target="_blank"
<IconButton color="success" icon="plus" tooltip="Add link" click={() => addLink()} /> rel="noopener noreferrer">Markdown</a
</div> >.
</p>
</div> </div>
</div> </div>
<div class="row m-1">
<div class="col-md">
<h4>Pronouns</h4>
{#each pronouns as _, index}
<EditablePronouns
bind:pronoun={pronouns[index]}
moveUp={() => movePronoun(index, true)}
moveDown={() => movePronoun(index, false)}
remove={() => removePronoun(index)}
/>
{/each}
<div class="input-group m-1">
<input
type="text"
class="form-control"
placeholder="Full set (e.g. it/it/its/its/itself)"
bind:value={newPronouns}
required
/>
<input
type="text"
class="form-control"
placeholder="Optional display text (e.g. it/its)"
bind:value={newPronounsDisplay}
/>
<IconButton
color="success"
icon="plus"
tooltip="Add pronouns"
click={() => addPronouns()}
/>
</div>
</div>
</div>
<hr />
<h4>
Fields <Button on:click={() => (fields = [...fields, { name: "New field", entries: [] }])}>
Add new field
</Button>
</h4>
</div> </div>
<div class="grid gap-3"> <div class="row m-1">
<div class="row row-cols-1 row-cols-md-2"> <div class="col-md">
{#each fields as _, index} <h4>Names</h4>
<EditableField {#each names as _, index}
bind:field={fields[index]} <EditableName
deleteField={() => removeField(index)} bind:value={names[index].value}
moveField={(up) => moveField(index, up)} bind:status={names[index].status}
moveUp={() => moveName(index, true)}
moveDown={() => moveName(index, false)}
remove={() => removeName(index)}
/> />
{/each} {/each}
<div class="input-group m-1">
<input type="text" class="form-control" bind:value={newName} />
<IconButton color="success" icon="plus" tooltip="Add name" click={() => addName()} />
</div>
</div>
<div class="col-md">
<h4>Links</h4>
{#each links as _, index}
<div class="input-group m-1">
<input type="text" class="form-control" bind:value={links[index]} />
<IconButton
color="danger"
icon="trash3"
tooltip="Remove link"
click={() => removeLink(index)}
/>
</div>
{/each}
<div class="input-group m-1">
<input type="text" class="form-control" bind:value={newLink} />
<IconButton color="success" icon="plus" tooltip="Add link" click={() => addLink()} />
</div>
</div> </div>
</div> </div>
{/if} <div class="row m-1">
<div class="col-md">
<h4>Pronouns</h4>
{#each pronouns as _, index}
<EditablePronouns
bind:pronoun={pronouns[index]}
moveUp={() => movePronoun(index, true)}
moveDown={() => movePronoun(index, false)}
remove={() => removePronoun(index)}
/>
{/each}
<div class="input-group m-1">
<input
type="text"
class="form-control"
placeholder="Full set (e.g. it/it/its/its/itself)"
bind:value={newPronouns}
required
/>
<input
type="text"
class="form-control"
placeholder="Optional display text (e.g. it/its)"
bind:value={newPronounsDisplay}
/>
<IconButton
color="success"
icon="plus"
tooltip="Add pronouns"
click={() => addPronouns()}
/>
</div>
</div>
</div>
<hr />
<h4>
Fields <Button on:click={() => (fields = [...fields, { name: "New field", entries: [] }])}>
Add new field
</Button>
</h4>
</div>
<div class="grid gap-3">
<div class="row row-cols-1 row-cols-md-2">
{#each fields as _, index}
<EditableField
bind:field={fields[index]}
deleteField={() => removeField(index)}
moveField={(up) => moveField(index, up)}
/>
{/each}
</div>
</div>

View file

@ -1,14 +1,16 @@
import type { APIError, Member } from "$lib/api/entities"; import type { MeUser, APIError, Member } from "$lib/api/entities";
import { apiFetch } from "$lib/api/fetch"; import { apiFetchClient } from "$lib/api/fetch";
import { error } from "@sveltejs/kit"; import { error } from "@sveltejs/kit";
export const ssr = false; export const ssr = false;
export const load = async ({ params }) => { export const load = async ({ params }) => {
try { try {
const member = await apiFetch<Member>(`/members/${params.id}`, {}); const user = await apiFetchClient<MeUser>(`/users/@me`);
const member = await apiFetchClient<Member>(`/members/${params.id}`);
return { return {
user,
member, member,
}; };
} catch (e) { } catch (e) {

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation";
import { import {
MAX_DESCRIPTION_LENGTH, MAX_DESCRIPTION_LENGTH,
userAvatars, userAvatars,
@ -12,7 +11,7 @@
} from "$lib/api/entities"; } from "$lib/api/entities";
import FallbackImage from "$lib/components/FallbackImage.svelte"; import FallbackImage from "$lib/components/FallbackImage.svelte";
import { userStore } from "$lib/store"; import { userStore } from "$lib/store";
import { Alert, Button, ButtonGroup, FormGroup, Icon, Input } from "sveltestrap"; import { Button, ButtonGroup, FormGroup, Icon, Input } from "sveltestrap";
import { encode } from "base64-arraybuffer"; import { encode } from "base64-arraybuffer";
import { apiFetchClient } from "$lib/api/fetch"; import { apiFetchClient } from "$lib/api/fetch";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";
@ -21,22 +20,20 @@
import EditablePronouns from "../EditablePronouns.svelte"; import EditablePronouns from "../EditablePronouns.svelte";
import ErrorAlert from "$lib/components/ErrorAlert.svelte"; import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import { addToast, delToast } from "$lib/toast"; import { addToast, delToast } from "$lib/toast";
import type { PageData } from "./$types";
const MAX_AVATAR_BYTES = 1_000_000; const MAX_AVATAR_BYTES = 1_000_000;
if (!$userStore) { export let data: PageData;
addToast({ header: "Error", body: "You are not logged in." });
goto("/");
}
let error: APIError | null = null; let error: APIError | null = null;
let bio: string = $userStore?.bio || ""; let bio: string = data.user.bio || "";
let display_name: string = $userStore?.display_name || ""; let display_name: string = data.user.display_name || "";
let links: string[] = $userStore ? window.structuredClone($userStore.links) : []; let links: string[] = window.structuredClone(data.user.links);
let names: FieldEntry[] = $userStore ? window.structuredClone($userStore.names) : []; let names: FieldEntry[] = window.structuredClone(data.user.names);
let pronouns: Pronoun[] = $userStore ? window.structuredClone($userStore.pronouns) : []; let pronouns: Pronoun[] = window.structuredClone(data.user.pronouns);
let fields: Field[] = $userStore ? window.structuredClone($userStore.fields) : []; let fields: Field[] = window.structuredClone(data.user.fields);
let avatar: string | null; let avatar: string | null;
let avatar_files: FileList | null; let avatar_files: FileList | null;
@ -60,14 +57,12 @@
fields: Field[], fields: Field[],
avatar: string | null, avatar: string | null,
) => { ) => {
if (!$userStore) return false; if (bio !== data.user.bio) return true;
if (display_name !== data.user.display_name) return true;
if (bio !== $userStore.bio) return true; if (!linksEqual(links, data.user.links)) return true;
if (display_name !== $userStore.display_name) return true; if (!fieldsEqual(fields, data.user.fields)) return true;
if (!linksEqual(links, $userStore.links)) return true; if (!namesEqual(names, data.user.names)) return true;
if (!fieldsEqual(fields, $userStore.fields)) return true; if (!pronounsEqual(pronouns, data.user.pronouns)) return true;
if (!namesEqual(names, $userStore.names)) return true;
if (!pronounsEqual(pronouns, $userStore.pronouns)) return true;
if (avatar !== null) return true; if (avatar !== null) return true;
return false; return false;
@ -211,6 +206,7 @@
fields, fields,
}); });
data.user = resp;
userStore.set(resp); userStore.set(resp);
localStorage.setItem("pronouns-user", JSON.stringify(resp)); localStorage.setItem("pronouns-user", JSON.stringify(resp));
@ -234,7 +230,7 @@
<h1> <h1>
Edit profile Edit profile
<ButtonGroup> <ButtonGroup>
<Button color="secondary" href="/@{$userStore?.name}"> <Button color="secondary" href="/@{data.user.name}">
<Icon name="chevron-left" /> <Icon name="chevron-left" />
Back to your profile Back to your profile
</Button> </Button>
@ -248,154 +244,149 @@
<ErrorAlert {error} /> <ErrorAlert {error} />
{/if} {/if}
{#if !$userStore} <div class="grid">
<Alert color="danger" fade={false}>Error: No user object</Alert> <div class="row m-1">
{:else} <div class="col-md">
<div class="grid"> <h4>Avatar</h4>
<div class="row m-1"> <div class="row">
<div class="col-md"> <div class="col-md text-center">
<h4>Avatar</h4> {#if avatar === ""}
<div class="row"> <FallbackImage alt="Current avatar" urls={[]} width={200} />
<div class="col-md text-center"> {:else if avatar}
{#if avatar === ""} <img
<FallbackImage alt="Current avatar" urls={[]} width={200} /> width={200}
{:else if avatar} height={200}
<img src={avatar}
width={200} alt="New avatar"
height={200} class="rounded-circle img-fluid"
src={avatar}
alt="New avatar"
class="rounded-circle img-fluid"
/>
{:else}
<FallbackImage alt="Current avatar" urls={userAvatars($userStore)} width={200} />
{/if}
</div>
<div class="col-md mt-2">
<input
class="form-control"
id="avatar"
type="file"
bind:files={avatar_files}
accept="image/png, image/jpeg, image/gif, image/webp"
/> />
<p class="text-muted mt-3"> {:else}
<Icon name="info-circle-fill" aria-hidden /> Only PNG, JPEG, GIF, and WebP images can be <FallbackImage alt="Current avatar" urls={userAvatars(data.user)} width={200} />
used as avatars. Avatars cannot be larger than 1 MB, and animated avatars will be made {/if}
static.
</p>
<p>
<a href="" on:click={() => (avatar = "")}>Remove avatar</a>
</p>
</div>
</div> </div>
</div> <div class="col-md mt-2">
<div class="col-md"> <input
<FormGroup floating label="Display name"> class="form-control"
<Input bind:value={display_name} /> id="avatar"
</FormGroup> type="file"
<div> bind:files={avatar_files}
<FormGroup floating label="Bio ({bio.length}/{MAX_DESCRIPTION_LENGTH})"> accept="image/png, image/jpeg, image/gif, image/webp"
<textarea style="min-height: 100px;" class="form-control" bind:value={bio} /> />
</FormGroup>
<p class="text-muted mt-3"> <p class="text-muted mt-3">
<Icon name="info-circle-fill" aria-hidden /> Your bio supports limited <Icon name="info-circle-fill" aria-hidden /> Only PNG, JPEG, GIF, and WebP images can be
<a used as avatars. Avatars cannot be larger than 1 MB, and animated avatars will be made static.
class="text-reset" </p>
href="https://commonmark.org/help/" <p>
target="_blank" <a href="" on:click={() => (avatar = "")}>Remove avatar</a>
rel="noopener noreferrer">Markdown</a
>.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div class="row m-1"> <div class="col-md">
<div class="col-md"> <FormGroup floating label="Display name">
<h4>Names</h4> <Input bind:value={display_name} />
{#each names as _, index} </FormGroup>
<EditableName <div>
bind:value={names[index].value} <FormGroup floating label="Bio ({bio.length}/{MAX_DESCRIPTION_LENGTH})">
bind:status={names[index].status} <textarea style="min-height: 100px;" class="form-control" bind:value={bio} />
moveUp={() => moveName(index, true)} </FormGroup>
moveDown={() => moveName(index, false)} <p class="text-muted mt-3">
remove={() => removeName(index)} <Icon name="info-circle-fill" aria-hidden /> Your bio supports limited
/> <a
{/each} class="text-reset"
<div class="input-group m-1"> href="https://commonmark.org/help/"
<input type="text" class="form-control" bind:value={newName} /> target="_blank"
<IconButton color="success" icon="plus" tooltip="Add name" click={() => addName()} /> rel="noopener noreferrer">Markdown</a
</div> >.
</div> </p>
<div class="col-md">
<h4>Links</h4>
{#each links as _, index}
<div class="input-group m-1">
<input type="text" class="form-control" bind:value={links[index]} />
<IconButton
color="danger"
icon="trash3"
tooltip="Remove link"
click={() => removeLink(index)}
/>
</div>
{/each}
<div class="input-group m-1">
<input type="text" class="form-control" bind:value={newLink} />
<IconButton color="success" icon="plus" tooltip="Add link" click={() => addLink()} />
</div>
</div> </div>
</div> </div>
<div class="row m-1">
<div class="col-md">
<h4>Pronouns</h4>
{#each pronouns as _, index}
<EditablePronouns
bind:pronoun={pronouns[index]}
moveUp={() => movePronoun(index, true)}
moveDown={() => movePronoun(index, false)}
remove={() => removePronoun(index)}
/>
{/each}
<div class="input-group m-1">
<input
type="text"
class="form-control"
placeholder="Full set (e.g. it/it/its/its/itself)"
bind:value={newPronouns}
required
/>
<input
type="text"
class="form-control"
placeholder="Optional display text (e.g. it/its)"
bind:value={newPronounsDisplay}
/>
<IconButton
color="success"
icon="plus"
tooltip="Add pronouns"
click={() => addPronouns()}
/>
</div>
</div>
</div>
<hr />
<h4>
Fields <Button on:click={() => (fields = [...fields, { name: "New field", entries: [] }])}>
Add new field
</Button>
</h4>
</div> </div>
<div class="grid gap-3"> <div class="row m-1">
<div class="row row-cols-1 row-cols-md-2"> <div class="col-md">
{#each fields as _, index} <h4>Names</h4>
<EditableField {#each names as _, index}
bind:field={fields[index]} <EditableName
deleteField={() => removeField(index)} bind:value={names[index].value}
moveField={(up) => moveField(index, up)} bind:status={names[index].status}
moveUp={() => moveName(index, true)}
moveDown={() => moveName(index, false)}
remove={() => removeName(index)}
/> />
{/each} {/each}
<div class="input-group m-1">
<input type="text" class="form-control" bind:value={newName} />
<IconButton color="success" icon="plus" tooltip="Add name" click={() => addName()} />
</div>
</div>
<div class="col-md">
<h4>Links</h4>
{#each links as _, index}
<div class="input-group m-1">
<input type="text" class="form-control" bind:value={links[index]} />
<IconButton
color="danger"
icon="trash3"
tooltip="Remove link"
click={() => removeLink(index)}
/>
</div>
{/each}
<div class="input-group m-1">
<input type="text" class="form-control" bind:value={newLink} />
<IconButton color="success" icon="plus" tooltip="Add link" click={() => addLink()} />
</div>
</div> </div>
</div> </div>
{/if} <div class="row m-1">
<div class="col-md">
<h4>Pronouns</h4>
{#each pronouns as _, index}
<EditablePronouns
bind:pronoun={pronouns[index]}
moveUp={() => movePronoun(index, true)}
moveDown={() => movePronoun(index, false)}
remove={() => removePronoun(index)}
/>
{/each}
<div class="input-group m-1">
<input
type="text"
class="form-control"
placeholder="Full set (e.g. it/it/its/its/itself)"
bind:value={newPronouns}
required
/>
<input
type="text"
class="form-control"
placeholder="Optional display text (e.g. it/its)"
bind:value={newPronounsDisplay}
/>
<IconButton
color="success"
icon="plus"
tooltip="Add pronouns"
click={() => addPronouns()}
/>
</div>
</div>
</div>
<hr />
<h4>
Fields <Button on:click={() => (fields = [...fields, { name: "New field", entries: [] }])}>
Add new field
</Button>
</h4>
</div>
<div class="grid gap-3">
<div class="row row-cols-1 row-cols-md-2">
{#each fields as _, index}
<EditableField
bind:field={fields[index]}
deleteField={() => removeField(index)}
moveField={(up) => moveField(index, up)}
/>
{/each}
</div>
</div>

View file

@ -1 +1,17 @@
import type { APIError, MeUser } from "$lib/api/entities";
import { apiFetchClient } from "$lib/api/fetch";
import { error } from "@sveltejs/kit";
export const ssr = false; export const ssr = false;
export const load = async () => {
try {
const user = await apiFetchClient<MeUser>(`/users/@me`);
return {
user,
};
} catch (e) {
throw error((e as APIError).code, (e as APIError).message);
}
};