abbashkyt-creator 7d8ce0e322 V0.1
2026-03-14 04:02:22 +03:00

194 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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}
</>
)
}