redirect from /edit/member/{id} to new page, add error page for redirects

This commit is contained in:
sam 2023-08-14 02:14:12 +02:00
parent 03311d7004
commit 5fe5f09032
No known key found for this signature in database
GPG key ID: B4EF20DDE721CAA1
7 changed files with 74 additions and 825 deletions

View file

@ -1,8 +1,12 @@
<script lang="ts">
import { page } from "$app/stores";
import { ErrorCode } from "$lib/api/entities";
import { ErrorCode } from "$lib/api/entities";
</script>
<svelte:head>
<title>Error - pronouns.cc</title>
</svelte:head>
<h1>An error occurred ({$page.status})</h1>
{#if $page.error?.code === ErrorCode.NotFound}

View file

@ -3,6 +3,10 @@
import { ErrorCode } from "$lib/api/entities";
</script>
<svelte:head>
<title>Error - pronouns.cc</title>
</svelte:head>
{#if $page.error?.code === ErrorCode.Forbidden || $page.error?.code === ErrorCode.InvalidToken}
<h1>Not logged in</h1>
<p>

View file

@ -0,0 +1,39 @@
<script lang="ts">
import { page } from "$app/stores";
import { ErrorCode } from "$lib/api/entities";
</script>
<svelte:head>
<title>Error - pronouns.cc</title>
</svelte:head>
{#if $page.error?.code === ErrorCode.Forbidden || $page.error?.code === ErrorCode.InvalidToken}
<h1>Not logged in</h1>
<p>
Either you aren't logged in, or your login has expired. Please <a href="/auth/login"
>log in again</a
>.
</p>
{:else if $page.error?.code === ErrorCode.NotOwnMember}
<h1>Not your member</h1>
<p>You can only edit your own members.</p>
{:else}
<h1>An error occurred ({$page.status})</h1>
{#if $page.status === 404}
<p>The user you were looking for couldn't be found. Please check for any typos.</p>
{:else if $page.status === 429}
<p>You've exceeded a rate limit, please try again later.</p>
{:else if $page.status === 500}
<p>An internal error occurred. Please try again later.</p>
<p>
If this error keeps happening, please <a
href="https://codeberg.org/pronounscc/pronouns.cc/issues"
target="_blank"
rel="noreferrer">file a bug report</a
> with an explanation of what you did to cause the error.
</p>
{/if}
<p>Error message: <code>{$page.error?.message}</code></p>
{/if}

View file

@ -1,808 +0,0 @@
<script lang="ts">
import { goto } from "$app/navigation";
import {
MAX_DESCRIPTION_LENGTH,
memberAvatars,
type APIError,
type Field,
type FieldEntry,
type Member,
type Pronoun,
type PrideFlag,
} from "$lib/api/entities";
import FallbackImage from "$lib/components/FallbackImage.svelte";
import {
Button,
ButtonGroup,
FormGroup,
Icon,
Input,
Modal,
ModalBody,
ModalFooter,
Popover,
TabContent,
TabPane,
Card,
CardBody,
CardHeader,
Alert,
} from "sveltestrap";
import { DateTime } from "luxon";
import { encode } from "base64-arraybuffer";
import prettyBytes from "pretty-bytes";
import { PUBLIC_SHORT_BASE } from "$env/static/public";
import { apiFetchClient, fastFetchClient } from "$lib/api/fetch";
import IconButton from "$lib/components/IconButton.svelte";
import EditableField from "$lib/components/edit/EditableField.svelte";
import EditableName from "$lib/components/edit/EditableName.svelte";
import EditablePronouns from "$lib/components/edit/EditablePronouns.svelte";
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import type { PageData, Snapshot } from "./$types";
import { addToast, delToast } from "$lib/toast";
import { memberNameRegex } from "$lib/api/regex";
import { charCount, renderMarkdown } from "$lib/utils";
import MarkdownHelp from "$lib/components/edit/MarkdownHelp.svelte";
import FlagButton from "$lib/components/edit/FlagButton.svelte";
const MAX_AVATAR_BYTES = 1_000_000;
export let data: PageData;
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}`);
}
let error: APIError | null = null;
let bio: string = data.member.bio || "";
let name: string = data.member.name;
let display_name: string = data.member.display_name || "";
let links: string[] = window.structuredClone(data.member.links);
let names: FieldEntry[] = window.structuredClone(data.member.names);
let pronouns: Pronoun[] = window.structuredClone(data.member.pronouns);
let fields: Field[] = window.structuredClone(data.member.fields);
let flags: PrideFlag[] = window.structuredClone(data.member.flags);
let unlisted: boolean = data.member.unlisted || false;
let memberNameValid = true;
$: memberNameValid = memberNameRegex.test(name);
let avatar: string | null;
let avatar_files: FileList | null;
let newName = "";
let newPronouns = "";
let newLink = "";
let flagSearch = "";
let filteredFlags: PrideFlag[];
$: filteredFlags = filterFlags(flagSearch, data.flags);
const filterFlags = (search: string, flags: PrideFlag[]) => {
return (
search
? flags.filter((flag) => flag.name.toLocaleLowerCase().includes(search.toLocaleLowerCase()))
: flags
).slice(0, 25);
};
let modified = false;
$: modified = isModified(
data.member,
bio,
name,
display_name,
links,
names,
pronouns,
fields,
flags,
avatar,
unlisted,
);
$: getAvatar(avatar_files).then((b64) => (avatar = b64));
const isModified = (
member: Member,
bio: string,
name: string,
display_name: string,
links: string[],
names: FieldEntry[],
pronouns: Pronoun[],
fields: Field[],
flags: PrideFlag[],
avatar: string | null,
unlisted: boolean,
) => {
if (name !== member.name) return true;
if (bio !== member.bio) return true;
if (display_name !== member.display_name) return true;
if (!linksEqual(links, member.links)) return true;
if (!fieldsEqual(fields, member.fields)) return true;
if (!flagsEqual(flags, member.flags)) return true;
if (!namesEqual(names, member.names)) return true;
if (!pronounsEqual(pronouns, member.pronouns)) return true;
if (avatar !== null) return true;
if (unlisted !== member.unlisted) return true;
return false;
};
const fieldsEqual = (arr1: Field[], arr2: Field[]) => {
if (arr1?.length !== arr2?.length) return false;
if (!arr1.every((_, i) => arr1[i].entries.length === arr2[i].entries.length)) return false;
if (!arr1.every((_, i) => arr1[i].name === arr2[i].name)) return false;
return arr1.every((_, i) =>
arr1[i].entries.every(
(entry, j) =>
entry.value === arr2[i].entries[j].value && entry.status === arr2[i].entries[j].status,
),
);
};
const namesEqual = (arr1: FieldEntry[], arr2: FieldEntry[]) => {
if (arr1?.length !== arr2?.length) return false;
if (!arr1.every((_, i) => arr1[i].value === arr2[i].value)) return false;
if (!arr1.every((_, i) => arr1[i].status === arr2[i].status)) return false;
return true;
};
const pronounsEqual = (arr1: Pronoun[], arr2: Pronoun[]) => {
if (arr1?.length !== arr2?.length) return false;
if (!arr1.every((_, i) => arr1[i].pronouns === arr2[i].pronouns)) return false;
if (!arr1.every((_, i) => arr1[i].display_text === arr2[i].display_text)) return false;
if (!arr1.every((_, i) => arr1[i].status === arr2[i].status)) return false;
return true;
};
const linksEqual = (arr1: string[], arr2: string[]) => {
if (arr1.length !== arr2.length) return false;
return arr1.every((_, i) => arr1[i] === arr2[i]);
};
const flagsEqual = (arr1: PrideFlag[], arr2: PrideFlag[]) => {
if (arr1.length !== arr2.length) return false;
return arr1.every((_, i) => arr1[i].id === arr2[i].id);
};
const getAvatar = async (list: FileList | null) => {
if (!list || list.length === 0) return null;
if (list[0].size > MAX_AVATAR_BYTES) {
addToast({
header: "Avatar too large",
body: `This avatar is too large, please resize it (maximum is ${prettyBytes(
MAX_AVATAR_BYTES,
)}, the file you tried to upload is ${prettyBytes(list[0].size)})`,
});
return null;
}
const buffer = await list[0].arrayBuffer();
const base64 = encode(buffer);
const uri = `data:${list[0].type};base64,${base64}`;
return uri;
};
const moveName = (index: number, up: boolean) => {
if (up && index == 0) return;
if (!up && index == names.length - 1) return;
const newIndex = up ? index - 1 : index + 1;
const temp = names[index];
names[index] = names[newIndex];
names[newIndex] = temp;
};
const movePronoun = (index: number, up: boolean) => {
if (up && index == 0) return;
if (!up && index == pronouns.length - 1) return;
const newIndex = up ? index - 1 : index + 1;
const temp = pronouns[index];
pronouns[index] = pronouns[newIndex];
pronouns[newIndex] = temp;
};
const moveField = (index: number, up: boolean) => {
if (up && index == 0) return;
if (!up && index == fields.length - 1) return;
const newIndex = up ? index - 1 : index + 1;
const temp = fields[index];
fields[index] = fields[newIndex];
fields[newIndex] = temp;
};
const moveLink = (index: number, up: boolean) => {
if (up && index == 0) return;
if (!up && index == links.length - 1) return;
const newIndex = up ? index - 1 : index + 1;
const temp = links[index];
links[index] = links[newIndex];
links[newIndex] = temp;
};
const moveFlag = (index: number, up: boolean) => {
if (up && index == 0) return;
if (!up && index == flags.length - 1) return;
const newIndex = up ? index - 1 : index + 1;
const temp = flags[index];
flags[index] = flags[newIndex];
flags[newIndex] = temp;
};
const addFlag = (flag: PrideFlag) => {
flags = [...flags, flag];
};
const removeFlag = (index: number) => {
flags.splice(index, 1);
flags = [...flags];
};
const addName = (event: Event) => {
event.preventDefault();
names = [...names, { value: newName, status: "okay" }];
newName = "";
};
const addPronouns = (event: Event) => {
event.preventDefault();
if (newPronouns in data.pronouns) {
const fullSet = data.pronouns[newPronouns];
pronouns = [
...pronouns,
{
pronouns: fullSet.pronouns.join("/"),
display_text: fullSet.display || null,
status: "okay",
},
];
} else {
pronouns = [...pronouns, { pronouns: newPronouns, display_text: null, status: "okay" }];
}
newPronouns = "";
};
const addLink = (event: Event) => {
event.preventDefault();
links = [...links, newLink];
newLink = "";
};
const removeName = (index: number) => {
names.splice(index, 1);
names = [...names];
};
const removePronoun = (index: number) => {
pronouns.splice(index, 1);
pronouns = [...pronouns];
};
const removeLink = (index: number) => {
links.splice(index, 1);
links = [...links];
};
const removeField = (index: number) => {
fields.splice(index, 1);
fields = [...fields];
};
const updateMember = async () => {
const toastId = addToast({
header: "Saving changes",
body: "Saving changes, please wait...",
duration: -1,
});
try {
const resp = await apiFetchClient<Member>(`/members/${data.member.id}`, "PATCH", {
name,
display_name,
avatar,
bio,
links,
names,
pronouns,
fields,
flags: flags.map((flag) => flag.id),
unlisted,
});
addToast({ header: "Success", body: "Successfully saved changes!" });
data.member = resp;
avatar = null;
error = null;
} catch (e) {
error = e as APIError;
} finally {
delToast(toastId);
}
};
const deleteMember = async () => {
try {
await fastFetchClient(`/members/${data.member.id}`, "DELETE");
toggleDeleteOpen();
addToast({
header: "Deleted member",
body: `Successfully deleted member ${data.member.name}!`,
});
goto(`/@${data.member.user.name}`);
} catch (e) {
deleteName = "";
deleteError = e as APIError;
}
};
let deleteModalPronoun = "the member's";
$: deleteModalPronoun = updateModalPronoun(pronouns);
const updateModalPronoun = (pronouns: Pronoun[]) => {
const filtered = pronouns.filter((entry) => entry.status === "favourite");
if (filtered.length < 1) return "the member's";
const split = filtered[0].pronouns.split("/");
if (split.length !== 5) return "the member's";
return split[2];
};
let deleteOpen = false;
const toggleDeleteOpen = () => (deleteOpen = !deleteOpen);
let deleteName = "";
let deleteError: APIError | null = null;
const now = DateTime.now().toLocal();
let canRerollSid: boolean;
$: canRerollSid =
now.diff(DateTime.fromISO(data.user.last_sid_reroll).toLocal(), "hours").hours >= 1;
const rerollSid = async () => {
try {
const resp = await apiFetchClient<Member>(`/members/${data.member.id}/reroll`);
addToast({ header: "Success", body: "Rerolled short ID!" });
error = null;
data.member.sid = resp.sid;
} catch (e) {
error = e as APIError;
}
};
const copyShortURL = async () => {
const url = `${PUBLIC_SHORT_BASE}/${data.member.sid}`;
await navigator.clipboard.writeText(url);
addToast({ body: "Copied the short link to your clipboard!", duration: 2000 });
};
interface SnapshotData {
bio: string;
name: string;
display_name: string;
links: string[];
names: FieldEntry[];
pronouns: Pronoun[];
fields: Field[];
flags: PrideFlag[];
unlisted: boolean;
avatar: string | null;
newName: string;
newPronouns: string;
newLink: string;
}
export const snapshot: Snapshot<SnapshotData> = {
capture: () => ({
bio,
name,
display_name,
links,
names,
pronouns,
fields,
flags,
unlisted,
avatar,
newName,
newPronouns,
newLink,
}),
restore: (value) => {
bio = value.bio;
name = value.name;
display_name = value.display_name;
links = value.links;
names = value.names;
pronouns = value.pronouns;
fields = value.fields;
flags = value.flags;
unlisted = value.unlisted;
avatar = value.avatar;
newName = value.newName;
newPronouns = value.newPronouns;
newLink = value.newLink;
},
};
</script>
<svelte:head>
<title>Edit member profile - pronouns.cc</title>
</svelte:head>
<h1>
Edit member profile
<ButtonGroup>
<IconButton
color="secondary"
icon="chevron-left"
href="/@{data.member.user.name}/{data.member.name}"
tooltip="Back to member"
/>
{#if modified}
<Button color="success" on:click={() => updateMember()} disabled={!memberNameValid}
>Save changes</Button
>
{/if}
<Button color="danger" on:click={toggleDeleteOpen}
>Delete {data.member.display_name ?? data.member.name}</Button
>
</ButtonGroup>
</h1>
<Modal header="Delete member" isOpen={deleteOpen} toggle={toggleDeleteOpen}>
<ModalBody>
<p>
If you want to delete this member, type {deleteModalPronoun} name (<code
>{data.member.name}</code
>) below:
</p>
<p>
<input type="text" class="form-control" bind:value={deleteName} />
</p>
{#if deleteError}
<ErrorAlert error={deleteError} />
{/if}
</ModalBody>
<ModalFooter>
<Button color="danger" disabled={deleteName !== data.member.name} on:click={deleteMember}>
Delete member
</Button>
<Button color="secondary" on:click={toggleDeleteOpen}>Cancel</Button>
</ModalFooter>
</Modal>
{#if error}
<ErrorAlert {error} />
{/if}
<TabContent>
<TabPane tabId="avatar" tab="Names and avatar" active>
<div class="row mt-3">
<div class="col-md">
<div class="row">
<div class="col-md text-center">
{#if avatar === ""}
<FallbackImage alt="Current avatar" urls={[]} width={200} />
{:else if avatar}
<img
width={200}
height={200}
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">
<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">
<Icon name="info-circle-fill" aria-hidden /> Only PNG, JPEG, GIF, and WebP images can be
used as avatars. Avatars cannot be larger than 1 MB, and animated avatars will be made
static.
</p>
<p>
<!-- svelte-ignore a11y-invalid-attribute -->
<a href="" on:click={() => (avatar = "")}>Remove avatar</a>
</p>
</div>
</div>
</div>
<div class="col-md">
<FormGroup floating label="Name">
<Input bind:value={name} />
<p class="text-muted mt-1">
<Icon name="info-circle-fill" aria-hidden />
The member name is only used as part of the link to their profile page.
</p>
</FormGroup>
{#if !memberNameValid}
<p class="text-danger-emphasis mb-2">That member name is not valid.</p>
{/if}
<FormGroup floating label="Display name">
<Input bind:value={display_name} />
</FormGroup>
<p class="text-muted mt-1">
<Icon name="info-circle-fill" aria-hidden />
Your display name is used in page titles and as a header.
</p>
</div>
</div>
<div>
<h4>Names</h4>
{#each names as _, index}
<EditableName
bind:value={names[index].value}
bind:status={names[index].status}
preferences={data.user.custom_preferences}
moveUp={() => moveName(index, true)}
moveDown={() => moveName(index, false)}
remove={() => removeName(index)}
/>
{/each}
<form class="input-group m-1" on:submit={addName}>
<input type="text" class="form-control" bind:value={newName} />
<IconButton type="submit" color="success" icon="plus" tooltip="Add name" />
</form>
</div>
</TabPane>
<TabPane tabId="bio" tab="Bio">
<div class="mt-3">
<div class="form">
<textarea class="form-control" style="height: 200px;" bind:value={bio} />
</div>
<p class="text-muted mt-1">
Using {charCount(bio)}/{MAX_DESCRIPTION_LENGTH} characters
</p>
<p class="text-muted my-2">
<MarkdownHelp />
</p>
{#if bio}
<hr />
<Card>
<CardHeader>Preview</CardHeader>
<CardBody>
{@html renderMarkdown(bio)}
</CardBody>
</Card>
{/if}
</div>
</TabPane>
<TabPane tabId="pronouns" tab="Pronouns">
<div class="mt-3">
<div class="col-md">
{#each pronouns as _, index}
<EditablePronouns
bind:pronoun={pronouns[index]}
preferences={data.user.custom_preferences}
moveUp={() => movePronoun(index, true)}
moveDown={() => movePronoun(index, false)}
remove={() => removePronoun(index)}
/>
{/each}
<form class="input-group m-1" on:submit={addPronouns}>
<input
type="text"
class="form-control"
placeholder="New pronouns"
bind:value={newPronouns}
required
/>
<IconButton
type="submit"
color="success"
icon="plus"
tooltip="Add pronouns"
disabled={newPronouns === ""}
/>
<Button id="pronouns-help" color="secondary"><Icon name="question" /></Button>
<Popover target="pronouns-help" placement="bottom">
For common pronouns, the short form (e.g. "she/her" or "he/him") is enough; for less
common pronouns, you will have to use all five forms (e.g. "ce/cir/cir/cirs/cirself").
</Popover>
</form>
</div>
</div>
</TabPane>
<TabPane tabId="fields" tab="Fields">
{#if data.member.fields.length === 0}
<Alert class="mt-3" color="secondary" fade={false}>
Fields are extra categories you can add separate from names and pronouns.<br />
For example, you could use them for gender terms, honorifics, or compliments.
</Alert>
{/if}
<div class="grid gap-3">
<div class="row row-cols-1 row-cols-md-2">
{#each fields as _, index}
<EditableField
bind:field={fields[index]}
preferences={data.user.custom_preferences}
deleteField={() => removeField(index)}
moveField={(up) => moveField(index, up)}
/>
{/each}
</div>
</div>
<div>
<Button on:click={() => (fields = [...fields, { name: "New field", entries: [] }])}>
<Icon name="plus" aria-hidden /> Add new field
</Button>
</div>
</TabPane>
<TabPane tabId="flags" tab="Flags">
<div class="mt-3">
{#each flags as _, index}
<ButtonGroup class="m-1">
<IconButton
icon="chevron-left"
color="secondary"
tooltip="Move flag to the left"
click={() => moveFlag(index, true)}
/>
<IconButton
icon="chevron-right"
color="secondary"
tooltip="Move flag to the right"
click={() => moveFlag(index, false)}
/>
<FlagButton
flag={flags[index]}
tooltip="Remove this flag from your profile"
on:click={() => removeFlag(index)}
/>
</ButtonGroup>
{/each}
</div>
<hr />
<div class="row">
<div class="col-md">
<Input
placeholder="Filter flags"
bind:value={flagSearch}
disabled={data.flags.length === 0}
/>
<div class="p-2">
{#each filteredFlags as flag (flag.id)}
<FlagButton
{flag}
tooltip="Add this flag to your profile"
on:click={() => addFlag(flag)}
/>
{:else}
{#if data.flags.length === 0}
You haven't uploaded any flags yet.
{:else}
There are no flags matching your search <strong>{flagSearch}</strong>.
{/if}
{/each}
</div>
</div>
<div class="col-md">
<Alert color="secondary" fade={false}>
{#if data.flags.length === 0}
<p><strong>Why can't I see any flags?</strong></p>
<p>
There are thousands of pride flags, and it would be impossible to bundle all of them
by default. Many labels also have multiple different flags that are favoured by
different people. Because of this, there are no flags available by default--instead,
you can upload flags in your <a href="/settings/flags">settings</a>. Your main profile
and your member profiles can all have different flags.
</p>
{:else}
To upload and delete flags, go to your <a href="/settings/flags">settings</a>.
{/if}
</Alert>
</div>
</div>
</TabPane>
<TabPane tabId="links" tab="Links">
<div class="mt-3">
{#each links as _, index}
<div class="input-group m-1">
<IconButton
icon="chevron-up"
color="secondary"
tooltip="Move link up"
click={() => moveLink(index, true)}
/>
<IconButton
icon="chevron-down"
color="secondary"
tooltip="Move link down"
click={() => moveLink(index, false)}
/>
<input type="text" class="form-control" bind:value={links[index]} />
<IconButton
color="danger"
icon="trash3"
tooltip="Remove link"
click={() => removeLink(index)}
/>
</div>
{/each}
<form class="input-group m-1" on:submit={addLink}>
<input type="text" class="form-control" bind:value={newLink} />
<IconButton type="submit" color="success" icon="plus" tooltip="Add link" />
</form>
</div>
</TabPane>
<TabPane tabId="other" tab="Other">
<div class="row mt-3">
<div class="col-md">
<div class="form-check">
<input class="form-check-input" type="checkbox" bind:checked={unlisted} id="unlisted" />
<label class="form-check-label" for="unlisted">Hide from member list</label>
</div>
<p class="text-muted mt-1">
{#if data.user.list_private}
<Icon name="exclamation-triangle-fill" aria-hidden />
Your member list is currently hidden, so <strong>this setting has no effect</strong>. If
you want to make your member list visible again,
<a href="/@{data.user.name}/other">edit your user profile</a>.
<br />
{/if}
<Icon name="info-circle-fill" aria-hidden />
This <em>only</em> hides this member from your member list.
<strong>
This member will still be visible to anyone at
<code class="text-nowrap">pronouns.cc/@{data.user.name}/{data.member.name}</code>.
</strong>
</p>
</div>
{#if PUBLIC_SHORT_BASE}
<div class="col-md">
<p>
Current short ID: <code>{data.member.sid}</code>
<ButtonGroup class="mb-1">
<Button color="secondary" disabled={!canRerollSid} on:click={() => rerollSid()}
>Reroll short ID</Button
>
<IconButton
icon="link-45deg"
tooltip="Copy short link"
color="secondary"
click={copyShortURL}
/>
</ButtonGroup>
<br />
<span class="text-muted">
<Icon name="info-circle-fill" aria-hidden />
This ID is used in <code>prns.cc</code> links. You can reroll one short ID every hour (shared
between your main profile and all members) by pressing the button above.
</span>
</p>
</div>
{/if}
</div>
</TabPane>
</TabContent>

View file

@ -1,9 +1,6 @@
import type { PrideFlag, MeUser, APIError, Member, PronounsJson } from "$lib/api/entities";
import { ErrorCode, type APIError, type Member, type MeUser } from "$lib/api/entities";
import { apiFetchClient } from "$lib/api/fetch";
import { error } from "@sveltejs/kit";
import pronounsRaw from "$lib/pronouns.json";
const pronouns = pronounsRaw as PronounsJson;
import { error, redirect } from "@sveltejs/kit";
export const ssr = false;
@ -11,15 +8,24 @@ export const load = async ({ params }) => {
try {
const user = await apiFetchClient<MeUser>(`/users/@me`);
const member = await apiFetchClient<Member>(`/members/${params.id}`);
const flags = await apiFetchClient<PrideFlag[]>("/users/@me/flags");
return {
user,
member,
pronouns: pronouns.autocomplete,
flags,
};
if (user.id !== member.user.id) {
throw {
code: ErrorCode.NotOwnMember,
message: "You can only edit your own members.",
} as APIError;
}
throw redirect(303, `/@${user.name}/${member.name}/edit`);
} catch (e) {
throw error((e as APIError).code, e as APIError);
if (
(e as APIError).code === ErrorCode.Forbidden ||
(e as APIError).code === ErrorCode.InvalidToken ||
(e as APIError).code === ErrorCode.NotOwnMember
) {
throw error(403, e as APIError);
}
throw e;
}
};

View file

@ -1 +0,0 @@
<div />

View file

@ -10,8 +10,13 @@ export const load = async () => {
throw redirect(303, `/@${resp.name}/edit`);
} catch (e) {
if ((e as APIError).code === ErrorCode.Forbidden || (e as APIError).code === ErrorCode.InvalidToken) {
throw error(403, e as APIError)
if (
(e as APIError).code === ErrorCode.Forbidden ||
(e as APIError).code === ErrorCode.InvalidToken
) {
throw error(403, e as APIError);
}
throw e;
}
};