forked from mirrors/pronouns.cc
feat(frontend): add links and add/delete names/pronouns to edit profile page
This commit is contained in:
parent
57ed81add3
commit
10adeec841
2 changed files with 167 additions and 57 deletions
16
frontend/src/lib/components/IconButton.svelte
Normal file
16
frontend/src/lib/components/IconButton.svelte
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Button, Icon, Tooltip } from "sveltestrap";
|
||||||
|
|
||||||
|
export let icon: string;
|
||||||
|
export let color: "primary" | "secondary" | "success" | "danger";
|
||||||
|
export let tooltip: string;
|
||||||
|
export let active: boolean = false;
|
||||||
|
export let click: (e: MouseEvent) => void;
|
||||||
|
|
||||||
|
let button: HTMLElement;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Tooltip target={button} placement="top">{tooltip}</Tooltip>
|
||||||
|
<Button {color} {active} on:click={click} bind:inner={button}>
|
||||||
|
<Icon name={icon} />
|
||||||
|
</Button>
|
|
@ -14,6 +14,7 @@
|
||||||
import { Alert, Button, FormGroup, Icon, Input } from "sveltestrap";
|
import { Alert, Button, 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";
|
||||||
|
|
||||||
const MAX_AVATAR_BYTES = 1_000_000;
|
const MAX_AVATAR_BYTES = 1_000_000;
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
|
|
||||||
let bio: string = $userStore?.bio || "";
|
let bio: string = $userStore?.bio || "";
|
||||||
let display_name: string = $userStore?.display_name || "";
|
let display_name: string = $userStore?.display_name || "";
|
||||||
|
let links: string[] = $userStore ? window.structuredClone($userStore.links) : [];
|
||||||
let names: FieldEntry[] = $userStore ? window.structuredClone($userStore.names) : [];
|
let names: FieldEntry[] = $userStore ? window.structuredClone($userStore.names) : [];
|
||||||
let pronouns: Pronoun[] = $userStore ? window.structuredClone($userStore.pronouns) : [];
|
let pronouns: Pronoun[] = $userStore ? window.structuredClone($userStore.pronouns) : [];
|
||||||
let fields: Field[] = $userStore ? window.structuredClone($userStore.fields) : [];
|
let fields: Field[] = $userStore ? window.structuredClone($userStore.fields) : [];
|
||||||
|
@ -32,10 +34,15 @@
|
||||||
let avatar: string | null;
|
let avatar: string | null;
|
||||||
let avatar_files: FileList | null;
|
let avatar_files: FileList | null;
|
||||||
|
|
||||||
|
let newName = "";
|
||||||
|
let newPronouns = "";
|
||||||
|
let newPronounsDisplay = "";
|
||||||
|
let newLink = "";
|
||||||
|
|
||||||
let modified = false;
|
let modified = false;
|
||||||
|
|
||||||
$: redirectIfNoAuth($userStore);
|
$: redirectIfNoAuth($userStore);
|
||||||
$: modified = isModified(bio, display_name, names, pronouns, fields);
|
$: modified = isModified(bio, display_name, links, names, pronouns, fields, avatar);
|
||||||
$: getAvatar(avatar_files).then((b64) => (avatar = b64));
|
$: getAvatar(avatar_files).then((b64) => (avatar = b64));
|
||||||
|
|
||||||
const redirectIfNoAuth = (user: MeUser | null) => {
|
const redirectIfNoAuth = (user: MeUser | null) => {
|
||||||
|
@ -47,14 +54,17 @@
|
||||||
const isModified = (
|
const isModified = (
|
||||||
bio: string,
|
bio: string,
|
||||||
display_name: string,
|
display_name: string,
|
||||||
|
links: string[],
|
||||||
names: FieldEntry[],
|
names: FieldEntry[],
|
||||||
pronouns: Pronoun[],
|
pronouns: Pronoun[],
|
||||||
fields: Field[],
|
fields: Field[],
|
||||||
|
avatar: string | null,
|
||||||
) => {
|
) => {
|
||||||
if (!$userStore) return false;
|
if (!$userStore) return false;
|
||||||
|
|
||||||
if (bio !== $userStore.bio) return true;
|
if (bio !== $userStore.bio) return true;
|
||||||
if (display_name !== $userStore.display_name) return true;
|
if (display_name !== $userStore.display_name) return true;
|
||||||
|
if (!linksEqual(links, $userStore.links)) return true;
|
||||||
if (!fieldsEqual(fields, $userStore.fields)) return true;
|
if (!fieldsEqual(fields, $userStore.fields)) return true;
|
||||||
if (!namesEqual(names, $userStore.names)) return true;
|
if (!namesEqual(names, $userStore.names)) return true;
|
||||||
if (!pronounsEqual(pronouns, $userStore.pronouns)) return true;
|
if (!pronounsEqual(pronouns, $userStore.pronouns)) return true;
|
||||||
|
@ -92,6 +102,11 @@
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const linksEqual = (arr1: string[], arr2: string[]) => {
|
||||||
|
if (arr1.length !== arr2.length) return false;
|
||||||
|
return arr1.every((_, i) => arr1[i] === arr2[i]);
|
||||||
|
};
|
||||||
|
|
||||||
const getAvatar = async (list: FileList | null) => {
|
const getAvatar = async (list: FileList | null) => {
|
||||||
if (!list || list.length === 0) return null;
|
if (!list || list.length === 0) return null;
|
||||||
if (list[0].size > MAX_AVATAR_BYTES) return null;
|
if (list[0].size > MAX_AVATAR_BYTES) return null;
|
||||||
|
@ -140,12 +155,53 @@
|
||||||
pronouns[newIndex] = temp;
|
pronouns[newIndex] = temp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addName = () => {
|
||||||
|
names = [...names, { value: newName, status: WordStatus.Okay }];
|
||||||
|
newName = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPronouns = () => {
|
||||||
|
pronouns = [
|
||||||
|
...pronouns,
|
||||||
|
{ pronouns: newPronouns, display_text: newPronounsDisplay || null, status: WordStatus.Okay },
|
||||||
|
];
|
||||||
|
newPronouns = "";
|
||||||
|
newPronounsDisplay = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const addLink = () => {
|
||||||
|
links = [...links, newLink];
|
||||||
|
newLink = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeName = (index: number) => {
|
||||||
|
if (names.length === 1) names = [];
|
||||||
|
else if (index === 0) names = names.slice(1);
|
||||||
|
else if (index === names.length - 1) names = names.slice(0, names.length - 1);
|
||||||
|
else names = [...names.slice(0, index - 1), ...names.slice(0, index + 1)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const removePronoun = (index: number) => {
|
||||||
|
if (pronouns.length === 1) pronouns = [];
|
||||||
|
else if (index === 0) pronouns = pronouns.slice(1);
|
||||||
|
else if (index === pronouns.length - 1) pronouns = pronouns.slice(0, pronouns.length - 1);
|
||||||
|
else pronouns = [...pronouns.slice(0, index - 1), ...pronouns.slice(0, index + 1)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeLink = (index: number) => {
|
||||||
|
if (links.length === 1) links = [];
|
||||||
|
else if (index === 0) links = links.slice(1);
|
||||||
|
else if (index === links.length - 1) links = links.slice(0, links.length - 1);
|
||||||
|
else links = [...links.slice(0, index - 1), ...links.slice(0, index + 1)];
|
||||||
|
};
|
||||||
|
|
||||||
const updateUser = async () => {
|
const updateUser = async () => {
|
||||||
try {
|
try {
|
||||||
const resp = await apiFetchClient<MeUser>("/users/@me", "PATCH", {
|
const resp = await apiFetchClient<MeUser>("/users/@me", "PATCH", {
|
||||||
display_name,
|
display_name,
|
||||||
avatar,
|
avatar,
|
||||||
bio,
|
bio,
|
||||||
|
links,
|
||||||
names,
|
names,
|
||||||
pronouns,
|
pronouns,
|
||||||
fields,
|
fields,
|
||||||
|
@ -227,46 +283,71 @@
|
||||||
<Icon name="chevron-down" />
|
<Icon name="chevron-down" />
|
||||||
</Button>
|
</Button>
|
||||||
<input type="text" class="form-control" bind:value={names[index].value} />
|
<input type="text" class="form-control" bind:value={names[index].value} />
|
||||||
<Button
|
<IconButton
|
||||||
color="secondary"
|
color="secondary"
|
||||||
on:click={() => (names[index].status = WordStatus.Favourite)}
|
icon="heart-fill"
|
||||||
|
tooltip="Favourite"
|
||||||
|
click={() => (names[index].status = WordStatus.Favourite)}
|
||||||
active={names[index].status === WordStatus.Favourite}
|
active={names[index].status === WordStatus.Favourite}
|
||||||
>
|
/>
|
||||||
<Icon name="heart-fill" />
|
<IconButton
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
on:click={() => (names[index].status = WordStatus.Okay)}
|
icon="hand-thumbs-up"
|
||||||
|
tooltip="Okay"
|
||||||
|
click={() => (names[index].status = WordStatus.Okay)}
|
||||||
active={names[index].status === WordStatus.Okay}
|
active={names[index].status === WordStatus.Okay}
|
||||||
>
|
/>
|
||||||
<Icon name="hand-thumbs-up" />
|
<IconButton
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
on:click={() => (names[index].status = WordStatus.Jokingly)}
|
icon="emoji-laughing"
|
||||||
|
tooltip="Jokingly"
|
||||||
|
click={() => (names[index].status = WordStatus.Jokingly)}
|
||||||
active={names[index].status === WordStatus.Jokingly}
|
active={names[index].status === WordStatus.Jokingly}
|
||||||
>
|
/>
|
||||||
<Icon name="emoji-laughing" />
|
<IconButton
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
on:click={() => (names[index].status = WordStatus.FriendsOnly)}
|
icon="people"
|
||||||
|
tooltip="Friends only"
|
||||||
|
click={() => (names[index].status = WordStatus.FriendsOnly)}
|
||||||
active={names[index].status === WordStatus.FriendsOnly}
|
active={names[index].status === WordStatus.FriendsOnly}
|
||||||
>
|
/>
|
||||||
<Icon name="people" />
|
<IconButton
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
on:click={() => (names[index].status = WordStatus.Avoid)}
|
icon="hand-thumbs-down"
|
||||||
|
tooltip="Avoid"
|
||||||
|
click={() => (names[index].status = WordStatus.Avoid)}
|
||||||
active={names[index].status === WordStatus.Avoid}
|
active={names[index].status === WordStatus.Avoid}
|
||||||
>
|
/>
|
||||||
<Icon name="hand-thumbs-down" />
|
<IconButton
|
||||||
</Button>
|
color="danger"
|
||||||
<Button color="danger">
|
icon="trash3"
|
||||||
<Icon name="trash3" />
|
tooltip="Remove name"
|
||||||
</Button>
|
click={() => removeName(index)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/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>
|
||||||
<div class="row m-1">
|
<div class="row m-1">
|
||||||
|
@ -282,46 +363,59 @@
|
||||||
</Button>
|
</Button>
|
||||||
<input type="text" class="form-control" bind:value={pronouns[index].pronouns} />
|
<input type="text" class="form-control" bind:value={pronouns[index].pronouns} />
|
||||||
<input type="text" class="form-control" bind:value={pronouns[index].display_text} />
|
<input type="text" class="form-control" bind:value={pronouns[index].display_text} />
|
||||||
<Button
|
<IconButton
|
||||||
color="secondary"
|
color="secondary"
|
||||||
on:click={() => (pronouns[index].status = WordStatus.Favourite)}
|
icon="heart-fill"
|
||||||
|
tooltip="Favourite"
|
||||||
|
click={() => (pronouns[index].status = WordStatus.Favourite)}
|
||||||
active={pronouns[index].status === WordStatus.Favourite}
|
active={pronouns[index].status === WordStatus.Favourite}
|
||||||
>
|
/>
|
||||||
<Icon name="heart-fill" />
|
<IconButton
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
on:click={() => (pronouns[index].status = WordStatus.Okay)}
|
icon="hand-thumbs-up"
|
||||||
|
tooltip="Okay"
|
||||||
|
click={() => (pronouns[index].status = WordStatus.Okay)}
|
||||||
active={pronouns[index].status === WordStatus.Okay}
|
active={pronouns[index].status === WordStatus.Okay}
|
||||||
>
|
/>
|
||||||
<Icon name="hand-thumbs-up" />
|
<IconButton
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
on:click={() => (pronouns[index].status = WordStatus.Jokingly)}
|
icon="emoji-laughing"
|
||||||
|
tooltip="Jokingly"
|
||||||
|
click={() => (pronouns[index].status = WordStatus.Jokingly)}
|
||||||
active={pronouns[index].status === WordStatus.Jokingly}
|
active={pronouns[index].status === WordStatus.Jokingly}
|
||||||
>
|
/>
|
||||||
<Icon name="emoji-laughing" />
|
<IconButton
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
on:click={() => (pronouns[index].status = WordStatus.FriendsOnly)}
|
icon="people"
|
||||||
|
tooltip="Friends only"
|
||||||
|
click={() => (pronouns[index].status = WordStatus.FriendsOnly)}
|
||||||
active={pronouns[index].status === WordStatus.FriendsOnly}
|
active={pronouns[index].status === WordStatus.FriendsOnly}
|
||||||
>
|
/>
|
||||||
<Icon name="people" />
|
<IconButton
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
color="secondary"
|
||||||
on:click={() => (pronouns[index].status = WordStatus.Avoid)}
|
icon="hand-thumbs-down"
|
||||||
|
tooltip="Avoid"
|
||||||
|
click={() => (pronouns[index].status = WordStatus.Avoid)}
|
||||||
active={pronouns[index].status === WordStatus.Avoid}
|
active={pronouns[index].status === WordStatus.Avoid}
|
||||||
>
|
/>
|
||||||
<Icon name="hand-thumbs-down" />
|
<IconButton
|
||||||
</Button>
|
color="danger"
|
||||||
<Button color="danger">
|
icon="trash3"
|
||||||
<Icon name="trash3" />
|
tooltip="Remove pronouns"
|
||||||
</Button>
|
click={() => removePronoun(index)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
<div class="input-group m-1">
|
||||||
|
<input type="text" class="form-control" bind:value={newPronouns} />
|
||||||
|
<input type="text" class="form-control" bind:value={newPronounsDisplay} />
|
||||||
|
<IconButton
|
||||||
|
color="success"
|
||||||
|
icon="plus"
|
||||||
|
tooltip="Add pronouns"
|
||||||
|
click={() => addPronouns()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue