2023-03-12 15:59:20 +01:00
|
|
|
<script lang="ts">
|
|
|
|
import { goto } from "$app/navigation";
|
2023-03-13 01:08:46 +01:00
|
|
|
import {
|
2023-03-13 17:11:05 +01:00
|
|
|
MAX_DESCRIPTION_LENGTH,
|
2023-03-13 02:04:09 +01:00
|
|
|
userAvatars,
|
2023-03-13 01:08:46 +01:00
|
|
|
WordStatus,
|
|
|
|
type APIError,
|
|
|
|
type Field,
|
|
|
|
type FieldEntry,
|
|
|
|
type MeUser,
|
|
|
|
type Pronoun,
|
|
|
|
} from "$lib/api/entities";
|
2023-03-12 15:59:20 +01:00
|
|
|
import FallbackImage from "$lib/components/FallbackImage.svelte";
|
|
|
|
import { userStore } from "$lib/store";
|
2023-03-13 01:08:46 +01:00
|
|
|
import { Alert, Button, FormGroup, Icon, Input } from "sveltestrap";
|
2023-03-12 15:59:20 +01:00
|
|
|
import { encode } from "base64-arraybuffer";
|
|
|
|
import { apiFetchClient } from "$lib/api/fetch";
|
2023-03-13 15:36:41 +01:00
|
|
|
import IconButton from "$lib/components/IconButton.svelte";
|
2023-03-12 15:59:20 +01:00
|
|
|
|
|
|
|
const MAX_AVATAR_BYTES = 1_000_000;
|
|
|
|
|
|
|
|
if (!$userStore) {
|
|
|
|
goto("/");
|
|
|
|
}
|
|
|
|
|
|
|
|
let error: APIError | null = null;
|
|
|
|
|
|
|
|
let bio: string = $userStore?.bio || "";
|
|
|
|
let display_name: string = $userStore?.display_name || "";
|
2023-03-13 15:36:41 +01:00
|
|
|
let links: string[] = $userStore ? window.structuredClone($userStore.links) : [];
|
2023-03-12 15:59:20 +01:00
|
|
|
let names: FieldEntry[] = $userStore ? window.structuredClone($userStore.names) : [];
|
|
|
|
let pronouns: Pronoun[] = $userStore ? window.structuredClone($userStore.pronouns) : [];
|
|
|
|
let fields: Field[] = $userStore ? window.structuredClone($userStore.fields) : [];
|
|
|
|
|
|
|
|
let avatar: string | null;
|
|
|
|
let avatar_files: FileList | null;
|
|
|
|
|
2023-03-13 15:36:41 +01:00
|
|
|
let newName = "";
|
|
|
|
let newPronouns = "";
|
|
|
|
let newPronounsDisplay = "";
|
|
|
|
let newLink = "";
|
|
|
|
|
2023-03-12 15:59:20 +01:00
|
|
|
let modified = false;
|
|
|
|
|
|
|
|
$: redirectIfNoAuth($userStore);
|
2023-03-13 15:36:41 +01:00
|
|
|
$: modified = isModified(bio, display_name, links, names, pronouns, fields, avatar);
|
2023-03-12 15:59:20 +01:00
|
|
|
$: getAvatar(avatar_files).then((b64) => (avatar = b64));
|
|
|
|
|
|
|
|
const redirectIfNoAuth = (user: MeUser | null) => {
|
|
|
|
if (!user) {
|
|
|
|
goto("/");
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const isModified = (
|
|
|
|
bio: string,
|
|
|
|
display_name: string,
|
2023-03-13 15:36:41 +01:00
|
|
|
links: string[],
|
2023-03-12 15:59:20 +01:00
|
|
|
names: FieldEntry[],
|
|
|
|
pronouns: Pronoun[],
|
|
|
|
fields: Field[],
|
2023-03-13 15:36:41 +01:00
|
|
|
avatar: string | null,
|
2023-03-12 15:59:20 +01:00
|
|
|
) => {
|
|
|
|
if (!$userStore) return false;
|
|
|
|
|
|
|
|
if (bio !== $userStore.bio) return true;
|
|
|
|
if (display_name !== $userStore.display_name) return true;
|
2023-03-13 15:36:41 +01:00
|
|
|
if (!linksEqual(links, $userStore.links)) return true;
|
2023-03-12 15:59:20 +01:00
|
|
|
if (!fieldsEqual(fields, $userStore.fields)) return true;
|
|
|
|
if (!namesEqual(names, $userStore.names)) return true;
|
|
|
|
if (!pronounsEqual(pronouns, $userStore.pronouns)) return true;
|
|
|
|
if (avatar !== null) return true;
|
|
|
|
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
const fieldsEqual = (arr1: Field[], arr2: Field[]) => {
|
|
|
|
if (arr1?.length !== arr2?.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;
|
|
|
|
};
|
|
|
|
|
2023-03-13 15:36:41 +01:00
|
|
|
const linksEqual = (arr1: string[], arr2: string[]) => {
|
|
|
|
if (arr1.length !== arr2.length) return false;
|
|
|
|
return arr1.every((_, i) => arr1[i] === arr2[i]);
|
|
|
|
};
|
|
|
|
|
2023-03-12 15:59:20 +01:00
|
|
|
const getAvatar = async (list: FileList | null) => {
|
|
|
|
if (!list || list.length === 0) return null;
|
|
|
|
if (list[0].size > MAX_AVATAR_BYTES) return null;
|
|
|
|
|
|
|
|
const buffer = await list[0].arrayBuffer();
|
|
|
|
const base64 = encode(buffer);
|
|
|
|
|
|
|
|
const uri = `data:${dataTypeFromFilename(list[0].name)};base64,${base64}`;
|
|
|
|
|
|
|
|
return uri;
|
|
|
|
};
|
|
|
|
|
|
|
|
const dataTypeFromFilename = (filename: string) => {
|
|
|
|
if (filename.endsWith(".webp")) {
|
|
|
|
return "image/webp";
|
|
|
|
} else if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) {
|
|
|
|
return "image/jpeg";
|
|
|
|
} else if (filename.endsWith(".png")) {
|
|
|
|
return "image/png";
|
|
|
|
} else if (filename.endsWith(".gif")) {
|
|
|
|
return "image/gif";
|
|
|
|
} else {
|
|
|
|
return "application/octet-stream";
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-03-13 01:08:46 +01:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2023-03-13 15:36:41 +01:00
|
|
|
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)];
|
|
|
|
};
|
|
|
|
|
2023-03-12 15:59:20 +01:00
|
|
|
const updateUser = async () => {
|
|
|
|
try {
|
|
|
|
const resp = await apiFetchClient<MeUser>("/users/@me", "PATCH", {
|
|
|
|
display_name,
|
|
|
|
avatar,
|
|
|
|
bio,
|
2023-03-13 15:36:41 +01:00
|
|
|
links,
|
2023-03-12 15:59:20 +01:00
|
|
|
names,
|
|
|
|
pronouns,
|
|
|
|
fields,
|
|
|
|
});
|
|
|
|
|
|
|
|
userStore.set(resp);
|
|
|
|
localStorage.setItem("pronouns-user", JSON.stringify(resp));
|
|
|
|
|
2023-03-13 01:08:46 +01:00
|
|
|
avatar = null;
|
2023-03-12 15:59:20 +01:00
|
|
|
error = null;
|
|
|
|
modified = false;
|
|
|
|
} catch (e) {
|
|
|
|
error = e as APIError;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
</script>
|
|
|
|
|
2023-03-13 01:08:46 +01:00
|
|
|
<svelte:head>
|
|
|
|
<title>Edit profile - pronouns.cc</title>
|
|
|
|
</svelte:head>
|
|
|
|
|
2023-03-12 15:59:20 +01:00
|
|
|
<h1>
|
|
|
|
Edit profile
|
|
|
|
{#if modified}
|
|
|
|
<Button color="success" on:click={() => updateUser()}>Save changes</Button>
|
|
|
|
{/if}
|
|
|
|
</h1>
|
|
|
|
|
|
|
|
{#if error}
|
|
|
|
<Alert color="danger" fade={false}>
|
|
|
|
<h4 class="alert-header">An error occurred</h4>
|
|
|
|
<p>
|
|
|
|
<b>{error.code}</b>: {error.message}
|
|
|
|
{#if error.details}{error.details}{/if}
|
|
|
|
</p>
|
|
|
|
</Alert>
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
{#if !$userStore}
|
|
|
|
<Alert color="danger" fade={false}>Error: No user object</Alert>
|
|
|
|
{:else}
|
|
|
|
<div class="grid">
|
2023-03-13 01:08:46 +01:00
|
|
|
<div class="row m-1">
|
2023-03-12 15:59:20 +01:00
|
|
|
<div class="col-md">
|
2023-03-13 01:08:46 +01:00
|
|
|
<h4>Avatar</h4>
|
2023-03-12 15:59:20 +01:00
|
|
|
<div class="row">
|
|
|
|
<div class="col-md">
|
|
|
|
{#if avatar}
|
|
|
|
<img width={200} src={avatar} alt="New avatar" class="rounded-circle img-fluid" />
|
|
|
|
{:else}
|
2023-03-13 02:04:09 +01:00
|
|
|
<FallbackImage alt="Current avatar" urls={userAvatars($userStore)} width={200} />
|
2023-03-12 15:59:20 +01:00
|
|
|
{/if}
|
|
|
|
</div>
|
|
|
|
<div class="col-md">
|
|
|
|
<input class="form-control" id="avatar" type="file" bind:files={avatar_files} />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="col-md">
|
|
|
|
<FormGroup floating label="Display name">
|
|
|
|
<Input bind:value={display_name} />
|
|
|
|
</FormGroup>
|
|
|
|
<div>
|
2023-03-13 17:11:05 +01:00
|
|
|
<FormGroup floating label="Bio ({bio.length}/{MAX_DESCRIPTION_LENGTH})">
|
2023-03-12 15:59:20 +01:00
|
|
|
<textarea style="min-height: 100px;" class="form-control" bind:value={bio} />
|
|
|
|
</FormGroup>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2023-03-13 01:08:46 +01:00
|
|
|
<div class="row m-1">
|
|
|
|
<div class="col-md">
|
|
|
|
<h4>Names</h4>
|
|
|
|
{#each names as _, index}
|
|
|
|
<div class="input-group m-1">
|
|
|
|
<Button color="secondary" on:click={() => moveName(index, true)}>
|
|
|
|
<Icon name="chevron-up" />
|
|
|
|
</Button>
|
|
|
|
<Button color="secondary" on:click={() => moveName(index, false)}>
|
|
|
|
<Icon name="chevron-down" />
|
|
|
|
</Button>
|
|
|
|
<input type="text" class="form-control" bind:value={names[index].value} />
|
2023-03-13 15:36:41 +01:00
|
|
|
<IconButton
|
2023-03-13 01:08:46 +01:00
|
|
|
color="secondary"
|
2023-03-13 15:36:41 +01:00
|
|
|
icon="heart-fill"
|
|
|
|
tooltip="Favourite"
|
|
|
|
click={() => (names[index].status = WordStatus.Favourite)}
|
2023-03-13 01:08:46 +01:00
|
|
|
active={names[index].status === WordStatus.Favourite}
|
2023-03-13 15:36:41 +01:00
|
|
|
/>
|
|
|
|
<IconButton
|
2023-03-13 01:08:46 +01:00
|
|
|
color="secondary"
|
2023-03-13 15:36:41 +01:00
|
|
|
icon="hand-thumbs-up"
|
|
|
|
tooltip="Okay"
|
|
|
|
click={() => (names[index].status = WordStatus.Okay)}
|
2023-03-13 01:08:46 +01:00
|
|
|
active={names[index].status === WordStatus.Okay}
|
2023-03-13 15:36:41 +01:00
|
|
|
/>
|
|
|
|
<IconButton
|
2023-03-13 01:08:46 +01:00
|
|
|
color="secondary"
|
2023-03-13 15:36:41 +01:00
|
|
|
icon="emoji-laughing"
|
|
|
|
tooltip="Jokingly"
|
|
|
|
click={() => (names[index].status = WordStatus.Jokingly)}
|
2023-03-13 01:08:46 +01:00
|
|
|
active={names[index].status === WordStatus.Jokingly}
|
2023-03-13 15:36:41 +01:00
|
|
|
/>
|
|
|
|
<IconButton
|
2023-03-13 01:08:46 +01:00
|
|
|
color="secondary"
|
2023-03-13 15:36:41 +01:00
|
|
|
icon="people"
|
|
|
|
tooltip="Friends only"
|
|
|
|
click={() => (names[index].status = WordStatus.FriendsOnly)}
|
2023-03-13 01:08:46 +01:00
|
|
|
active={names[index].status === WordStatus.FriendsOnly}
|
2023-03-13 15:36:41 +01:00
|
|
|
/>
|
|
|
|
<IconButton
|
2023-03-13 01:08:46 +01:00
|
|
|
color="secondary"
|
2023-03-13 15:36:41 +01:00
|
|
|
icon="hand-thumbs-down"
|
|
|
|
tooltip="Avoid"
|
|
|
|
click={() => (names[index].status = WordStatus.Avoid)}
|
2023-03-13 01:08:46 +01:00
|
|
|
active={names[index].status === WordStatus.Avoid}
|
2023-03-13 15:36:41 +01:00
|
|
|
/>
|
|
|
|
<IconButton
|
|
|
|
color="danger"
|
|
|
|
icon="trash3"
|
|
|
|
tooltip="Remove name"
|
|
|
|
click={() => removeName(index)}
|
|
|
|
/>
|
2023-03-13 01:08:46 +01:00
|
|
|
</div>
|
|
|
|
{/each}
|
2023-03-13 15:36:41 +01:00
|
|
|
<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>
|
2023-03-13 01:08:46 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="row m-1">
|
|
|
|
<div class="col-md">
|
|
|
|
<h4>Pronouns</h4>
|
|
|
|
{#each pronouns as _, index}
|
|
|
|
<div class="input-group m-1">
|
|
|
|
<Button color="secondary" on:click={() => movePronoun(index, true)}>
|
|
|
|
<Icon name="chevron-up" />
|
|
|
|
</Button>
|
|
|
|
<Button color="secondary" on:click={() => movePronoun(index, false)}>
|
|
|
|
<Icon name="chevron-down" />
|
|
|
|
</Button>
|
|
|
|
<input type="text" class="form-control" bind:value={pronouns[index].pronouns} />
|
|
|
|
<input type="text" class="form-control" bind:value={pronouns[index].display_text} />
|
2023-03-13 15:36:41 +01:00
|
|
|
<IconButton
|
2023-03-13 01:08:46 +01:00
|
|
|
color="secondary"
|
2023-03-13 15:36:41 +01:00
|
|
|
icon="heart-fill"
|
|
|
|
tooltip="Favourite"
|
|
|
|
click={() => (pronouns[index].status = WordStatus.Favourite)}
|
2023-03-13 01:08:46 +01:00
|
|
|
active={pronouns[index].status === WordStatus.Favourite}
|
2023-03-13 15:36:41 +01:00
|
|
|
/>
|
|
|
|
<IconButton
|
2023-03-13 01:08:46 +01:00
|
|
|
color="secondary"
|
2023-03-13 15:36:41 +01:00
|
|
|
icon="hand-thumbs-up"
|
|
|
|
tooltip="Okay"
|
|
|
|
click={() => (pronouns[index].status = WordStatus.Okay)}
|
2023-03-13 01:08:46 +01:00
|
|
|
active={pronouns[index].status === WordStatus.Okay}
|
2023-03-13 15:36:41 +01:00
|
|
|
/>
|
|
|
|
<IconButton
|
2023-03-13 01:08:46 +01:00
|
|
|
color="secondary"
|
2023-03-13 15:36:41 +01:00
|
|
|
icon="emoji-laughing"
|
|
|
|
tooltip="Jokingly"
|
|
|
|
click={() => (pronouns[index].status = WordStatus.Jokingly)}
|
2023-03-13 01:08:46 +01:00
|
|
|
active={pronouns[index].status === WordStatus.Jokingly}
|
2023-03-13 15:36:41 +01:00
|
|
|
/>
|
|
|
|
<IconButton
|
2023-03-13 01:08:46 +01:00
|
|
|
color="secondary"
|
2023-03-13 15:36:41 +01:00
|
|
|
icon="people"
|
|
|
|
tooltip="Friends only"
|
|
|
|
click={() => (pronouns[index].status = WordStatus.FriendsOnly)}
|
2023-03-13 01:08:46 +01:00
|
|
|
active={pronouns[index].status === WordStatus.FriendsOnly}
|
2023-03-13 15:36:41 +01:00
|
|
|
/>
|
|
|
|
<IconButton
|
2023-03-13 01:08:46 +01:00
|
|
|
color="secondary"
|
2023-03-13 15:36:41 +01:00
|
|
|
icon="hand-thumbs-down"
|
|
|
|
tooltip="Avoid"
|
|
|
|
click={() => (pronouns[index].status = WordStatus.Avoid)}
|
2023-03-13 01:08:46 +01:00
|
|
|
active={pronouns[index].status === WordStatus.Avoid}
|
2023-03-13 15:36:41 +01:00
|
|
|
/>
|
|
|
|
<IconButton
|
|
|
|
color="danger"
|
|
|
|
icon="trash3"
|
|
|
|
tooltip="Remove pronouns"
|
|
|
|
click={() => removePronoun(index)}
|
|
|
|
/>
|
2023-03-13 01:08:46 +01:00
|
|
|
</div>
|
|
|
|
{/each}
|
2023-03-13 15:36:41 +01:00
|
|
|
<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>
|
2023-03-13 01:08:46 +01:00
|
|
|
</div>
|
|
|
|
</div>
|
2023-03-12 15:59:20 +01:00
|
|
|
</div>
|
|
|
|
{/if}
|