forked from mirrors/pronouns.cc
move member edit page to /@user/member/edit
This commit is contained in:
parent
56c9270fdb
commit
03311d7004
9 changed files with 480 additions and 29 deletions
|
@ -10,6 +10,9 @@
|
|||
>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>
|
||||
|
||||
|
|
|
@ -107,7 +107,9 @@
|
|||
href="/@{$member.user.name}/{data.member.name}"
|
||||
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}
|
||||
>Delete {data.member.display_name ?? data.member.name}</Button
|
||||
>
|
||||
|
@ -119,30 +121,27 @@
|
|||
{/if}
|
||||
|
||||
<Nav tabs>
|
||||
<NavItem
|
||||
><ActiveLink href="/@{$member.user.name}/{$member.name}/edit">Names and avatar</ActiveLink
|
||||
></NavItem
|
||||
>
|
||||
<NavItem
|
||||
><ActiveLink href="/@{$member.user.name}/{$member.name}/edit/bio">Bio</ActiveLink></NavItem
|
||||
>
|
||||
<NavItem
|
||||
><ActiveLink href="/@{$member.user.name}/{$member.name}/edit/pronouns">Pronouns</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
|
||||
><ActiveLink href="/@{$member.user.name}/{$member.name}/edit/links">Links</ActiveLink></NavItem
|
||||
>
|
||||
<NavItem
|
||||
><ActiveLink href="/@{$member.user.name}/{$member.name}/edit/other">Other</ActiveLink></NavItem
|
||||
>
|
||||
<NavItem>
|
||||
<ActiveLink href="/@{$member.user.name}/{$member.name}/edit">Names and avatar</ActiveLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<ActiveLink href="/@{$member.user.name}/{$member.name}/edit/bio">Bio</ActiveLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<ActiveLink href="/@{$member.user.name}/{$member.name}/edit/pronouns">Pronouns</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>
|
||||
<ActiveLink href="/@{$member.user.name}/{$member.name}/edit/links">Links</ActiveLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<ActiveLink href="/@{$member.user.name}/{$member.name}/edit/other">Other</ActiveLink>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
|
||||
<div class="mt-3">
|
||||
|
|
|
@ -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 { error, redirect } from "@sveltejs/kit";
|
||||
|
||||
|
@ -11,10 +18,20 @@ export const ssr = false;
|
|||
export const load = (async ({ params }) => {
|
||||
try {
|
||||
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");
|
||||
|
||||
if (user.name !== params.username || member.user.name !== params.username || member.name !== params.memberName) {
|
||||
if (user.id !== member.user.id) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in a new issue