Initial Commit
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user