move member edit page to /@user/member/edit

This commit is contained in:
sam 2023-08-14 02:03:09 +02:00
parent 56c9270fdb
commit 03311d7004
No known key found for this signature in database
GPG Key ID: B4EF20DDE721CAA1
9 changed files with 480 additions and 29 deletions

View File

@ -10,6 +10,9 @@
>log in again</a >log in again</a
>. >.
</p> </p>
{:else if $page.error?.code === ErrorCode.NotOwnMember}
<h1>Not your member</h1>
<p>You can only edit your own members.</p>
{:else} {:else}
<h1>An error occurred ({$page.status})</h1> <h1>An error occurred ({$page.status})</h1>

View File

@ -107,7 +107,9 @@
href="/@{$member.user.name}/{data.member.name}" href="/@{$member.user.name}/{data.member.name}"
tooltip="Back to member" tooltip="Back to member"
/> />
<Button color="success" on:click={() => updateMember()} disabled={!memberNameValid}>Save changes</Button> <Button color="success" on:click={() => updateMember()} disabled={!memberNameValid}>
Save changes
</Button>
<Button color="danger" on:click={toggleDeleteOpen} <Button color="danger" on:click={toggleDeleteOpen}
>Delete {data.member.display_name ?? data.member.name}</Button >Delete {data.member.display_name ?? data.member.name}</Button
> >
@ -119,30 +121,27 @@
{/if} {/if}
<Nav tabs> <Nav tabs>
<NavItem <NavItem>
><ActiveLink href="/@{$member.user.name}/{$member.name}/edit">Names and avatar</ActiveLink <ActiveLink href="/@{$member.user.name}/{$member.name}/edit">Names and avatar</ActiveLink>
></NavItem </NavItem>
> <NavItem>
<NavItem <ActiveLink href="/@{$member.user.name}/{$member.name}/edit/bio">Bio</ActiveLink>
><ActiveLink href="/@{$member.user.name}/{$member.name}/edit/bio">Bio</ActiveLink></NavItem </NavItem>
> <NavItem>
<NavItem <ActiveLink href="/@{$member.user.name}/{$member.name}/edit/pronouns">Pronouns</ActiveLink>
><ActiveLink href="/@{$member.user.name}/{$member.name}/edit/pronouns">Pronouns</ActiveLink </NavItem>
></NavItem <NavItem>
> <ActiveLink href="/@{$member.user.name}/{$member.name}/edit/fields">Fields</ActiveLink>
<NavItem </NavItem>
><ActiveLink href="/@{$member.user.name}/{$member.name}/edit/fields">Fields</ActiveLink <NavItem>
></NavItem <ActiveLink href="/@{$member.user.name}/{$member.name}/edit/flags">Flags</ActiveLink>
> </NavItem>
<NavItem <NavItem>
><ActiveLink href="/@{$member.user.name}/{$member.name}/edit/flags">Flags</ActiveLink></NavItem <ActiveLink href="/@{$member.user.name}/{$member.name}/edit/links">Links</ActiveLink>
> </NavItem>
<NavItem <NavItem>
><ActiveLink href="/@{$member.user.name}/{$member.name}/edit/links">Links</ActiveLink></NavItem <ActiveLink href="/@{$member.user.name}/{$member.name}/edit/other">Other</ActiveLink>
> </NavItem>
<NavItem
><ActiveLink href="/@{$member.user.name}/{$member.name}/edit/other">Other</ActiveLink></NavItem
>
</Nav> </Nav>
<div class="mt-3"> <div class="mt-3">

View File

