- backend: user_avatar_objects binding (PG+SQLite), GC preserves avatar blobs, avatar service (EXIF-stripping WebP 128/512), public avatar read route - web: avatar upload/crop on profile, round avatar in header/members/comments/author - android: avatar upload/crop, round avatar on profile and albums header - openspec: add-user-avatar proposal/design/specs/tasks
101 lines
2.6 KiB
Svelte
101 lines
2.6 KiB
Svelte
<script lang="ts">
|
|
import { api } from '$lib/api';
|
|
import Avatar from './Avatar.svelte';
|
|
import Icon from './Icon.svelte';
|
|
import { auth } from '$lib/stores/auth.svelte';
|
|
import type { Comment } from '$lib/types';
|
|
|
|
interface Props {
|
|
photoId: string;
|
|
}
|
|
|
|
let { photoId }: Props = $props();
|
|
|
|
let comments = $state<Comment[]>([]);
|
|
let text = $state('');
|
|
let error = $state('');
|
|
|
|
async function load() {
|
|
try {
|
|
const data = await api<{ comments: Comment[] }>(`/photos/${photoId}/comments`);
|
|
comments = data.comments;
|
|
} catch (e) {
|
|
error = (e as Error).message;
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
photoId;
|
|
load();
|
|
});
|
|
|
|
async function submit(ev: Event) {
|
|
ev.preventDefault();
|
|
if (!text.trim()) return;
|
|
try {
|
|
const data = await api<{ comment: Comment }>(`/photos/${photoId}/comments`, {
|
|
method: 'POST',
|
|
body: { text }
|
|
});
|
|
comments = [...comments, data.comment];
|
|
text = '';
|
|
} catch (e) {
|
|
error = (e as Error).message;
|
|
}
|
|
}
|
|
|
|
async function remove(id: string) {
|
|
try {
|
|
await api(`/comments/${id}`, { method: 'DELETE' });
|
|
comments = comments.filter((c) => c.id !== id);
|
|
} catch (e) {
|
|
error = (e as Error).message;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="mt-2">
|
|
{#each comments as comment (comment.id)}
|
|
<div class="border-border mb-1 flex items-start justify-between gap-2 border-b py-1 text-sm">
|
|
<span class="text-text inline-flex items-start gap-2">
|
|
<Avatar name={comment.user.name} url={comment.user.avatar_url} size="size-6" />
|
|
<span><strong>{comment.user.name}</strong>: {comment.text}</span>
|
|
</span>
|
|
{#if auth.user?.id === comment.user_id}
|
|
<button
|
|
type="button"
|
|
class="text-text-subtle hover:text-danger inline-flex shrink-0 items-center gap-0.5 text-xs"
|
|
aria-label="Удалить комментарий"
|
|
title="Удалить комментарий"
|
|
onclick={() => remove(comment.id)}
|
|
>
|
|
<Icon name="delete" class="size-3.5" />
|
|
удалить
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
|
|
{#if auth.isAuthenticated}
|
|
<form class="relative mt-2" onsubmit={submit}>
|
|
<input
|
|
bind:value={text}
|
|
placeholder="Комментарий…"
|
|
class="border-border bg-bg text-text w-full rounded-lg border py-1.5 pr-10 pl-3 text-sm"
|
|
/>
|
|
<button
|
|
class="text-primary hover:bg-bg-muted absolute top-1/2 right-1 inline-flex -translate-y-1/2 items-center justify-center rounded-md p-1.5 disabled:opacity-50"
|
|
type="submit"
|
|
aria-label="Отправить"
|
|
title="Отправить"
|
|
disabled={!text.trim()}
|
|
>
|
|
<Icon name="send" class="size-4" />
|
|
</button>
|
|
</form>
|
|
{/if}
|
|
{#if error}
|
|
<p class="text-danger mt-1 text-xs">{error}</p>
|
|
{/if}
|
|
</div>
|