2025-10-17 20:32:10 +08:00

148 lines
5.3 KiB
TypeScript

"use client";
import { useState } from 'react';
import { Image } from 'antd';
import { CloseOutlined, UserOutlined, CameraOutlined, BulbOutlined } from '@ant-design/icons';
import type { PhotoPreviewSectionProps, PhotoType } from './types';
import './styles.css';
/**
* Photo Preview Section Component
* Displays a horizontal list of photos with type indicators and delete functionality
* @param photos - Array of photos with type information
* @param onDelete - Callback when a photo is deleted
* @param className - Additional CSS classes
*/
export default function PhotoPreviewSection({
photos = [],
onDelete,
className = '',
}: PhotoPreviewSectionProps) {
const [previewVisible, setPreviewVisible] = useState(false);
const [previewImage, setPreviewImage] = useState('');
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
/**
* Get icon for photo type
*/
const getTypeIcon = (type: PhotoType) => {
switch (type) {
case 'character':
return <UserOutlined className="text-sm" />;
case 'scene':
return <CameraOutlined className="text-sm" />;
case 'prop':
return <BulbOutlined className="text-sm" />;
default:
return null;
}
};
/**
* Get label for photo type
*/
const getTypeLabel = (type: PhotoType) => {
switch (type) {
case 'character':
return 'Character';
case 'scene':
return 'Scene';
case 'prop':
return 'Prop';
default:
return '';
}
};
/**
* Handle photo preview
*/
const handlePreview = (url: string) => {
setPreviewImage(url);
setPreviewVisible(true);
};
/**
* Handle photo deletion
*/
const handleDelete = (index: number) => {
onDelete?.(index);
};
/** Don't render if no photos */
if (photos.length === 0) {
return null;
}
return (
<div data-alt="photo-preview-section" className={`w-full ${className}`}>
{/* Photo List Container with Horizontal Scroll */}
<div
data-alt="photo-list-container"
className="flex gap-2 overflow-x-auto overflow-y-hidden photo-list-scrollbar pb-1"
>
{photos.map((photo, index) => (
<div
key={photo.id || `photo-${index}`}
data-alt="photo-item"
className="relative flex-shrink-0 w-16 h-16 rounded-md overflow-visible border border-white/10 hover:border-cyan-400/60 transition-all duration-200 cursor-pointer group bg-black/20"
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
>
{/* Photo Image */}
<div
className="w-full h-full rounded-md overflow-hidden"
onClick={() => handlePreview(photo.url)}
>
<img
src={photo.url}
alt={`${getTypeLabel(photo.type)} ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
{/* Type Icon - Bottom Left Corner */}
<div
data-alt="type-icon"
className="absolute bottom-1 left-1 w-5 h-5 bg-black/60 backdrop-blur-sm text-cyan-400 rounded-sm flex items-center justify-center z-[5]"
>
{getTypeIcon(photo.type)}
</div>
{/* Delete Button - Top Right Corner */}
{hoveredIndex === index && (
<button
data-alt="delete-button"
className="absolute top-1 right-1 p-1 bg-[#636364] opacity-80 hover:opacity-100 text-white rounded-full flex items-center justify-center transition-all duration-200 shadow-lg hover:scale-110 z-10"
onClick={(e) => {
e.stopPropagation();
handleDelete(index);
}}
>
<CloseOutlined className="text-[10px]" />
</button>
)}
</div>
))}
</div>
{/* Preview Modal - Using absolute positioning to avoid layout space */}
<div style={{ position: 'absolute', width: 0, height: 0, overflow: 'hidden', opacity: 0, pointerEvents: 'none' }}>
<Image
width={0}
height={0}
src={previewImage}
preview={{
visible: previewVisible,
src: previewImage,
onVisibleChange: (visible) => {
setPreviewVisible(visible);
},
}}
/>
</div>
</div>
);
}