import React, { useState, useEffect, useRef } from 'react'; import { X, Send, MessageSquare, User, Loader2, Calendar, MapPin, Download } from 'lucide-react'; import { io } from 'socket.io-client'; 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; }; uploader?: { id: string; name: string; }; }; photoGroup?: any[]; onSelectPhoto?: (photo: any) => void; onLoginSuccess?: (user: any) => void; } export const PublicPhotoModal: React.FC = ({ isOpen, onClose, photo, photoGroup = [], onSelectPhoto, onLoginSuccess }) => { const [comments, setComments] = useState([]); const [newComment, setNewComment] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isSending, setIsSending] = useState(false); const commentsEndRef = useRef(null); 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 } ]; }); } }); 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); } }; if (!isOpen) return null; return (
{/* Backdrop */}
{/* Container */}
{/* Close Button Mobile/Desktop */} {/* Left Side: Photo Detail */}
Public Map Upload {/* 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 ( ); })}
)} {/* Photo Metadata */}
{photo.uploader?.name || 'Ẩn danh'} {new Date(photo.capturedAt).toLocaleDateString('vi-VN', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })} {photo.metadata?.lat && photo.metadata?.lng && ( {photo.metadata.lat.toFixed(4)}, {photo.metadata.lng.toFixed(4)} )} {photo.originalUrl && ( Tải ảnh gốc )}
{/* Right Side: Comments */}
{/* Comments Header */}

Bình luận cộng đồng

Ảnh chia sẻ công khai trên bản đồ

{/* Comments list scroll area */}
{isLoading ? (
Đang tải bình luận...
) : comments.length === 0 ? (
Chưa có bình luận nào. Hãy bắt đầu cuộc trò chuyện!
) : ( comments.map((c) => { return (
{c.userName} {new Date(c.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}

{c.content}

); }) )}
{/* Comment Input Area */}
setNewComment(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && !isSending && handleSend()} placeholder="Viết bình luận công khai..." className="flex-1 bg-slate-800/65 border border-slate-700/70 text-slate-100 placeholder-slate-500 rounded-2xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all" disabled={isSending} />
); };