@ -1,4 +1,11 @@
import type { PrideFlag, MeUser, APIError, Member, PronounsJson } from "$lib/api/entities"; import {
type PrideFlag,
type MeUser,
type APIError,
type Member,
type PronounsJson,
ErrorCode,
} from "$lib/api/entities";
import { apiFetchClient } from "$lib/api/fetch"; import { apiFetchClient } from "$lib/api/fetch";
import { error, redirect } from "@sveltejs/kit"; import { error, redirect } from "@sveltejs/kit";
@ -11,11 +18,21 @@ export const ssr = false;
export const load = (async ({ params }) => { export const load = (async ({ params }) => {
try { try {
const user = await apiFetchClient<MeUser>(`/users/@me`); const user = await apiFetchClient<MeUser>(`/users/@me`);
const member = await apiFetchClient<Member>(`/users/@me/members/${params.memberName}`); const member = await apiFetchClient<Member>(
`/users/${params.username}/members/${params.memberName}`,
);
const flags = await apiFetchClient<PrideFlag[]>("/users/@me/flags"); const flags = await apiFetchClient<PrideFlag[]>("/users/@me/flags");
if (user.name !== params.username || member.user.name !== params.username || member.name !== params.memberName) { if (user.id !== member.user.id) {
throw redirect(303, `/@${user.name}/${member.name}`); throw { code: ErrorCode.NotOwnMember, message: "Can only edit your own members" } as APIError;
}
if (
user.name !== params.username ||
member.user.name !== params.username ||
member.name !== params.memberName
) {
throw redirect(303, `/@${user.name}/${member.name}`);
} }
return { return {

View File

@ -0,0 +1,29 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Writable } from "svelte/store";
import { MAX_DESCRIPTION_LENGTH, type Member } from "$lib/api/entities";
import { charCount, renderMarkdown } from "$lib/utils";
import MarkdownHelp from "$lib/components/edit/MarkdownHelp.svelte";
import { Card, CardBody, CardHeader } from "sveltestrap";
const member = getContext<Writable<Member>>("member");
</script>
<div class="form">
<textarea class="form-control" style="height: 200px;" bind:value={$member.bio} />
</div>
<p class="text-muted mt-1">
Using {charCount($member.bio || "")}/{MAX_DESCRIPTION_LENGTH} characters
</p>
<p class="text-muted my-2">
<MarkdownHelp />
</p>
{#if $member.bio}
<hr />
<Card>
<CardHeader>Preview</CardHeader>
<CardBody>
{@html renderMarkdown($member.bio)}
</CardBody>
</Card>
{/if}

View File

@ -0,0 +1,53 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Writable } from "svelte/store";
import { Alert, Button, Icon } from "sveltestrap";
import type { Member } from "$lib/api/entities";
import EditableField from "$lib/components/edit/EditableField.svelte";
const member = getContext<Writable<Member>>("member");
const moveField = (index: number, up: boolean) => {
if (up && index == 0) return;
if (!up && index == $member.fields.length - 1) return;
const newIndex = up ? index - 1 : index + 1;
const temp = $member.fields[index];
$member.fields[index] = $member.fields[newIndex];
$member.fields[newIndex] = temp;
$member.fields = [...$member.fields];
};
const removeField = (index: number) => {
$member.fields.splice(index, 1);
$member.fields = [...$member.fields];
};
</script>
{#if $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 $member.fields as _, index}
<EditableField
bind:field={$member.fields[index]}
preferences={$member.user.custom_preferences}
deleteField={() => removeField(index)}
moveField={(up) => moveField(index, up)}
/>
{/each}
</div>
</div>
<div>
<Button
on:click={() => ($member.fields = [...$member.fields, { name: "New field", entries: [] }])}
>
<Icon name="plus" aria-hidden /> Add new field
</Button>
</div>

View File

@ -0,0 +1,104 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Writable } from "svelte/store";
import { Alert, ButtonGroup, Input } from "sveltestrap";
import type { PageData } from "./$types";
import type { Member, PrideFlag } from "$lib/api/entities";
import IconButton from "$lib/components/IconButton.svelte";
import FlagButton from "$lib/components/edit/FlagButton.svelte";
export let data: PageData;
const member = getContext<Writable<Member>>("member");
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);
};
const addFlag = (flag: PrideFlag) => {
$member.flags = [...$member.flags, flag];
};
const moveFlag = (index: number, up: boolean) => {
if (up && index == 0) return;
if (!up && index == $member.flags.length - 1) return;
const newIndex = up ? index - 1 : index + 1;
const temp = $member.flags[index];
$member.flags[index] = $member.flags[newIndex];
$member.flags[newIndex] = temp;
$member.flags = [...$member.flags];
};
const removeFlag = (index: number) => {
$member.flags.splice(index, 1);
$member.flags = [...$member.flags];
};
</script>
<div>
{#each $member.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={$member.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>

View File

@ -0,0 +1,63 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Writable } from "svelte/store";
import type { Member } from "$lib/api/entities";
import IconButton from "$lib/components/IconButton.svelte";
const member = getContext<Writable<Member>>("member");
let newLink = "";
const addLink = (event: Event) => {
event.preventDefault();
$member.links = [...$member.links, newLink];
newLink = "";
};
const removeLink = (index: number) => {
$member.links.splice(index, 1);
$member.links = [...$member.links];
};
const moveLink = (index: number, up: boolean) => {
if (up && index == 0) return;
if (!up && index == $member.links.length - 1) return;
const newIndex = up ? index - 1 : index + 1;
const temp = $member.links[index];
$member.links[index] = $member.links[newIndex];
$member.links[newIndex] = temp;
$member.links = [...$member.links];
};
</script>
{#each $member.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={$member.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>

View File

@ -0,0 +1,99 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Writable } from "svelte/store";
import { DateTime } from "luxon";
import { Button, ButtonGroup, Icon } from "sveltestrap";
import type { APIError, Member } from "$lib/api/entities";
import { PUBLIC_SHORT_BASE } from "$env/static/public";
import IconButton from "$lib/components/IconButton.svelte";
import { apiFetchClient } from "$lib/api/fetch";
import { addToast } from "$lib/toast";
import type { PageData } from "./$types";
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
export let data: PageData;
const member = getContext<Writable<Member>>("member");
let error: 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;
$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 });
};
</script>
{#if error}
<ErrorAlert {error} />
{/if}
<div class="row">
<div class="col-md">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
bind:checked={$member.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="/@{$member.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/@{$member.user.name}/{$member.name}</code>.
</strong>
</p>
</div>
{#if PUBLIC_SHORT_BASE}
<div class="col-md">
<p>
Current short ID: <code>{$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>

View File

@ -0,0 +1,84 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Writable } from "svelte/store";
import type { Member } from "$lib/api/entities";
import { Button, Icon, Popover } from "sveltestrap";
import EditablePronouns from "$lib/components/edit/EditablePronouns.svelte";
import IconButton from "$lib/components/IconButton.svelte";
import type { PageData } from "./$types";
export let data: PageData;
const member = getContext<Writable<Member>>("member");
let newPronouns = "";
const movePronoun = (index: number, up: boolean) => {
if (up && index == 0) return;
if (!up && index == $member.pronouns.length - 1) return;
const newIndex = up ? index - 1 : index + 1;
const temp = $member.pronouns[index];
$member.pronouns[index] = $member.pronouns[newIndex];
$member.pronouns[newIndex] = temp;
$member.pronouns = [...$member.pronouns];
};
const addPronouns = (event: Event) => {
event.preventDefault();
if (newPronouns in data.pronouns) {
const fullSet = data.pronouns[newPronouns];
$member.pronouns = [
...$member.pronouns,
{
pronouns: fullSet.pronouns.join("/"),
display_text: fullSet.display || null,
status: "okay",
},
];
} else {
$member.pronouns = [
...$member.pronouns,
{ pronouns: newPronouns, display_text: null, status: "okay" },
];
}
newPronouns = "";
};
const removePronoun = (index: number) => {
$member.pronouns.splice(index, 1);
$member.pronouns = [...$member.pronouns];
};
</script>
{#each $member.pronouns as _, index}
<EditablePronouns
bind:pronoun={$member.pronouns[index]}
preferences={$member.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>