forked from mirrors/pronouns.cc
fix: don't use userStore in edit profile pages so they can be used after refresh
This commit is contained in:
parent
ce214d2066
commit
7764f0f80c
4 changed files with 310 additions and 306 deletions
|
@ -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>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue