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>
73 lines
1.9 KiB
React
73 lines
1.9 KiB
React
import { useEffect } from 'react'
|
||
import { imgUrl } from '../api'
|
||
|
||
export default function Lightbox({ photos, index, onClose, onNav, footer }) {
|
||
const photo = photos[index]
|
||
|
||
useEffect(() => {
|
||
const onKey = (e) => {
|
||
if (e.key === 'Escape') onClose()
|
||
if (e.key === 'ArrowRight' && index < photos.length - 1) onNav(index + 1)
|
||
if (e.key === 'ArrowLeft' && index > 0) onNav(index - 1)
|
||
}
|
||
window.addEventListener('keydown', onKey)
|
||
return () => window.removeEventListener('keydown', onKey)
|
||
}, [index, photos.length, onClose, onNav])
|
||
|
||
useEffect(() => {
|
||
document.body.style.overflow = 'hidden'
|
||
return () => {
|
||
document.body.style.overflow = ''
|
||
}
|
||
}, [])
|
||
|
||
if (!photo) return null
|
||
|
||
return (
|
||
<div className="lightbox" onClick={onClose}>
|
||
<div className="lb-top" onClick={(e) => e.stopPropagation()}>
|
||
<span className="lb-name">{photo.filename}</span>
|
||
<span className="lb-count">
|
||
{index + 1} / {photos.length}
|
||
</span>
|
||
<button className="lb-btn" onClick={onClose} title="Close (Esc)">
|
||
✕
|
||
</button>
|
||
</div>
|
||
<button
|
||
className="lb-nav lb-prev"
|
||
disabled={index === 0}
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
onNav(index - 1)
|
||
}}
|
||
>
|
||
‹
|
||
</button>
|
||
<div className="lb-stage">
|
||
<img
|
||
className="lb-img"
|
||
src={imgUrl(photo, 'preview')}
|
||
alt={photo.filename}
|
||
onClick={(e) => e.stopPropagation()}
|
||
/>
|
||
</div>
|
||
<button
|
||
className="lb-nav lb-next"
|
||
disabled={index === photos.length - 1}
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
onNav(index + 1)
|
||
}}
|
||
>
|
||
›
|
||
</button>
|
||
{footer && (
|
||
<div className="lb-footer" onClick={(e) => e.stopPropagation()}>
|
||
{footer(photo)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|