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>
53 lines
1.3 KiB
React
53 lines
1.3 KiB
React
export function fmtBytes(bytes) {
|
|
if (!bytes) return '0 B'
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
const i = Math.min(Math.floor(Math.log2(bytes) / 10), units.length - 1)
|
|
const value = bytes / 2 ** (10 * i)
|
|
return `${value >= 100 || i === 0 ? Math.round(value) : value.toFixed(1)} ${units[i]}`
|
|
}
|
|
|
|
export default function SelectionBar({
|
|
count,
|
|
total,
|
|
selectedBytes,
|
|
totalBytes,
|
|
onSelectAll,
|
|
onClear,
|
|
onDownload,
|
|
onDownloadAll,
|
|
}) {
|
|
if (total === 0) return null
|
|
|
|
if (count === 0) {
|
|
return (
|
|
<div className="select-bar">
|
|
<span className="select-count">
|
|
{total} photo{total === 1 ? '' : 's'} · {fmtBytes(totalBytes)}
|
|
</span>
|
|
<button className="btn btn-primary" onClick={onDownloadAll}>
|
|
Download all as ZIP
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="select-bar">
|
|
<span className="select-count">
|
|
{count} of {total} selected
|
|
</span>
|
|
{count < total && (
|
|
<button className="btn" onClick={onSelectAll}>
|
|
Select all
|
|
</button>
|
|
)}
|
|
<button className="btn" onClick={onClear}>
|
|
Clear
|
|
</button>
|
|
<button className="btn btn-primary" onClick={onDownload}>
|
|
Download {count} as ZIP ({fmtBytes(selectedBytes)})
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|