forked from mirrors/pronouns.cc
add fields and flags to new edit page
This commit is contained in:
parent
61f1464e37
commit
575aa01fa5
3 changed files with 155 additions and 360 deletions
|
@ -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}
|
||||||
|
|
51
frontend/src/routes/edit/profile/fields/+page.svelte
Normal file
51
frontend/src/routes/edit/profile/fields/+page.svelte
Normal 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>
|
104
frontend/src/routes/edit/profile/flags/+page.svelte
Normal file
104
frontend/src/routes/edit/profile/flags/+page.svelte
Normal 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>
|
Loading…
Reference in a new issue