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
+53
View File
@@ -0,0 +1,53 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import '../app.css';
import ThemeSwitcher from '$lib/components/ThemeSwitcher.svelte';
import { auth } from '$lib/stores/auth.svelte';
import { theme } from '$lib/stores/theme.svelte';
import { authApi } from '$lib/api';
let { children } = $props();
onMount(() => {
theme.init();
if (auth.isAuthenticated && !auth.user) {
authApi
.me()
.then((data) => auth.setUser(data.user))
.catch(() => {});
}
});
async function logout() {
if (auth.refresh) {
await authApi.logout(auth.refresh).catch(() => {});
}
auth.clear();
goto('/login');
}
</script>
<div class="bg-bg text-text min-h-screen">
<header class="border-border bg-bg-elevated border-b">
<div class="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-3">
<a href="/" class="text-lg font-semibold">📷 Galo</a>
<div class="flex items-center gap-3">
{#if auth.isAuthenticated}
<span class="text-text-muted hidden text-sm sm:inline">{auth.user?.name ?? ''}</span>
<button
type="button"
class="text-text-muted hover:text-text text-sm"
onclick={logout}>Выйти</button
>
{:else}
<a href="/login" class="text-text-muted hover:text-text text-sm">Войти</a>
{/if}
<ThemeSwitcher />
</div>
</div>
</header>
<main class="mx-auto max-w-6xl px-4 py-6">
{@render children()}
</main>
</div>