| import { memo, useState, type MouseEvent as ReactMouseEvent } from 'react'; |
| import { ImageLightbox } from './image-preview/lightbox'; |
| import { useImageDownload } from './image-preview/use-image-download'; |
| import { useI18n } from '../i18n'; |
|
|
| interface ImagePreviewProps { |
| imageUrls: string[]; |
| } |
|
|
| export const ImagePreview = memo(function ImagePreview({ imageUrls }: ImagePreviewProps) { |
| const { t } = useI18n(); |
| const [activeIndex, setActiveIndex] = useState(0); |
| const [isLightboxOpen, setIsLightboxOpen] = useState(false); |
| const [zoom, setZoom] = useState(1); |
| const { isDownloadingSingle, isDownloadingAll, handleDownloadAll, handleDownloadSingle } = useImageDownload(imageUrls); |
| const safeActiveIndex = Math.min(activeIndex, Math.max(0, imageUrls.length - 1)); |
| const activeImage = imageUrls[safeActiveIndex]; |
| const hasImages = imageUrls.length > 0; |
|
|
| return ( |
| <div className="h-full flex flex-col bg-bg-secondary/30 rounded-2xl overflow-hidden"> |
| <div className="flex items-center justify-between px-4 py-2.5"> |
| <h3 className="text-xs font-medium text-text-secondary/80 uppercase tracking-wide">{t('image.title')}</h3> |
| <div className="flex items-center gap-3"> |
| <button |
| onClick={() => setIsLightboxOpen(true)} |
| disabled={!hasImages} |
| className="text-xs text-text-secondary/70 hover:text-accent transition-colors flex items-center gap-1.5 disabled:opacity-40 disabled:cursor-not-allowed" |
| > |
| <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 3h6m0 0v6m0-6l-7 7M9 21H3m0 0v-6m0 6l7-7" /> |
| </svg> |
| {t('image.openLightbox')} |
| </button> |
| <button |
| onClick={() => void handleDownloadSingle(activeImage, safeActiveIndex)} |
| disabled={!hasImages || isDownloadingSingle || isDownloadingAll} |
| className="text-xs text-text-secondary/70 hover:text-accent transition-colors flex items-center gap-1.5 disabled:opacity-40 disabled:cursor-not-allowed" |
| > |
| <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /> |
| </svg> |
| {isDownloadingSingle ? t('image.downloading') : t('common.download')} |
| </button> |
| <button |
| onClick={() => void handleDownloadAll(imageUrls)} |
| disabled={!hasImages || isDownloadingAll || isDownloadingSingle} |
| className="text-xs text-text-secondary/70 hover:text-accent transition-colors flex items-center gap-1.5 disabled:opacity-40 disabled:cursor-not-allowed" |
| > |
| <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /> |
| </svg> |
| {isDownloadingAll ? t('image.zipping') : t('image.downloadAll')} |
| </button> |
| </div> |
| </div> |
| |
| <div className="flex-1 bg-black/90 flex items-center justify-center"> |
| {activeImage ? ( |
| <div |
| role="button" |
| tabIndex={0} |
| onClick={(event: ReactMouseEvent<HTMLDivElement>) => { |
| if (event.button !== 0) { |
| return; |
| } |
| setIsLightboxOpen(true); |
| }} |
| onKeyDown={(event) => { |
| if (event.key === 'Enter' || event.key === ' ') { |
| event.preventDefault(); |
| setIsLightboxOpen(true); |
| } |
| }} |
| className="w-full h-full cursor-zoom-in" |
| title={t('image.openTitle')} |
| > |
| <img src={activeImage} alt={t('image.itemAlt', { index: safeActiveIndex + 1 })} className="w-full h-full object-contain" /> |
| </div> |
| ) : ( |
| <p className="text-xs text-text-secondary/60">{t('image.empty')}</p> |
| )} |
| </div> |
| |
| {imageUrls.length > 1 && ( |
| <div className="px-3 py-2 bg-bg-secondary/40"> |
| <div className="flex gap-2 overflow-x-auto"> |
| {imageUrls.map((url, index) => ( |
| <button |
| key={`${url}-${index}`} |
| type="button" |
| onClick={() => setActiveIndex(index)} |
| className={`shrink-0 rounded-md overflow-hidden border transition-all ${ |
| index === safeActiveIndex ? 'border-accent' : 'border-border/50 opacity-80 hover:opacity-100' |
| }`} |
| > |
| <img |
| src={url} |
| alt={t('image.thumbAlt', { index: index + 1 })} |
| className="w-16 h-12 object-cover" |
| /> |
| </button> |
| ))} |
| </div> |
| </div> |
| )} |
| |
| <ImageLightbox |
| isOpen={isLightboxOpen} |
| activeImage={activeImage} |
| activeIndex={safeActiveIndex} |
| total={imageUrls.length} |
| zoom={zoom} |
| maxZoom={3} |
| onZoomChange={setZoom} |
| onClose={() => { |
| setIsLightboxOpen(false); |
| setZoom(1); |
| }} |
| /> |
| </div> |
| ); |
| }); |
| |