forked from 77media/video-flow
335 lines
14 KiB
TypeScript
335 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
import { Modal, Upload, message } from 'antd';
|
|
import NextImage from 'next/image';
|
|
import {
|
|
CloseOutlined,
|
|
UserOutlined,
|
|
CameraOutlined,
|
|
BulbOutlined,
|
|
CloudUploadOutlined,
|
|
CheckOutlined
|
|
} from '@ant-design/icons';
|
|
import type { PhotoType } from '../PhotoPreview/types';
|
|
|
|
interface AddItemModalProps {
|
|
/** Modal visibility state */
|
|
visible: boolean;
|
|
/** Close handler */
|
|
onClose: () => void;
|
|
/** Type of item to add */
|
|
itemType: PhotoType | null;
|
|
/** Submit handler */
|
|
onSubmit: (data: { name: string; description: string; file: File; index?: number }) => void;
|
|
/** Edit mode - initial data for editing */
|
|
editData?: {
|
|
index: number;
|
|
name?: string;
|
|
description?: string;
|
|
imageUrl?: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Modern modal for adding/editing items (character, scene, prop) with name, description, and image.
|
|
* Features a card-based, non-traditional form design matching the app theme.
|
|
*/
|
|
export function AddItemModal({
|
|
visible,
|
|
onClose,
|
|
itemType,
|
|
onSubmit,
|
|
editData,
|
|
}: AddItemModalProps) {
|
|
const [name, setName] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [imageFile, setImageFile] = useState<File | null>(null);
|
|
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const isEditMode = !!editData;
|
|
|
|
/** Initialize form with edit data */
|
|
useEffect(() => {
|
|
if (visible && editData) {
|
|
setName(editData.name || '');
|
|
setDescription(editData.description || '');
|
|
setImagePreview(editData.imageUrl || null);
|
|
setImageFile(null);
|
|
} else if (visible) {
|
|
setName('');
|
|
setDescription('');
|
|
setImagePreview(null);
|
|
setImageFile(null);
|
|
}
|
|
}, [visible, editData]);
|
|
|
|
/** Get item configuration based on type */
|
|
const getItemConfig = () => {
|
|
const prefix = isEditMode ? 'Edit' : 'Add';
|
|
switch (itemType) {
|
|
case 'character':
|
|
return {
|
|
icon: <UserOutlined className="text-2xl" />,
|
|
title: `${prefix} Character`,
|
|
namePlaceholder: 'Character name',
|
|
descriptionPlaceholder: 'Describe the character\'s appearance, personality, or key features...',
|
|
};
|
|
case 'scene':
|
|
return {
|
|
icon: <CameraOutlined className="text-2xl" />,
|
|
title: `${prefix} Scene`,
|
|
namePlaceholder: 'Scene name',
|
|
descriptionPlaceholder: 'Describe the location, atmosphere, or visual elements...',
|
|
};
|
|
case 'prop':
|
|
return {
|
|
icon: <BulbOutlined className="text-2xl" />,
|
|
title: `${prefix} Prop`,
|
|
namePlaceholder: 'Prop name',
|
|
descriptionPlaceholder: 'Describe the object\'s appearance, function, or significance...',
|
|
};
|
|
default:
|
|
return {
|
|
icon: null,
|
|
title: `${prefix} Item`,
|
|
namePlaceholder: 'Item name',
|
|
descriptionPlaceholder: 'Add a description...',
|
|
};
|
|
}
|
|
};
|
|
|
|
const config = getItemConfig();
|
|
|
|
/** Handle file selection */
|
|
const handleFileSelect = (file: File) => {
|
|
if (!file.type.startsWith('image/')) {
|
|
message.error('Please upload an image file');
|
|
return;
|
|
}
|
|
|
|
setImageFile(file);
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
setImagePreview(e.target?.result as string);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
};
|
|
|
|
/** Handle drag and drop */
|
|
const handleDrop = (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
setIsDragging(false);
|
|
|
|
const file = e.dataTransfer.files[0];
|
|
if (file) {
|
|
handleFileSelect(file);
|
|
}
|
|
};
|
|
|
|
/** Handle form submission */
|
|
const handleSubmit = () => {
|
|
if (!name.trim()) {
|
|
message.warning('Please enter a name');
|
|
return;
|
|
}
|
|
|
|
// In edit mode, if no new file is selected, we need to use existing image
|
|
if (!imageFile && !imagePreview) {
|
|
message.warning('Please upload an image');
|
|
return;
|
|
}
|
|
|
|
// If editing and no new file, create a dummy file for the existing image
|
|
const submitFile = imageFile || new File([], 'existing-image');
|
|
|
|
onSubmit({
|
|
name: name.trim(),
|
|
description: description.trim(),
|
|
file: submitFile,
|
|
...(isEditMode && editData && { index: editData.index }),
|
|
});
|
|
|
|
// Reset form
|
|
setName('');
|
|
setDescription('');
|
|
setImageFile(null);
|
|
setImagePreview(null);
|
|
onClose();
|
|
};
|
|
|
|
/** Handle modal close */
|
|
const handleClose = () => {
|
|
setName('');
|
|
setDescription('');
|
|
setImageFile(null);
|
|
setImagePreview(null);
|
|
onClose();
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
open={visible}
|
|
onCancel={handleClose}
|
|
footer={null}
|
|
closeIcon={<CloseOutlined className="text-gray-400 hover:text-cyan-400" />}
|
|
width="90%"
|
|
style={{ maxWidth: '480px' }}
|
|
styles={{
|
|
mask: { backdropFilter: 'blur(4px)', backgroundColor: 'rgba(0, 0, 0, 0.5)' },
|
|
content: {
|
|
padding: 0,
|
|
backdropFilter: 'blur(12px)',
|
|
backgroundColor: 'rgba(0, 0, 0, 0.85)',
|
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
borderRadius: '24px',
|
|
overflow: 'hidden',
|
|
},
|
|
}}
|
|
>
|
|
<div data-alt="add-item-modal" className="relative">
|
|
{/* Header with gradient */}
|
|
<div
|
|
data-alt="modal-header"
|
|
className="px-4 py-3 bg-gradient-to-r from-cyan-500/10 to-blue-500/10 border-b border-white/10"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-8 h-8 rounded-full bg-cyan-400/20 border border-cyan-400/40 flex items-center justify-center text-cyan-400">
|
|
{config.icon}
|
|
</div>
|
|
<h2 className="text-base font-semibold text-white">{config.title}</h2>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div data-alt="modal-content" className="p-4 space-y-4">
|
|
{/* Image Upload Area */}
|
|
<div data-alt="image-upload-section">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<CloudUploadOutlined className="text-cyan-400 text-sm" />
|
|
<span className="text-xs text-gray-300">Image</span>
|
|
</div>
|
|
|
|
<div
|
|
data-alt="upload-area"
|
|
className={`relative rounded-2xl border-2 border-dashed transition-all duration-300 overflow-hidden ${
|
|
isDragging
|
|
? 'border-cyan-400 bg-cyan-400/10'
|
|
: imagePreview
|
|
? 'border-cyan-400/40 bg-black/20'
|
|
: 'border-white/20 bg-black/20 hover:border-cyan-400/60 hover:bg-black/30'
|
|
}`}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
setIsDragging(true);
|
|
}}
|
|
onDragLeave={() => setIsDragging(false)}
|
|
onDrop={handleDrop}
|
|
onClick={() => fileInputRef.current?.click()}
|
|
style={{ cursor: 'pointer', aspectRatio: '16/9' }}
|
|
>
|
|
{imagePreview ? (
|
|
<div className="relative w-full h-full group">
|
|
<NextImage
|
|
src={imagePreview}
|
|
alt="Preview"
|
|
fill
|
|
className="object-cover"
|
|
unoptimized
|
|
/>
|
|
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center">
|
|
<span className="text-sm text-white">Click to change</span>
|
|
</div>
|
|
<div className="absolute top-2 right-2 w-6 h-6 rounded-full bg-cyan-400 flex items-center justify-center">
|
|
<CheckOutlined className="text-xs text-black" />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center gap-3 text-center px-6">
|
|
<CloudUploadOutlined className={`text-4xl transition-colors duration-200 ${
|
|
isDragging ? 'text-cyan-400' : 'text-gray-400'
|
|
}`} />
|
|
<div>
|
|
<p className="text-sm text-gray-300 mb-1">
|
|
Click to upload or drag & drop
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
PNG, JPG, WEBP up to 10MB
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
className="hidden"
|
|
onChange={(e) => {
|
|
const file = e.target.files?.[0];
|
|
if (file) handleFileSelect(file);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Name Input */}
|
|
<div data-alt="name-section">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<div className="w-1 h-3 bg-cyan-400 rounded-full" />
|
|
<span className="text-xs text-gray-300">Name</span>
|
|
</div>
|
|
<input
|
|
data-alt="name-input"
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder={config.namePlaceholder}
|
|
className="w-full px-3 py-2 bg-black/30 border border-white/10 rounded-lg text-gray-200 placeholder-gray-500 outline-none focus:border-cyan-400/60 focus:bg-black/40 transition-all duration-200 text-sm"
|
|
maxLength={50}
|
|
/>
|
|
</div>
|
|
|
|
{/* Description Input */}
|
|
<div data-alt="description-section">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<div className="w-1 h-3 bg-cyan-400 rounded-full" />
|
|
<span className="text-xs text-gray-300">Description</span>
|
|
<span className="text-xs text-gray-500">(Optional)</span>
|
|
</div>
|
|
<textarea
|
|
data-alt="description-input"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder={config.descriptionPlaceholder}
|
|
rows={3}
|
|
className="w-full px-3 py-2 bg-black/30 border border-white/10 rounded-lg text-gray-200 placeholder-gray-500 outline-none focus:border-cyan-400/60 focus:bg-black/40 transition-all duration-200 resize-none text-sm"
|
|
maxLength={200}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer with Actions */}
|
|
<div data-alt="modal-footer" className="px-4 py-3 bg-black/30 border-t border-white/10 flex items-center justify-end gap-2">
|
|
<button
|
|
data-alt="cancel-button"
|
|
onClick={handleClose}
|
|
className="px-4 py-2 rounded-lg bg-transparent border border-white/20 text-gray-300 hover:bg-white/5 hover:border-white/30 hover:text-white transition-all duration-200 text-xs font-medium"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
data-alt="submit-button"
|
|
onClick={handleSubmit}
|
|
className="px-4 py-2 rounded-lg bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-400 hover:to-blue-400 text-white shadow-lg shadow-cyan-500/20 hover:shadow-cyan-500/40 transition-all duration-200 text-xs font-medium"
|
|
>
|
|
{isEditMode ? 'Save Changes' : `Add ${itemType ? itemType.charAt(0).toUpperCase() + itemType.slice(1) : 'Item'}`}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|