import React, { useState, useRef } from 'react'; import { X, Upload, Image as ImageIcon, Loader2 } from 'lucide-react'; import { useNotification } from '@/hooks/useNotification'; import { useTourStore } from '@/store/useTourStore'; import { processImageModeration } from '@/hooks/useImageModeration'; import { compressImage } from '../utils/image'; interface AddPhotoModalProps { isOpen: boolean; onClose: () => void; tourId: string; onSuccess?: () => void; isPublicView?: boolean; } export const AddPhotoModal: React.FC = ({ isOpen, onClose, tourId, onSuccess, isPublicView }) => { const [selectedFiles, setSelectedFiles] = useState([]); const [previews, setPreviews] = useState([]); const [isUploading, setIsUploading] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const fileInputRef = useRef(null); const notify = useNotification(); const fetchTour = useTourStore(state => state.fetchTour); if (!isOpen) return null; const handleFileChange = async (e: React.ChangeEvent) => { if (e.target.files) { const files = Array.from(e.target.files); const newValidFiles: File[] = []; const newValidPreviews: string[] = []; setIsProcessing(true); notify({ title: 'Đang kiểm duyệt...', message: 'Đang kiểm tra và lọc hình ảnh của bạn...', type: 'info' }); try { for (const file of files) { // 1. Kiểm tra cơ bản: Tệp có dung lượng hay không if (!file || file.size === 0) { notify({ title: 'Lỗi tệp', message: `Tệp ${file.name} trống hoặc không khả dụng.`, type: 'error' }); continue; } // Nén ảnh trước const compressedFile = await compressImage(file); // 2. Chạy kiểm duyệt hình ảnh const moderationResult = await processImageModeration(compressedFile); if (moderationResult.blocked) { notify({ title: 'Ảnh bị từ chối', message: `Ảnh ${file.name} chứa nội dung không phù hợp và bị chặn.`, type: 'error' }); continue; } const processedFile = moderationResult.file; const previewUrl = URL.createObjectURL(processedFile); // 3. Kiểm tra tính toàn vẹn: Thử load ảnh vào bộ nhớ để xác nhận file "tồn tại" và đọc được const isValidImage = await new Promise((resolve) => { const img = new Image(); img.onload = () => resolve(true); img.onerror = () => resolve(false); img.src = previewUrl; }); if (isValidImage) { newValidFiles.push(processedFile); newValidPreviews.push(previewUrl); } else { URL.revokeObjectURL(previewUrl); // Thu hồi ngay nếu không hợp lệ notify({ title: 'Lỗi ảnh', message: `Không thể đọc nội dung ảnh: ${file.name}. Vui lòng kiểm tra lại file.`, type: 'error' }); } } setSelectedFiles(prev => [...prev, ...newValidFiles]); setPreviews(prev => [...prev, ...newValidPreviews]); } catch (err) { console.error('File checking error:', err); notify({ title: 'Lỗi', message: 'Lỗi trong quá trình kiểm duyệt ảnh.', type: 'error' }); } finally { setIsProcessing(false); } } }; const removeFile = (index: number) => { // Thu hồi URL khi xóa khỏi danh sách chờ để giải phóng bộ nhớ URL.revokeObjectURL(previews[index]); setSelectedFiles(prev => prev.filter((_, i) => i !== index)); setPreviews(prev => prev.filter((_, i) => i !== index)); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (selectedFiles.length === 0) return; setIsUploading(true); try { // Lấy tọa độ hiện tại của người dùng làm dự phòng nếu ảnh EXIF không có GPS const location = await Promise.race([ new Promise((resolve) => { if (!navigator.geolocation) { resolve(null); } else { navigator.geolocation.getCurrentPosition( (pos) => resolve(pos), () => resolve(null), { timeout: 4000, enableHighAccuracy: true } ); } }), new Promise((resolve) => setTimeout(() => resolve(null), 4500)) ]); const formData = new FormData(); selectedFiles.forEach(file => { formData.append('images', file); }); if (location) { formData.append('latitude', location.coords.latitude.toString()); formData.append('longitude', location.coords.longitude.toString()); } const response = await fetch(`/api/v1/tours/${tourId}/photos`, { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: formData }); if (!response.ok) throw new Error('Upload failed'); notify({ title: 'Thành công', message: `Đã tải lên ${selectedFiles.length} ảnh.`, type: 'success' }); // Giải phóng bộ nhớ sau khi hoàn tất previews.forEach(url => URL.revokeObjectURL(url)); setSelectedFiles([]); setPreviews([]); // Refresh tour data (applies to both authenticated and public users) // This ensures newly uploaded photos appear immediately without requiring a page reload fetchTour(tourId); if (onSuccess) onSuccess(); // For public users: redirect to landing page after upload (after data refresh) // For authenticated users: close modal and show updated tour if (isPublicView) { onClose(); // Give time for tour data to refresh before redirecting setTimeout(() => { localStorage.setItem('fromPublicUpload', 'true'); window.location.href = '/'; }, 1500); } else { onClose(); } } catch (error) { notify({ title: 'Lỗi', message: 'Không thể tải ảnh lên. Vui lòng thử lại.', type: 'error' }); } finally { setIsUploading(false); } }; return (

Tải ảnh lên

fileInputRef.current?.click()} className="border-2 border-dashed border-[var(--border)] rounded-[32px] p-10 flex flex-col items-center justify-center cursor-pointer hover:bg-[var(--background)]/50 hover:border-blue-200 transition-all mb-6 group" >

Nhấn để chọn ảnh

Hỗ trợ JPG, PNG, WEBP

{previews.length > 0 && (

Đã chọn {previews.length} tệp

{previews.map((src, idx) => (
preview
))}
)}
); };