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 { processAndResizeImage } from '../utils/imageProcessor'; import { getDeviceLocation } from '../utils/geolocation'; interface AddPhotoModalProps { isOpen: boolean; onClose: () => void; tourId: string; onSuccess?: () => void; isPublicView?: boolean; } interface PendingPhoto { file: File; latitude: number; longitude: number; } 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); 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 { // Fetch device location once to serve as Priority 2 fallback for files without EXIF const deviceLocation = await getDeviceLocation(); const newValidPhotos: PendingPhoto[] = []; const newValidPreviews: string[] = []; for (const file of files) { 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; } // Process image: Reads EXIF data and forces 2K resizing on device memory const { file: processedFile, latitude: exifLat, longitude: exifLng } = await processAndResizeImage(file); // 2. Chạy kiểm duyệt hình ảnh const moderationResult = await processImageModeration(processedFile); 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 finalProcessedFile = moderationResult.file; const previewUrl = URL.createObjectURL(finalProcessedFile); // 3. Kiểm tra tính toàn vẹn const isValidImage = await new Promise((resolve) => { const img = new Image(); img.onload = () => resolve(true); img.onerror = () => resolve(false); img.src = previewUrl; }); if (isValidImage) { // Resolve coordinates based on priority checklist: let finalLat: number | null = exifLat; let finalLng: number | null = exifLng; // Priority 2: Device location if ((finalLat === null || finalLng === null) && deviceLocation) { finalLat = deviceLocation.latitude; finalLng = deviceLocation.longitude; } // Priority 3: Map view state if (finalLat === null || finalLng === null) { const lastViewStateStr = localStorage.getItem('map_view_state'); if (lastViewStateStr) { try { const lastViewState = JSON.parse(lastViewStateStr); if (lastViewState && Array.isArray(lastViewState.center) && lastViewState.center.length === 2) { finalLat = Number(lastViewState.center[0]); finalLng = Number(lastViewState.center[1]); } } catch (e) { console.error('[AddPhotoModal] Error parsing map_view_state:', e); } } } // Priority 4: Fallback defaults if (finalLat === null || finalLng === null) { finalLat = 10.7769; finalLng = 106.7009; } newValidPhotos.push({ file: finalProcessedFile, latitude: finalLat, longitude: finalLng }); newValidPreviews.push(previewUrl); } else { URL.revokeObjectURL(previewUrl); 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, ...newValidPhotos]); 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 { // Upload each photo sequentially so we can attach its specific coordinates for (const item of selectedFiles) { const formData = new FormData(); formData.append('latitude', item.latitude.toString()); formData.append('longitude', item.longitude.toString()); formData.append('images', item.file); 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('Tải lên ảnh thất bại'); } // 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
))}
)}
); };