import React, { useState, useEffect, useRef } from 'react'; import { X, Send, MessageSquare, User, Loader2, Calendar, MapPin, Download, Edit, Heart, Trash2 } from 'lucide-react'; import { io } from 'socket.io-client'; import { CoordinateSelectModal } from './CoordinateSelectModal'; import { useTranslation } from '../hooks/useTranslation'; import { useConfirm } from '../hooks/useConfirm'; import { useNotification } from '../hooks/useNotification'; interface Comment { id: string; userName: string; content: string; createdAt: string; userId: string; } interface PublicPhotoModalProps { isOpen: boolean; onClose: () => void; photo: { id: string; imageUrl: string; originalUrl?: string; capturedAt: string; metadata?: { lat?: number; lng?: number; title?: string; description?: string; }; uploader?: { id: string; name: string; }; uploaderId?: string; }; photoGroup?: any[]; onSelectPhoto?: (photo: any) => void; onLoginSuccess?: (user: any) => void; onUpdatePhoto?: (updatedPhoto: any) => void; } export const PublicPhotoModal: React.FC = ({ isOpen, onClose, photo, photoGroup = [], onSelectPhoto, onLoginSuccess, onUpdatePhoto }) => { const { t } = useTranslation(); const confirm = useConfirm(); const notify = useNotification(); const [comments, setComments] = useState([]); const [newComment, setNewComment] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isSending, setIsSending] = useState(false); const commentsEndRef = useRef(null); const [currentUser, setCurrentUser] = useState(null); const [isEditing, setIsEditing] = useState(false); const [editTitle, setEditTitle] = useState(''); const [editDescription, setEditDescription] = useState(''); const [editLat, setEditLat] = useState(''); const [editLng, setEditLng] = useState(''); const [isSavingEdit, setIsSavingEdit] = useState(false); const [isMapOpen, setIsMapOpen] = useState(false); const [resolvedAddress, setResolvedAddress] = useState(''); const [isFullscreen, setIsFullscreen] = useState(false); const isLoggedIn = !!localStorage.getItem('token') && !localStorage.getItem('guest_token'); useEffect(() => { const lat = photo?.metadata?.lat; const lng = photo?.metadata?.lng; if (typeof lat === 'number' && typeof lng === 'number') { setResolvedAddress('Đang xác định địa điểm...'); fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&accept-language=vi`) .then(res => { if (!res.ok) throw new Error(); return res.json(); }) .then(data => { if (data && data.display_name) { const shortAddress = data.display_name.split(',').slice(0, 3).join(',').trim(); setResolvedAddress(shortAddress || data.display_name); } else { setResolvedAddress(`${lat.toFixed(4)}, ${lng.toFixed(4)}`); } }) .catch(() => { setResolvedAddress(`${lat.toFixed(4)}, ${lng.toFixed(4)}`); }); } else { setResolvedAddress('Chưa xác định tọa độ'); } }, [photo?.id, photo?.metadata?.lat, photo?.metadata?.lng]); const checkCurrentUser = () => { const userStr = localStorage.getItem('user') || localStorage.getItem('guest_user'); if (userStr) { try { setCurrentUser(JSON.parse(userStr)); } catch (e) {} } else { setCurrentUser(null); } }; useEffect(() => { checkCurrentUser(); }, [isOpen]); useEffect(() => { if (photo) { setEditTitle(photo.metadata?.title || ''); setEditDescription(photo.metadata?.description || ''); setEditLat(photo.metadata?.lat ?? ''); setEditLng(photo.metadata?.lng ?? ''); setIsEditing(false); // Reset editing mode when selected photo changes } }, [photo]); const handleSaveEdit = async () => { if (editLat !== '' && (isNaN(editLat) || editLat < -90 || editLat > 90)) { alert('Vĩ độ không hợp lệ (-90 đến 90)'); return; } if (editLng !== '' && (isNaN(editLng) || editLng < -180 || editLng > 180)) { alert('Kinh độ không hợp lệ (-180 đến 180)'); return; } setIsSavingEdit(true); try { const token = localStorage.getItem('token') || localStorage.getItem('guest_token'); const res = await fetch(`/api/v1/photos/${photo.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ title: editTitle, description: editDescription, latitude: editLat === '' ? undefined : editLat, longitude: editLng === '' ? undefined : editLng }) }); if (res.ok) { const updatedPhoto = await res.json(); setIsEditing(false); if (onUpdatePhoto) { onUpdatePhoto(updatedPhoto); } } else { const err = await res.json(); alert(err.message || 'Lỗi khi cập nhật thông tin ảnh.'); } } catch (error) { console.error('Lỗi khi cập nhật thông tin ảnh:', error); alert('Không thể kết nối đến máy chủ.'); } finally { setIsSavingEdit(false); } }; const likedUserIds = photo.metadata && Array.isArray((photo.metadata as any).likedUserIds) ? (photo.metadata as any).likedUserIds : []; const isLiked = currentUser && likedUserIds.includes(currentUser.id); const likeCount = likedUserIds.length; const handleToggleLike = async () => { let token = localStorage.getItem('token') || localStorage.getItem('guest_token'); let userObj = currentUser; if (!token) { try { const guestRes = await fetch('/api/v1/auth/create-guest', { method: 'POST' }); if (guestRes.ok) { const guestData = await guestRes.json(); token = guestData.access_token; userObj = guestData.user; localStorage.setItem('token', token!); localStorage.setItem('user', JSON.stringify(userObj)); if (onLoginSuccess) { onLoginSuccess(userObj); } setCurrentUser(userObj); } else { return; } } catch (e) { console.error('Không thể tạo phiên khách:', e); return; } } try { const res = await fetch(`/api/v1/photos/${photo.id}/toggle-like`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { const updatedPhoto = await res.json(); if (onUpdatePhoto) { onUpdatePhoto(updatedPhoto); } } } catch (error) { console.error('Lỗi khi thích ảnh:', error); } }; const fetchComments = async () => { setIsLoading(true); try { const res = await fetch(`/api/v1/public-photos/${photo.id}/comments`); if (res.ok) { const data = await res.json(); setComments( data.map((c: any) => ({ id: c.id, userName: c.user?.name || 'Ẩn danh', content: c.content, createdAt: c.createdAt, userId: c.userId })) ); } } catch (error) { console.error('Lỗi khi tải bình luận:', error); } finally { setIsLoading(false); } }; useEffect(() => { if (!isOpen || !photo.id) return; fetchComments(); const socket = io(); socket.emit('joinPhoto', photo.id); socket.on('photoCommentAdded', (newCommentData: any) => { if (newCommentData.photoId === photo.id) { setComments(prev => { if (prev.find(c => c.id === newCommentData.id)) return prev; return [ ...prev, { id: newCommentData.id, userName: newCommentData.user?.name || 'Ẩn danh', content: newCommentData.content, createdAt: newCommentData.createdAt, userId: newCommentData.userId } ]; }); } }); socket.on('photoCommentDeleted', (deleted: any) => { if (deleted.photoId === photo.id) { setComments(prev => prev.filter(c => c.id !== deleted.id)); } }); return () => { socket.disconnect(); }; }, [isOpen, photo.id]); useEffect(() => { commentsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [comments]); const handleSend = async () => { if (!newComment.trim()) return; setIsSending(true); try { let token = localStorage.getItem('token'); let currentUser = JSON.parse(localStorage.getItem('user') || 'null'); // Nếu chưa có token, tự động tạo tài khoản khách if (!token) { const guestRes = await fetch('/api/v1/auth/create-guest', { method: 'POST' }); if (!guestRes.ok) throw new Error('Không thể tạo tài khoản khách tự động.'); const guestData = await guestRes.json(); token = guestData.access_token; currentUser = guestData.user; localStorage.setItem('guest_token', token!); localStorage.setItem('guest_user', JSON.stringify(currentUser)); localStorage.setItem('token', token!); localStorage.setItem('user', JSON.stringify(currentUser)); if (onLoginSuccess) { onLoginSuccess(currentUser); } } let res = await fetch(`/api/v1/public-photos/${photo.id}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ content: newComment }) }); if (res.status === 401) { console.warn('Token invalid or expired. Creating a new guest user and retrying comment...'); localStorage.removeItem('guest_token'); localStorage.removeItem('guest_user'); localStorage.removeItem('token'); localStorage.removeItem('user'); const guestRes = await fetch('/api/v1/auth/create-guest', { method: 'POST' }); if (!guestRes.ok) throw new Error('Không thể tạo lại phiên khách.'); const guestData = await guestRes.json(); token = guestData.access_token; currentUser = guestData.user; localStorage.setItem('guest_token', token!); localStorage.setItem('guest_user', JSON.stringify(currentUser)); localStorage.setItem('token', token!); localStorage.setItem('user', JSON.stringify(currentUser)); if (onLoginSuccess) { onLoginSuccess(currentUser); } res = await fetch(`/api/v1/public-photos/${photo.id}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ content: newComment }) }); } if (res.ok) { setNewComment(''); fetchComments(); } else { const err = await res.json(); console.error('Lỗi khi gửi bình luận:', err.message); } } catch (error) { console.error('Lỗi khi gửi bình luận:', error); } finally { setIsSending(false); } }; const handleDeleteComment = async (commentId: string) => { const shouldDelete = await confirm({ title: t('deleteComment') || 'Xóa bình luận', message: t('confirmDeleteComment') || 'Bạn có chắc chắn muốn xóa bình luận này không?' }); if (!shouldDelete) return; try { const token = localStorage.getItem('token') || localStorage.getItem('guest_token'); const res = await fetch(`/api/v1/public-photos/comments/${commentId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { setComments(prev => prev.filter(c => c.id !== commentId)); notify({ title: 'Thành công', message: 'Bình luận đã được xóa.', type: 'success' }); } else { const err = await res.json(); notify({ title: 'Lỗi', message: err.message || 'Lỗi khi xóa bình luận.', type: 'error' }); } } catch (error) { console.error('Lỗi khi xóa bình luận:', error); notify({ title: 'Lỗi', message: 'Không thể kết nối đến máy chủ.', type: 'error' }); } }; const isAuthorized = currentUser?.isAdmin || (currentUser && photo.uploader && currentUser.id === photo.uploader.id) || (currentUser && photo.uploaderId && currentUser.id === photo.uploaderId); if (!isOpen) return null; return (
{/* Backdrop */}
{ e.stopPropagation(); onClose(); }} /> {/* Container */}
e.stopPropagation()} > {/* Close Button Mobile/Desktop */} {/* Left Side: Photo Detail */}
{/* Photo wrapper for mobile view (handles top overlay name and bottom-right like) */}
{/* Mobile Only: Uploader details overlay */}
{photo.uploader?.name || 'Ẩn danh'}
{/* Like Button Overlay */} { e.preventDefault(); setIsFullscreen(true); }} > Public Map Upload { if (!isLoggedIn) e.preventDefault(); }} onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }} className={`w-full h-auto max-h-[85vh] object-contain md:h-full md:max-h-none select-none ${ !isLoggedIn ? 'pointer-events-none' : '' }`} draggable={false} /> {/* Shield overlay to prevent Save Image As on right-click / long press for non-owners */} {(!isAuthorized || !isLoggedIn) && ( {/* Info & Timeline overlay inside photo panel */}
{/* Timeline scroll */} {photoGroup && photoGroup.length > 1 && (
Lịch sử ảnh tại vị trí này ({photoGroup.length})
{photoGroup.map((p) => { const isActive = p.id === photo.id; return ( ); })}
)} {isEditing ? (

Chỉnh sửa thông tin ảnh

setEditTitle(e.target.value)} placeholder="Nhập tiêu đề cho ảnh..." className="w-full bg-slate-850 border border-slate-700/60 text-slate-100 rounded-xl px-3 py-2 text-base md:text-xs focus:outline-none focus:ring-1 focus:ring-emerald-500" />