Files
ds-galo/frontend/src/lib/components/Comments.svelte
T
iu 04617d1cdb feat(profile): add round avatar with upload and cropping
- 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
2026-10-12 16:16:47 +03:00

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>