Initial Commit

This commit is contained in:
2026-04-09 23:23:31 +02:00
commit a68677681f
94 changed files with 15170 additions and 0 deletions
@@ -0,0 +1,67 @@
import { useRef, useState } from 'react';
import { api } from '../../api';
import { useQueryClient } from '@tanstack/react-query';
interface Props {
catalogId: string;
}
export default function ImageUploadZone({ catalogId }: Props) {
const inputRef = useRef<HTMLInputElement>(null);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const qc = useQueryClient();
const handleFiles = async (files: FileList | null) => {
if (!files || files.length === 0) return;
setUploading(true);
setError(null);
for (const file of Array.from(files)) {
const fd = new FormData();
fd.append('file', file);
try {
await api.gallery.upload(catalogId, fd);
qc.invalidateQueries({ queryKey: ['gallery', catalogId] });
} catch (e) {
setError(`Upload failed: ${e instanceof Error ? e.message : 'Unknown error'}`);
}
}
setUploading(false);
};
return (
<div>
<div
onClick={() => inputRef.current?.click()}
onDragOver={e => e.preventDefault()}
onDrop={e => { e.preventDefault(); handleFiles(e.dataTransfer.files); }}
style={{
border: '1px dashed var(--border-hi)',
borderRadius: 4,
padding: '20px',
textAlign: 'center',
cursor: 'pointer',
color: 'var(--text-lo)',
fontFamily: 'var(--font-mono)',
fontSize: 12,
background: 'var(--bg-deep)',
transition: 'border-color 0.15s',
}}
>
{uploading ? 'Uploading...' : 'Drop images here or click to upload (JPEG, PNG, TIFF — max 50MB)'}
</div>
<input
ref={inputRef}
type="file"
accept=".jpg,.jpeg,.png,.tiff,.tif"
multiple
style={{ display: 'none' }}
onChange={e => handleFiles(e.target.files)}
/>
{error && (
<div style={{ color: 'var(--danger)', fontSize: 11, marginTop: 6 }}>{error}</div>
)}
</div>
);
}
@@ -0,0 +1,109 @@
import { useState } from 'react';
import type { GalleryImage } from '../../api/types';
import { api } from '../../api';
import { useQueryClient } from '@tanstack/react-query';
interface Props {
images: GalleryImage[];
catalogId: string;
}
export default function LightboxView({ images, catalogId }: Props) {
const [lightbox, setLightbox] = useState<GalleryImage | null>(null);
const qc = useQueryClient();
if (images.length === 0) {
return (
<div style={{ color: 'var(--text-lo)', fontFamily: 'var(--font-mono)', fontSize: 12, padding: '8px 0' }}>
No images yet.
</div>
);
}
return (
<>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
{images.map(img => (
<div
key={img.id}
onClick={() => setLightbox(img)}
style={{
cursor: 'pointer',
borderRadius: 3,
overflow: 'hidden',
background: 'var(--bg-deep)',
aspectRatio: '1',
position: 'relative',
}}
>
<img
src={img.url}
alt={img.caption ?? img.filename}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</div>
))}
</div>
{lightbox && (
<div
onClick={() => setLightbox(null)}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.92)',
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 }}
/>
<div style={{ marginTop: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{lightbox.caption && (
<span style={{ color: 'var(--text-mid)', fontSize: 12, fontFamily: 'var(--font-sans)' }}>
{lightbox.caption}
</span>
)}
<button
onClick={() => {
api.gallery.delete(lightbox.id).then(() => {
qc.invalidateQueries({ queryKey: ['gallery', catalogId] });
setLightbox(null);
});
}}
style={{ color: 'var(--danger)', fontSize: 12, marginLeft: 'auto' }}
>
Delete
</button>
</div>
<button
onClick={() => setLightbox(null)}
style={{
position: 'absolute',
top: -12,
right: -12,
color: 'var(--text-hi)',
fontSize: 20,
background: 'var(--bg-panel)',
borderRadius: '50%',
width: 28,
height: 28,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
</button>
</div>
</div>
)}
</>
);
}