You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

185 lines
5.6 KiB

import { randomString } from '@/lib/random'
import { cn } from '@/lib/utils'
import logger from '@/lib/logger'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import modalManager from '@/services/modal-manager.service'
import { TImetaInfo } from '@/types'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import { preferBlossomPrimalDisplayUrl } from '@/lib/url'
import { useTranslation } from 'react-i18next'
import Lightbox from 'yet-another-react-lightbox'
import Captions from 'yet-another-react-lightbox/plugins/captions'
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
import 'yet-another-react-lightbox/plugins/captions.css'
import Image from '../Image'
export default function ImageWithLightbox({
image,
className,
classNames = {}
}: {
image: TImetaInfo
className?: string
classNames?: {
wrapper?: string
}
}) {
const id = useMemo(() => `image-with-lightbox-${randomString()}`, [])
const { t } = useTranslation()
const { autoLoadMedia } = useContentPolicy()
const [display, setDisplay] = useState(autoLoadMedia)
const [index, setIndex] = useState(-1)
useEffect(() => {
setDisplay(autoLoadMedia)
if (!autoLoadMedia) {
setIndex(-1)
}
}, [autoLoadMedia])
const logLightboxEvent = useCallback((stage: string, details?: Record<string, unknown>) => {
logger.info('[LightboxTrace]', {
stage,
id,
imageUrl: image.url,
index,
pathname: window.location.pathname,
search: window.location.search,
hash: window.location.hash,
...details
})
}, [id, image.url, index])
useEffect(() => {
if (index >= 0) {
logLightboxEvent('modal-register')
modalManager.register(id, () => {
logLightboxEvent('modal-callback-close')
setIndex(-1)
})
} else {
logLightboxEvent('modal-unregister')
modalManager.unregister(id)
}
}, [id, index, logLightboxEvent])
useEffect(() => {
if (index < 0) return
const onCaptureKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
logLightboxEvent('escape-keydown-capture', {
defaultPrevented: event.defaultPrevented,
eventPhase: event.eventPhase
})
}
}
const onPopState = (event: PopStateEvent) => {
logLightboxEvent('window-popstate-while-open', {
hasState: !!event.state,
state: event.state
})
}
window.addEventListener('keydown', onCaptureKeydown, true)
window.addEventListener('popstate', onPopState)
return () => {
window.removeEventListener('keydown', onCaptureKeydown, true)
window.removeEventListener('popstate', onPopState)
}
}, [index, logLightboxEvent])
const handlePhotoClick = (event: React.MouseEvent) => {
logLightboxEvent('thumbnail-click', {
defaultPreventedBefore: event.defaultPrevented
})
event.stopPropagation()
event.preventDefault()
logLightboxEvent('set-open-index')
setIndex(0)
}
// The portal is always mounted (not conditional on `index >= 0`) so that React
// never removes it while yet-another-react-lightbox is mid-cleanup, which would
// otherwise cause "Node.removeChild: The node to be removed is not a child of
// this node". Visibility is controlled via the `open` prop instead.
return (
<div className="max-w-[400px]">
{display ? (
<Image
key={0}
className={className}
classNames={{
wrapper: cn('rounded-lg cursor-zoom-in', classNames.wrapper),
errorPlaceholder: 'aspect-square h-[30vh]'
}}
image={image}
onClick={(e) => handlePhotoClick(e)}
/>
) : (
<span
className="text-primary hover:underline truncate w-fit cursor-pointer inline-block"
onClick={(e) => {
e.stopPropagation()
setDisplay(true)
}}
>
[{t('Click to load image')}]
</span>
)}
{createPortal(
<div
data-lightbox-overlay
onClick={(e) => {
logLightboxEvent('overlay-click', { target: (e.target as HTMLElement)?.tagName })
e.stopPropagation()
}}
onPointerDown={(e) => {
logLightboxEvent('overlay-pointerdown', { target: (e.target as HTMLElement)?.tagName })
e.stopPropagation()
}}
onMouseDown={(e) => {
logLightboxEvent('overlay-mousedown', { target: (e.target as HTMLElement)?.tagName })
e.stopPropagation()
}}
onTouchStart={(e) => {
logLightboxEvent('overlay-touchstart', { target: (e.target as HTMLElement)?.tagName })
e.stopPropagation()
}}
>
<Lightbox
index={index}
slides={[
{
src: preferBlossomPrimalDisplayUrl(image.url),
alt: image.alt || image.url,
title: image.alt || undefined
}
]}
plugins={[Zoom, Captions]}
open={index >= 0}
close={() => {
logLightboxEvent('lightbox-close-callback')
setIndex(-1)
}}
controller={{
closeOnBackdropClick: false,
closeOnPullUp: true,
closeOnPullDown: true
}}
render={{
buttonPrev: () => null,
buttonNext: () => null
}}
styles={{
toolbar: { paddingTop: '2.25rem' }
}}
/>
</div>,
document.body
)}
</div>
)
}