feat(frontend): user profile flag editor
This commit is contained in:
parent
d9d48c3cbf
commit
2a0df335bc
12 changed files with 270 additions and 13 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue