Files
Astronome/frontend/src/pages/Gallery.tsx
T
2026-04-10 00:09:42 +02:00

282 lines
9.5 KiB
TypeScript

import { useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { api } from '../api';
import type { GalleryImage } from '../api/types';
type GalleryImageWithTarget = GalleryImage & {
target_name?: string;
target_common_name?: string;
};
function fmtDate(ts: number): string {
return new Date(ts * 1000).toLocaleDateString('fr-FR', {
year: 'numeric', month: 'short', day: 'numeric',
});
}
export default function Gallery() {
const [lightbox, setLightbox] = useState<GalleryImageWithTarget | null>(null);
const [filterTarget, setFilterTarget] = useState('');
const qc = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ['gallery-all'],
queryFn: () => api.gallery.listAll(),
});
const images = data?.items ?? [];
// Unique targets for filter
const targets = Array.from(
new Map(images.map(img => [img.catalog_id, img.target_common_name ?? img.target_name ?? img.catalog_id])).entries()
).sort((a, b) => a[1].localeCompare(b[1]));
const filtered = filterTarget
? images.filter(img => img.catalog_id === filterTarget)
: images;
// Group by target
const grouped: Record<string, GalleryImageWithTarget[]> = {};
for (const img of filtered) {
const key = img.catalog_id;
if (!grouped[key]) grouped[key] = [];
grouped[key].push(img);
}
return (
<div style={{ padding: '24px 28px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 20, marginBottom: 24 }}>
<h1 style={{
fontFamily: 'var(--font-display)',
fontSize: 20,
fontWeight: 700,
color: 'var(--text-hi)',
letterSpacing: '0.04em',
}}>
Gallery
</h1>
<span style={{ color: 'var(--text-lo)', fontFamily: 'var(--font-mono)', fontSize: 12 }}>
{images.length} image{images.length !== 1 ? 's' : ''}
</span>
{targets.length > 1 && (
<select
value={filterTarget}
onChange={e => setFilterTarget(e.target.value)}
style={{
marginLeft: 'auto',
background: 'var(--bg-deep)',
border: '1px solid var(--border)',
color: 'var(--text-mid)',
fontFamily: 'var(--font-mono)',
fontSize: 11,
padding: '5px 10px',
borderRadius: 3,
}}
>
<option value="">All targets</option>
{targets.map(([id, label]) => (
<option key={id} value={id}>{label} ({id})</option>
))}
</select>
)}
</div>
{isLoading && (
<div style={{ color: 'var(--text-lo)', fontFamily: 'var(--font-mono)', fontSize: 12 }}>
Loading images
</div>
)}
{!isLoading && images.length === 0 && (
<div style={{
color: 'var(--text-lo)',
fontFamily: 'var(--font-mono)',
fontSize: 12,
padding: '40px 0',
textAlign: 'center',
}}>
No images yet. Upload images from the Targets page Log & Gallery tab.
</div>
)}
{Object.entries(grouped).map(([catalogId, imgs]) => {
const first = imgs[0];
const targetLabel = first.target_common_name ?? first.target_name ?? catalogId;
return (
<div key={catalogId} style={{ marginBottom: 32 }}>
<div style={{
display: 'flex',
alignItems: 'baseline',
gap: 10,
marginBottom: 12,
borderBottom: '1px solid var(--border)',
paddingBottom: 8,
}}>
<span style={{
fontFamily: 'var(--font-display)',
fontSize: 14,
fontWeight: 700,
color: 'var(--text-hi)',
}}>
{targetLabel}
</span>
<span style={{
fontFamily: 'var(--font-mono)',
fontSize: 11,
color: 'var(--amber)',
}}>
{catalogId}
</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-lo)', marginLeft: 'auto' }}>
{imgs.length} image{imgs.length !== 1 ? 's' : ''}
</span>
</div>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
gap: 8,
}}>
{imgs.map(img => (
<div
key={img.id}
onClick={() => setLightbox(img)}
style={{
cursor: 'pointer',
borderRadius: 4,
overflow: 'hidden',
background: 'var(--bg-deep)',
border: '1px solid var(--border)',
transition: 'border-color 0.15s',
}}
onMouseEnter={e => (e.currentTarget.style.borderColor = 'var(--border-hi)')}
onMouseLeave={e => (e.currentTarget.style.borderColor = 'var(--border)')}
>
<div style={{ aspectRatio: '4/3', overflow: 'hidden' }}>
<img
src={img.url}
alt={img.caption ?? img.filename}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
loading="lazy"
/>
</div>
<div style={{ padding: '6px 8px' }}>
{img.caption && (
<div style={{
color: 'var(--text-mid)',
fontFamily: 'var(--font-sans)',
fontSize: 11,
marginBottom: 2,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}>
{img.caption}
</div>
)}
<div style={{ color: 'var(--text-lo)', fontFamily: 'var(--font-mono)', fontSize: 10 }}>
{fmtDate(img.created_at)}
</div>
</div>
</div>
))}
</div>
</div>
);
})}
{/* Lightbox */}
{lightbox && (
<div
onClick={() => setLightbox(null)}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.94)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}
>
<div
onClick={e => e.stopPropagation()}
style={{ position: 'relative', maxWidth: '90vw', maxHeight: '90vh' }}
>
<img
src={lightbox.url}
alt={lightbox.caption ?? lightbox.filename}
style={{ maxWidth: '100%', maxHeight: '85vh', borderRadius: 4, display: 'block' }}
/>
<div style={{
marginTop: 10,
display: 'flex',
alignItems: 'center',
gap: 12,
}}>
<div>
<div style={{ color: 'var(--text-hi)', fontFamily: 'var(--font-display)', fontSize: 13, fontWeight: 600 }}>
{lightbox.target_common_name ?? lightbox.target_name ?? lightbox.catalog_id}
<span style={{ color: 'var(--amber)', fontFamily: 'var(--font-mono)', fontSize: 11, marginLeft: 8 }}>
{lightbox.catalog_id}
</span>
</div>
{lightbox.caption && (
<div style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-sans)', fontSize: 12, marginTop: 2 }}>
{lightbox.caption}
</div>
)}
<div style={{ color: 'var(--text-lo)', fontFamily: 'var(--font-mono)', fontSize: 11, marginTop: 2 }}>
{fmtDate(lightbox.created_at)}
</div>
</div>
<button
onClick={() => {
api.gallery.delete(lightbox.id).then(() => {
qc.invalidateQueries({ queryKey: ['gallery-all'] });
qc.invalidateQueries({ queryKey: ['gallery', lightbox.catalog_id] });
setLightbox(null);
});
}}
style={{
marginLeft: 'auto',
color: 'var(--danger)',
fontFamily: 'var(--font-mono)',
fontSize: 12,
border: '1px solid var(--danger)',
borderRadius: 3,
padding: '4px 10px',
}}
>
Delete
</button>
</div>
<button
onClick={() => setLightbox(null)}
style={{
position: 'absolute',
top: -12,
right: -12,
color: 'var(--text-hi)',
fontSize: 18,
background: 'var(--bg-panel)',
border: '1px solid var(--border-hi)',
borderRadius: '50%',
width: 28,
height: 28,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
}}
>
</button>
</div>
</div>
)}
</div>
);
}