Initial release: self-hosted client photo gallery
ci / docker (push) Successful in 13s

Rust (axum + sqlx) API and worker sharing a Postgres-backed job queue
(SKIP LOCKED, heartbeat, reaper, typed statuses), S3 storage with derived
keys and a fully private bucket, OIDC photographer login with per-request
allowlist checks, client share links with argon2 passwords and lockout,
cookie-based image authorization with sliding expiry, hand-rolled
spec-compliant streaming ZIP downloads with exact Content-Length,
React + Vite gallery frontend, single Docker image, Helm chart for
external S3 + Postgres, and Gitea CI.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-17 13:12:42 +02:00
co-authored by Claude
commit 16d2a56a78
55 changed files with 11962 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
import { imgUrl } from '../api'
// Justified gallery: rows are built with flexbox, each tile's flex-grow is
// proportional to its aspect ratio so rows fill the container edge to edge.
// When `selected`/`onToggleSelect` are provided, tiles get a select checkmark;
// selection state lives in the parent so it survives lightbox open/close.
export default function Gallery({ photos, onOpen, overlay, selected, onToggleSelect }) {
if (photos.length === 0) return null
const selecting = selected && selected.size > 0
return (
<div className={`gallery${selecting ? ' selecting' : ''}`}>
{photos.map((p, i) => {
const ar = p.width && p.height ? p.width / p.height : 1.5
const isSelected = selected ? selected.has(p.id) : false
return (
<div
key={p.id}
className={`g-item${isSelected ? ' selected' : ''}`}
style={{ '--ar': ar }}
onClick={() => onOpen && onOpen(i)}
>
<img src={imgUrl(p, 'thumb')} loading="lazy" alt={p.filename} />
{onToggleSelect && (
<button
className="g-check"
title={isSelected ? 'Deselect' : 'Select'}
onClick={(e) => {
e.stopPropagation()
onToggleSelect(p.id)
}}
>
</button>
)}
{overlay && overlay(p)}
</div>
)
})}
<div className="g-spacer" />
</div>
)
}