Files
ds-galo/frontend/src/routes/albums/[id]/settings/+page.svelte
T
iu 997ec5e6c1 feat(frontend): replace emoji with inline Material Symbols icon system
Add a self-contained icon system: vendored Material Symbols registry, an
<Icon> component that references a single <symbol> sprite via <use>, plus
IconButton/IconSprite. Replace every emoji/Unicode glyph (logo, theme
switcher, cover placeholder, like/comment counts, lightbox controls, back
links) and add leading icons to key controls. Decorative icons are
aria-hidden; icon-only buttons get labels; theme toggles expose
aria-pressed; counters get accessible names.

Recorded in openspec change material-icons-ui (all tasks complete).
2026-10-10 13:19:29 +03:00

169 lines
4.7 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { api } from '$lib/api';
import Icon from '$lib/components/Icon.svelte';
import type { Album, ShareLink } from '$lib/types';
const albumId = $derived($page.params.id);
let album = $state<Album | null>(null);
let shareLinks = $state<ShareLink[]>([]);
let title = $state('');
let description = $state('');
let expiresIn = $state('168h');
let error = $state('');
let saved = $state(false);
async function load() {
try {
const [albumData, linksData] = await Promise.all([
api<{ album: Album }>(`/albums/${albumId}`),
api<{ share_links: ShareLink[] }>(`/albums/${albumId}/share`)
]);
album = albumData.album;
title = album.title;
description = album.description;
shareLinks = linksData.share_links;
} catch (e) {
error = (e as Error).message;
}
}
$effect(() => {
load();
});
async function save(ev: Event) {
ev.preventDefault();
error = '';
saved = false;
try {
const data = await api<{ album: Album }>(`/albums/${albumId}`, {
method: 'PATCH',
body: { title, description }
});
album = data.album;
saved = true;
} catch (e) {
error = (e as Error).message;
}
}
async function createLink(ev: Event) {
ev.preventDefault();
try {
await api(`/albums/${albumId}/share`, { method: 'POST', body: { expires_in: expiresIn } });
await load();
} catch (e) {
error = (e as Error).message;
}
}
async function revoke(id: string) {
try {
await api(`/albums/${albumId}/share/${id}`, { method: 'DELETE' });
shareLinks = shareLinks.filter((l) => l.id !== id);
} catch (e) {
error = (e as Error).message;
}
}
async function removeAlbum() {
if (!confirm('Удалить альбом вместе с фото?')) return;
try {
await api(`/albums/${albumId}`, { method: 'DELETE' });
goto('/');
} catch (e) {
error = (e as Error).message;
}
}
</script>
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold">Настройки</h1>
<a href="/albums/{albumId}" class="text-primary inline-flex items-center gap-1 text-sm">
<Icon name="arrow_back" class="size-4" />
К альбому
</a>
</div>
{#if error}<p class="text-danger mb-4 text-sm">{error}</p>{/if}
<form class="border-border bg-bg-elevated mb-6 space-y-3 rounded-xl border p-4" onsubmit={save}>
<h2 class="font-semibold">Альбом</h2>
<label class="block">
<span class="text-text-muted mb-1 block text-sm">Название</span>
<input
bind:value={title}
class="border-border bg-bg text-text w-full rounded-lg border px-3 py-2"
/>
</label>
<label class="block">
<span class="text-text-muted mb-1 block text-sm">Описание</span>
<textarea
bind:value={description}
rows="3"
class="border-border bg-bg text-text w-full rounded-lg border px-3 py-2"
></textarea>
</label>
<button
class="bg-primary text-primary-fg inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm"
type="submit"
>
<Icon name="save" class="size-4" />
Сохранить
</button>
{#if saved}<span class="text-success ml-3 text-sm">Сохранено</span>{/if}
</form>
<div class="border-border bg-bg-elevated mb-6 space-y-3 rounded-xl border p-4">
<h2 class="font-semibold">Ссылки для просмотра</h2>
<form class="flex items-end gap-2" onsubmit={createLink}>
<label class="text-sm">
<span class="text-text-muted mb-1 block">Срок действия</span>
<input
bind:value={expiresIn}
class="border-border bg-bg text-text w-32 rounded-lg border px-2 py-1.5"
/>
</label>
<button
class="bg-primary text-primary-fg inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm"
type="submit"
>
<Icon name="add_link" class="size-4" />
Создать ссылку
</button>
</form>
<ul class="divide-border divide-y">
{#each shareLinks as link (link.id)}
<li class="flex items-center justify-between gap-2 py-2 text-sm">
<span class="text-text-muted truncate">
{link.revoked_at ? 'отозвана' : 'до ' + new Date(link.expires_at).toLocaleString()}
· <a class="text-primary" href="/share/{link.token}">/share/{link.token}</a>
</span>
{#if !link.revoked_at}
<button
type="button"
class="text-danger inline-flex shrink-0 items-center gap-1"
onclick={() => revoke(link.id)}
>
<Icon name="link_off" class="size-4" />
Отозвать
</button>
{/if}
</li>
{/each}
</ul>
</div>
<button
type="button"
class="border-danger text-danger inline-flex items-center gap-1.5 rounded-lg border px-4 py-2 text-sm"
onclick={removeAlbum}
>
<Icon name="delete_forever" class="size-4" />
Удалить альбом
</button>