feat: implement shared-album multi-user photo album

Backend (Go/Gin/pgx): env config, pgx pool with retries, Garage S3 client
(path-style, region garage), embedded golang-migrate migrations, JWT auth with
refresh rotation, albums CRUD, members/roles/invites with no-escalation matrix,
photo upload + async imaging/goexif WebP variants, cursor pagination, presigned
URLs, likes/comments, SSE hub (album + personal), notifications, share links.

Frontend (SvelteKit 2 + Svelte 5 + Tailwind v4): API wrapper with 401 refresh,
auth store, all screens, Uploader/PhotoGrid/Lightbox/Comments, SSE client with
reconnect + REST catch-up, three themes + system via data-theme.

Deploy without Docker: Makefile builds the frontend into backend/static and
embeds it (go:embed) into a single statically linked binary; systemd unit and
nginx SSE example included.

Use gen2brain/webp (-tags nodynamic) instead of cgo kolesa-team/go-webp to keep
CGO_ENABLED=0 static builds; design.md/proposal.md updated accordingly.
This commit is contained in:
iu
2026-10-10 12:50:33 +03:00
commit 2b2b342492
105 changed files with 12494 additions and 0 deletions
@@ -0,0 +1,45 @@
<script lang="ts">
import type { Photo } from '$lib/types';
interface Props {
photos: Photo[];
onOpen?: (index: number) => void;
}
let { photos, onOpen }: Props = $props();
</script>
{#if photos.length === 0}
<p class="text-text-muted p-8 text-center">Пока нет фотографий.</p>
{:else}
<ul class="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
{#each photos as photo, i (photo.id)}
<li>
<button
type="button"
class="border-border bg-bg-muted group relative block aspect-square w-full overflow-hidden rounded-lg border"
onclick={() => onOpen?.(i)}
>
{#if photo.thumb_url}
<img
src={photo.thumb_url}
alt={photo.original_filename}
loading="lazy"
decoding="async"
class="h-full w-full object-cover transition-transform duration-200 group-hover:scale-105"
/>
{:else}
<span class="text-text-subtle flex h-full w-full items-center justify-center text-xs">
обработка…
</span>
{/if}
<span
class="bg-bg/70 text-text absolute right-1 bottom-1 rounded px-1.5 py-0.5 text-xs"
>
♥ {photo.likes_count} · 💬 {photo.comments_count}
</span>
</button>
</li>
{/each}
</ul>
{/if}