194 lines
7.4 KiB
TypeScript
194 lines
7.4 KiB
TypeScript
'use client'
|
||
import { useState, useEffect } from 'react'
|
||
import { createPortal } from 'react-dom'
|
||
import Image from 'next/image'
|
||
|
||
export default function ImageGallery({ images }: { images: string[] }) {
|
||
// activeIdx = which thumbnail is highlighted in the strip
|
||
const [activeIdx, setActiveIdx] = useState(0)
|
||
// lightboxIdx = null means closed; a number = open at that image
|
||
const [lightboxIdx, setLightboxIdx] = useState<number | null>(null)
|
||
// portal guard: document.body only available client-side
|
||
const [mounted, setMounted] = useState(false)
|
||
|
||
useEffect(() => { setMounted(true) }, [])
|
||
|
||
// Keyboard navigation for lightbox (← → Esc)
|
||
useEffect(() => {
|
||
if (lightboxIdx === null) return
|
||
const handler = (e: KeyboardEvent) => {
|
||
if (e.key === 'ArrowLeft') lbGo((lightboxIdx - 1 + images.length) % images.length)
|
||
if (e.key === 'ArrowRight') lbGo((lightboxIdx + 1) % images.length)
|
||
if (e.key === 'Escape') setLightboxIdx(null)
|
||
}
|
||
window.addEventListener('keydown', handler)
|
||
return () => window.removeEventListener('keydown', handler)
|
||
}, [lightboxIdx, images.length]) // eslint-disable-line react-hooks/exhaustive-deps
|
||
|
||
if (!images.length) return null
|
||
|
||
// Navigate lightbox AND keep strip highlight in sync
|
||
const lbGo = (idx: number) => { setLightboxIdx(idx); setActiveIdx(idx) }
|
||
|
||
// Strip-only prev/next (highlight moves, lightbox stays closed)
|
||
const stripPrev = (e: React.MouseEvent) => {
|
||
e.stopPropagation()
|
||
setActiveIdx(i => (i - 1 + images.length) % images.length)
|
||
}
|
||
const stripNext = (e: React.MouseEvent) => {
|
||
e.stopPropagation()
|
||
setActiveIdx(i => (i + 1) % images.length)
|
||
}
|
||
|
||
// Lightbox arrow handlers
|
||
const lbPrev = (e: React.MouseEvent) => {
|
||
e.stopPropagation()
|
||
if (lightboxIdx !== null) lbGo((lightboxIdx - 1 + images.length) % images.length)
|
||
}
|
||
const lbNext = (e: React.MouseEvent) => {
|
||
e.stopPropagation()
|
||
if (lightboxIdx !== null) lbGo((lightboxIdx + 1) % images.length)
|
||
}
|
||
|
||
// ── Lightbox portal ──────────────────────────────────────────────────────
|
||
// Rendered via createPortal so it escapes the panel's z-50 stacking context.
|
||
// Sized & positioned identically to the detail panel (w-96 h-full fixed right-0 top-0).
|
||
const lightbox = lightboxIdx !== null && mounted
|
||
? createPortal(
|
||
// Semi-transparent backdrop — clicking it closes the lightbox
|
||
<div
|
||
className="fixed inset-0 z-[100] flex items-stretch justify-end"
|
||
style={{ background: 'rgba(0,0,0,0.5)' }}
|
||
onClick={() => setLightboxIdx(null)}
|
||
>
|
||
{/* Inner panel — same w-96 as the detail panel */}
|
||
<div
|
||
className="h-full w-96 border-l border-ghost-accent flex flex-col"
|
||
style={{ background: 'var(--color-ghost-bg)' }}
|
||
onClick={e => e.stopPropagation()}
|
||
>
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between px-4 py-3 border-b border-ghost-border shrink-0">
|
||
<span className="font-mono text-ghost-dim text-xs tracking-widest">
|
||
IMAGE {lightboxIdx + 1} / {images.length}
|
||
</span>
|
||
<button
|
||
onClick={() => setLightboxIdx(null)}
|
||
className="text-ghost-dim hover:text-ghost-danger font-mono text-xs transition-colors"
|
||
>
|
||
✕ CLOSE
|
||
</button>
|
||
</div>
|
||
|
||
{/* Full-size image */}
|
||
<div className="flex-1 flex items-center justify-center p-4 overflow-hidden">
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img
|
||
src={images[lightboxIdx]}
|
||
alt={`Lot image ${lightboxIdx + 1}`}
|
||
className="max-w-full max-h-full object-contain rounded"
|
||
/>
|
||
</div>
|
||
|
||
{/* Footer — arrows + dot indicators */}
|
||
{images.length > 1 && (
|
||
<div className="flex items-center justify-between px-4 py-3 border-t border-ghost-border shrink-0">
|
||
<button
|
||
onClick={lbPrev}
|
||
className="font-mono text-ghost-accent hover:text-ghost-text text-sm transition-colors px-2 py-1"
|
||
>
|
||
‹ PREV
|
||
</button>
|
||
|
||
{/* Dot indicators — click to jump */}
|
||
<div className="flex gap-1.5 items-center">
|
||
{images.map((_, i) => (
|
||
<button
|
||
key={i}
|
||
onClick={() => lbGo(i)}
|
||
className={`rounded-full transition-all ${
|
||
i === lightboxIdx
|
||
? 'w-3 h-3 bg-ghost-accent'
|
||
: 'w-2 h-2 bg-ghost-border hover:bg-ghost-dim'
|
||
}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
<button
|
||
onClick={lbNext}
|
||
className="font-mono text-ghost-accent hover:text-ghost-text text-sm transition-colors px-2 py-1"
|
||
>
|
||
NEXT ›
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>,
|
||
document.body
|
||
)
|
||
: null
|
||
|
||
// ── Thumbnail strip ──────────────────────────────────────────────────────
|
||
return (
|
||
<>
|
||
<div>
|
||
<div className="text-ghost-dim text-xs font-mono mb-2">LOT IMAGES</div>
|
||
|
||
{/* Strip: ‹ [thumbnails] › */}
|
||
<div className="flex items-center gap-1">
|
||
{images.length > 1 && (
|
||
<button
|
||
onClick={stripPrev}
|
||
title="Previous image"
|
||
className="text-ghost-dim hover:text-ghost-accent font-mono text-xl leading-none px-0.5 shrink-0 transition-colors"
|
||
>
|
||
‹
|
||
</button>
|
||
)}
|
||
|
||
<div className="flex gap-2 overflow-x-auto flex-1 pb-1">
|
||
{images.map((src, i) => (
|
||
<button
|
||
key={i}
|
||
onClick={() => { setActiveIdx(i); setLightboxIdx(i) }}
|
||
title={`Open image ${i + 1}`}
|
||
className={`shrink-0 rounded transition-all border-2 ${
|
||
i === activeIdx
|
||
? 'border-ghost-accent scale-105'
|
||
: 'border-ghost-border hover:border-ghost-dim'
|
||
}`}
|
||
>
|
||
<Image
|
||
src={src}
|
||
alt={`Lot image ${i + 1}`}
|
||
width={100}
|
||
height={80}
|
||
className="object-cover rounded"
|
||
onError={e => {
|
||
const parent = e.currentTarget.parentElement
|
||
if (parent) parent.style.display = 'none'
|
||
}}
|
||
/>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{images.length > 1 && (
|
||
<button
|
||
onClick={stripNext}
|
||
title="Next image"
|
||
className="text-ghost-dim hover:text-ghost-accent font-mono text-xl leading-none px-0.5 shrink-0 transition-colors"
|
||
>
|
||
›
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Lightbox rendered at document.body via portal */}
|
||
{lightbox}
|
||
</>
|
||
)
|
||
}
|