add fields and flags to new edit page

This commit is contained in:
sam 2023-08-10 18:09:10 +02:00
parent 61f1464e37
commit 575aa01fa5
No known key found for this signature in database
GPG key ID: B4EF20DDE721CAA1
3 changed files with 155 additions and 360 deletions

View file

@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
import { import {
MAX_DESCRIPTION_LENGTH,
userAvatars,
type APIError, type APIError,
type Field, type Field,
type FieldEntry, type FieldEntry,
@ -11,42 +9,25 @@
type CustomPreferences, type CustomPreferences,
type PrideFlag, type PrideFlag,
} from "$lib/api/entities"; } from "$lib/api/entities";
import FallbackImage from "$lib/components/FallbackImage.svelte";
import { userStore } from "$lib/store"; import { userStore } from "$lib/store";
import { import {
Alert,
Button, Button,
ButtonGroup, ButtonGroup,
Card,
CardBody,
CardHeader,
FormGroup, FormGroup,
InputGroup, InputGroup,
Icon, Icon,
Input, Input,
Popover,
TabContent, TabContent,
TabPane, TabPane,
InputGroupText,
} from "sveltestrap"; } from "sveltestrap";
import { encode } from "base64-arraybuffer";
import { DateTime, FixedOffsetZone } from "luxon"; import { DateTime, FixedOffsetZone } from "luxon";
import { apiFetchClient } from "$lib/api/fetch"; import { apiFetchClient } from "$lib/api/fetch";
import { PUBLIC_SHORT_BASE } from "$env/static/public"; import { PUBLIC_SHORT_BASE } from "$env/static/public";
import IconButton from "$lib/components/IconButton.svelte"; import IconButton from "$lib/components/IconButton.svelte";
import EditableField from "../EditableField.svelte";
import EditableName from "../EditableName.svelte";
import EditablePronouns from "../EditablePronouns.svelte";
import ErrorAlert from "$lib/components/ErrorAlert.svelte"; import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import { addToast, delToast } from "$lib/toast"; import { addToast, delToast } from "$lib/toast";
import type { PageData, Snapshot } from "./$types"; import type { PageData, Snapshot } from "./$types";
import { charCount, renderMarkdown } from "$lib/utils";
import MarkdownHelp from "../MarkdownHelp.svelte";
import prettyBytes from "pretty-bytes";
import CustomPreference from "./CustomPreference.svelte"; import CustomPreference from "./CustomPreference.svelte";
import FlagButton from "../FlagButton.svelte";
const MAX_AVATAR_BYTES = 1_000_000;
export let data: PageData; export let data: PageData;
@ -65,7 +46,6 @@
let timezone = data.user.timezone; let timezone = data.user.timezone;
let avatar: string | null; let avatar: string | null;
let avatar_files: FileList | null;
let newName = ""; let newName = "";
let newPronouns = ""; let newPronouns = "";
@ -103,7 +83,6 @@
custom_preferences, custom_preferences,
timezone, timezone,
); );
$: getAvatar(avatar_files).then((b64) => (avatar = b64));
const isModified = ( const isModified = (
user: MeUser, user: MeUser,
@ -193,27 +172,6 @@
.every((entry) => entry); .every((entry) => entry);
}; };
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}`;
console.log(uri.slice(0, 128));
return uri;
};
let currentTime = ""; let currentTime = "";
let displayTimezone = ""; let displayTimezone = "";
$: setTime(timezone); $: setTime(timezone);
@ -232,39 +190,6 @@
displayTimezone = zone.formatOffset(now.toUnixInteger(), "narrow"); displayTimezone = zone.formatOffset(now.toUnixInteger(), "narrow");
}; };
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) => { const moveLink = (index: number, up: boolean) => {
if (up && index == 0) return; if (up && index == 0) return;
if (!up && index == links.length - 1) return; if (!up && index == links.length - 1) return;
@ -276,52 +201,6 @@
links[newIndex] = temp; 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) => { const addLink = (event: Event) => {
event.preventDefault(); event.preventDefault();
@ -342,26 +221,11 @@
custom_preferences = custom_preferences; custom_preferences = custom_preferences;
}; };
const removeName = (index: number) => {
names.splice(index, 1);
names = [...names];
};
const removePronoun = (index: number) => {
pronouns.splice(index, 1);
pronouns = [...pronouns];
};
const removeLink = (index: number) => { const removeLink = (index: number) => {
links.splice(index, 1); links.splice(index, 1);
links = [...links]; links = [...links];
}; };
const removeField = (index: number) => {
fields.splice(index, 1);
fields = [...fields];
};
const removePreference = (id: string) => { const removePreference = (id: string) => {
delete custom_preferences[id]; delete custom_preferences[id];
custom_preferences = custom_preferences; custom_preferences = custom_preferences;
@ -511,230 +375,6 @@
{/if} {/if}
<TabContent> <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={userAvatars(data.user)} 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="Username">
<Input bind:value={data.user.name} readonly />
<p class="text-muted mt-1">
<Icon name="info-circle-fill" aria-hidden />
You can change your username in
<a href="/settings" class="text-reset">your settings</a>.
</p>
</FormGroup>
<FormGroup floating label="Display name">
<Input bind:value={display_name} />
<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>
</FormGroup>
</div>
</div>
<div>
<h4>Names</h4>
{#each names as _, index}
<EditableName
bind:value={names[index].value}
bind:status={names[index].status}
preferences={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={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.user.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={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"> <TabPane tabId="links" tab="Links">
<div class="mt-3"> <div class="mt-3">
{#each links as _, index} {#each links as _, index}

View file

@ -0,0 +1,51 @@
<script lang="ts">
import { getContext } from "svelte";
import type { Writable } from "svelte/store";
import { Alert, Button, Icon } from "sveltestrap";
import type { MeUser } from "$lib/api/entities";
import EditableField from "../../EditableField.svelte";
const user = getContext<Writable<MeUser>>("user");
const moveField = (index: number, up: boolean) => {
if (up && index == 0) return;
if (!up && index == $user.fields.length - 1) return;
const newIndex = up ? index - 1 : index + 1;
const temp = $user.fields[index];
$user.fields[index] = $user.fields[newIndex];
$user.fields[newIndex] = temp;
$user.fields = [...$user.fields];
};
const removeField = (index: number) => {
$user.fields.splice(index, 1);
$user.fields = [...$user.fields];
};
</script>
{#if $user.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 $user.fields as _, index}
<EditableField
bind:field={$user.fields[index]}
preferences={$user.custom_preferences}
deleteField={() => removeField(index)}
moveField={(up) => moveField(index, up)}
/>
{/each}
</div>
</div>
<div>
<Button on:click={() => ($user.fields = [...$user.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 { MeUser, PrideFlag } from "$lib/api/entities";
import IconButton from "$lib/components/IconButton.svelte";
import FlagButton from "../../FlagButton.svelte";
export let data: PageData;
const user = getContext<Writable<MeUser>>("user");
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) => {
$user.flags = [...$user.flags, flag];
};
const moveFlag = (index: number, up: boolean) => {
if (up && index == 0) return;
if (!up && index == $user.flags.length - 1) return;
const newIndex = up ? index - 1 : index + 1;
const temp = $user.flags[index];
$user.flags[index] = $user.flags[newIndex];
$user.flags[newIndex] = temp;
$user.flags = [...$user.flags];
};
const removeFlag = (index: number) => {
$user.flags.splice(index, 1);
$user.flags = [...$user.flags];
};
</script>
<div>
{#each $user.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={$user.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>