feat(frontend): user profile flag editor

This commit is contained in:
sam 2024-12-09 16:33:06 +01:00
parent d9d48c3cbf
commit 2a0df335bc
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
12 changed files with 270 additions and 13 deletions

View file

@ -10,11 +10,18 @@
type?: "submit" | "reset" | "button";
id?: string;
onclick?: MouseEventHandler<HTMLButtonElement>;
outline?: boolean;
};
let { icon, tooltip, color = "primary", type, id, onclick }: Props = $props();
let { icon, tooltip, color = "primary", type, id, onclick, outline }: Props = $props();
</script>
<button {id} {type} use:tippy={{ content: tooltip }} class="btn btn-{color}" {onclick}>
<button
{id}
{type}
use:tippy={{ content: tooltip }}
class="btn {outline ? `btn-outline-${color}` : `btn-${color}`}"
{onclick}
>
<Icon name={icon} />
<span class="visually-hidden">{tooltip}</span>
</button>

View file

@ -0,0 +1,35 @@
<script lang="ts">
import type { PrideFlag } from "$api/models";
import type { MouseEventHandler } from "svelte/elements";
import EditorFlagImage from "./EditorFlagImage.svelte";
import tippy from "$lib/tippy";
type Props = {
flag: PrideFlag;
tooltip?: string;
class?: string;
onclick: MouseEventHandler<HTMLButtonElement>;
padding?: boolean;
};
let { flag, tooltip, class: className, onclick, padding }: Props = $props();
let tip = $derived(tooltip ? tippy : () => {});
</script>
<button
type="button"
class="btn btn-outline-secondary {className || ''}"
class:padding
{onclick}
use:tip={{ content: tooltip }}
>
<EditorFlagImage {flag} />
{flag.name}
</button>
<style>
.padding {
margin-right: 5px;
margin-bottom: 5px;
}
</style>

View file

@ -24,11 +24,16 @@
<img class="flag" src={flag.image_url ?? DEFAULT_FLAG} alt={flag.description ?? flag.name} />
</span>
<div class="w-lg-50">
<input class="mb-2 form-control" placeholder="Name" bind:value={name} autocomplete="off" />
<input
class="mb-2 form-control"
placeholder={$t("settings.flag-name-placeholder")}
bind:value={name}
autocomplete="off"
/>
<textarea
class="mb-2 form-control"
style="height: 5rem;"
placeholder="Description"
placeholder={$t("settings.flag-description-placeholder")}
bind:value={description}
autocomplete="off"
></textarea>

View file

@ -0,0 +1,48 @@
<script lang="ts">
import type { PrideFlag } from "$api/models";
import InfoCircleFill from "svelte-bootstrap-icons/lib/InfoCircleFill.svelte";
import Search from "svelte-bootstrap-icons/lib/Search.svelte";
import FlagButton from "./FlagButton.svelte";
import { t } from "$lib/i18n";
type Props = { flags: PrideFlag[]; select(flag: PrideFlag): void };
let { flags, select }: Props = $props();
let query = $state("");
let filteredFlags = $derived(search(query));
function search(q: string) {
if (!q) return flags.slice(0, 20);
return flags.filter((f) => f.name.toLowerCase().indexOf(q.toLowerCase()) !== -1).slice(0, 20);
}
</script>
<input class="form-control" placeholder={$t("editor.flag-search-placeholder")} bind:value={query} />
<div class="mt-3">
{#each filteredFlags as flag (flag.id)}
<FlagButton {flag} onclick={() => select(flag)} padding />
{:else}
<div class="text-secondary text-center">
<p>
<Search class="no-flags-icon" height={64} width={64} aria-hidden />
</p>
<p>
{#if query}
{$t("editor.flag-search-no-flags")}
{:else}
{$t("editor.flag-search-no-account-flags")}
{/if}
</p>
</div>
{/each}
{#if flags.length > 0}
<p class="text-secondary mt-2">
<InfoCircleFill aria-hidden />
{$t("editor.flag-search-hint")}
<a href="/settings/flags">{$t("editor.flag-manage-your-flags")}</a>
</p>
{:else}
<p><a href="/settings/flags">{$t("editor.flag-manage-your-flags")}</a></p>
{/if}
</div>

View file

@ -0,0 +1,95 @@
<script lang="ts">
import type { RawApiError } from "$api/error";
import type { PrideFlag } from "$api/models";
import FlagSearch from "$components/editor/FlagSearch.svelte";
import IconButton from "$components/IconButton.svelte";
import { t } from "$lib/i18n";
import FlagButton from "./FlagButton.svelte";
import FormStatusMarker from "./FormStatusMarker.svelte";
type Props = {
profileFlags: PrideFlag[];
allFlags: PrideFlag[];
save(flags: string[]): Promise<void>;
form: { ok: boolean; error: RawApiError | null } | null;
};
let { profileFlags, allFlags, save, form }: Props = $props();
let flags = $state(profileFlags);
const select = (flag: PrideFlag) => {
flags = [...flags, flag];
};
const removeFlag = (flag: PrideFlag) => {
const idx = flags.indexOf(flag);
if (idx === -1) return;
flags.splice(idx, 1);
flags = [...flags];
};
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;
flags = [...flags];
};
const saveChanges = () => save(flags.map((f) => f.id));
</script>
<div class="row">
<div class="col-md">
<h4>
{$t("settings.flag-title")}
<button type="button" class="btn btn-primary" onclick={() => saveChanges()}>
{$t("save-changes")}
</button>
</h4>
<FormStatusMarker {form} />
{#each flags as flag, i}
<div class="d-block">
<div class="btn-group flag-group">
<IconButton
icon="chevron-up"
tooltip={$t("editor.move-flag-up")}
color="secondary"
outline
onclick={() => moveFlag(i, true)}
/>
<IconButton
icon="chevron-down"
tooltip={$t("editor.move-flag-down")}
color="secondary"
outline
onclick={() => moveFlag(i, false)}
/>
<FlagButton
{flag}
onclick={() => removeFlag(flag)}
tooltip={$t("editor.remove-this-flag")}
/>
</div>
</div>
{:else}
<p class="text-secondary">
{$t("editor.no-flags-hint")}
</p>
{/each}
</div>
<div class="col-md">
<h4>{$t("editor.add-flags-header")}</h4>
<FlagSearch flags={allFlags} {select} />
</div>
</div>
<style>
.flag-group {
margin-right: 5px;
margin-bottom: 5px;
}
</style>