import React, { useState, useRef, useEffect } from 'react'; import { Compass, Map as MapIcon, Camera, ShieldAlert, Image as ImageIcon, X } from 'lucide-react'; import { LoginModal } from '../components/LoginModal'; import { ReportBusinessModal } from '../components/ReportBusinessModal'; import { TagSelectModal } from '../components/TagSelectModal'; import { MapProfileDropdown } from '../components/MapProfileDropdown'; import { ProfileSettingsModal } from '../components/ProfileSettingsModal'; import { MyToursModal } from '../components/modals/MyToursModal'; import { PhotoGalleryModal as MyPhotosModal } from '../components/modals/PhotoGalleryModal'; import { LiveChatModal } from '../components/modals/LiveChatModal'; import { FriendsManagerModal } from '../components/modals/FriendsManagerModal'; import { useNotification } from '@/hooks/useNotification'; import { processImageModeration } from '../hooks/useImageModeration'; import { useTranslation } from '../hooks/useTranslation'; import { compressImage } from '../utils/image'; interface LandingPageProps { onContinue?: () => void; onGoToSignup?: () => void; onGoToMap?: () => void; onLoginSuccess?: (user: any) => void; isInitialSetup?: boolean; user?: any; onLogout?: () => void; onGoToDashboard?: (tab?: 'tours' | 'connections' | 'photos' | 'chats') => void; onOpenNavigation?: (payload: any) => void; } export const LandingPage: React.FC = ({ onGoToSignup, onGoToMap, onLoginSuccess, user, onLogout, onOpenNavigation, }) => { const [isLoginModalOpen, setIsLoginModalOpen] = useState(false); const [isProfileSettingsOpen, setIsProfileSettingsOpen] = useState(false); const [isMyToursOpen, setIsMyToursOpen] = useState(false); const [isMyPhotosOpen, setIsMyPhotosOpen] = useState(false); const [isChatOpen, setIsChatOpen] = useState(false); const [isFriendsOpen, setIsFriendsOpen] = useState(false); const [chatTargetUserId, setChatTargetUserId] = useState(null); const [isReportModalOpen, setIsReportModalOpen] = useState(false); const [isTagsModalOpen, setIsTagsModalOpen] = useState(false); const [isPhotoSourceModalOpen, setIsPhotoSourceModalOpen] = useState(false); const fileInputRef = useRef(null); const cameraInputRef = useRef(null); const galleryInputRef = useRef(null); const [pendingPhotoFile, setPendingPhotoFile] = useState(null); const [pendingPhotoLocation, setPendingPhotoLocation] = useState(null); const [photoPreviewUrl, setPhotoPreviewUrl] = useState(''); const notify = useNotification(); const { t } = useTranslation(); const [publicPhotos, setPublicPhotos] = useState([]); const [trustedUsers, setTrustedUsers] = useState([]); const [blacklist, setBlacklist] = useState([]); const [currentBgIndex, setCurrentBgIndex] = useState(0); const [bg1, setBg1] = useState('/background.avif'); const [bg2, setBg2] = useState(''); const [fade1, setFade1] = useState(true); const [fade2, setFade2] = useState(false); const [activeSlot, setActiveSlot] = useState<1 | 2>(1); const isLoggedIn = !!localStorage.getItem('token') && !localStorage.getItem('guest_token'); useEffect(() => { const nextUrl = publicPhotos.length > 0 ? publicPhotos[currentBgIndex]?.imageUrl : '/background.avif'; if (!nextUrl) return; const currentUrl = activeSlot === 1 ? bg1 : bg2; if (nextUrl === currentUrl) return; if (activeSlot === 1) { setBg2(nextUrl); setFade2(true); setFade1(false); setActiveSlot(2); } else { setBg1(nextUrl); setFade1(true); setFade2(false); setActiveSlot(1); } }, [currentBgIndex, publicPhotos]); const fetchPublicPhotos = async () => { try { const res = await fetch('/api/v1/public-photos'); if (res.ok) { const data = await res.json(); const sortedData = data.sort((a: any, b: any) => { const likesA = a.metadata?.likedUserIds?.length || 0; const likesB = b.metadata?.likedUserIds?.length || 0; return likesB - likesA; }); setPublicPhotos(sortedData); } } catch (e) { console.error('Lỗi khi tải ảnh công khai:', e); } }; const fetchTrustedUsers = async () => { try { const res = await fetch('/api/v1/users/trusted'); if (res.ok) { const data = await res.json(); setTrustedUsers(data); } } catch (e) { console.error('Lỗi khi tải thành viên uy tín:', e); } }; const fetchBlacklist = async () => { try { const res = await fetch('/api/v1/reports/blacklist'); if (res.ok) { const data = await res.json(); setBlacklist(data); } } catch (e) { console.error('Lỗi khi tải blacklist:', e); } }; // Update Open Graph meta tags when public photos are fetched useEffect(() => { if (publicPhotos.length > 0) { const mainPhoto = publicPhotos[0]; const photoUrl = mainPhoto.imageUrl || mainPhoto.originalUrl || '/background.avif'; const photoTitle = mainPhoto.metadata?.title || 'Travel Planner - Khám phá chuyến đi tuyệt vời'; const photoDescription = mainPhoto.metadata?.description || `Được chia sẻ bởi ${mainPhoto.uploader?.name || 'một thành viên'}. Khám phá những hành trình tuyệt vời trên Travel Planner.`; // Update og:image let ogImage = document.querySelector('meta[property="og:image"]'); if (!ogImage) { ogImage = document.createElement('meta'); ogImage.setAttribute('property', 'og:image'); document.head.appendChild(ogImage); } ogImage.setAttribute('content', `https://yotrip.labz.io.vn${photoUrl}`); // Update og:title let ogTitle = document.querySelector('meta[property="og:title"]'); if (!ogTitle) { ogTitle = document.createElement('meta'); ogTitle.setAttribute('property', 'og:title'); document.head.appendChild(ogTitle); } ogTitle.setAttribute('content', photoTitle); // Update og:description let ogDescription = document.querySelector('meta[property="og:description"]'); if (!ogDescription) { ogDescription = document.createElement('meta'); ogDescription.setAttribute('property', 'og:description'); document.head.appendChild(ogDescription); } ogDescription.setAttribute('content', photoDescription); // Update twitter:image let twitterImage = document.querySelector('meta[name="twitter:image"]'); if (!twitterImage) { twitterImage = document.createElement('meta'); twitterImage.setAttribute('name', 'twitter:image'); document.head.appendChild(twitterImage); } twitterImage.setAttribute('content', `https://yotrip.labz.io.vn${photoUrl}`); } }, [publicPhotos]); useEffect(() => { fetchPublicPhotos(); fetchTrustedUsers(); fetchBlacklist(); }, []); useEffect(() => { if (publicPhotos.length <= 1) return; const interval = setInterval(() => { setCurrentBgIndex((prev) => (prev + 1) % publicPhotos.length); }, 12000); return () => clearInterval(interval); }, [publicPhotos]); const handleFileChange = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; notify({ title: 'Đang xử lý...', message: 'Vui lòng chờ trong giây lát.', type: 'info' }); try { // Nén ảnh trước const compressedFile = await compressImage(file); // 0. Kiểm duyệt ả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' }); return; } const processedFile = moderationResult.file; // Lấy tọa độ hiện tại của người dùng với cơ chế chống treo (Promise.race) 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)) ]); // Lưu file và location vào state pending, hiển thị modal tags setPendingPhotoFile(processedFile); setPendingPhotoLocation(location); // Tạo preview URL cho ảnh const previewUrl = URL.createObjectURL(processedFile); setPhotoPreviewUrl(previewUrl); setIsTagsModalOpen(true); } catch (error: any) { notify({ title: 'Lỗi', message: error.message, type: 'error' }); } finally { // Reset input để có thể chọn lại cùng 1 file if (event.target) event.target.value = ''; } }; const handleConfirmTags = async (selectedTags: string[]) => { if (!pendingPhotoFile) return; setIsTagsModalOpen(false); notify({ title: 'Đang tải lên...', message: 'Vui lòng chờ trong giây lát.', type: 'info' }); try { // 1. Kiểm tra xem người dùng đã đăng nhập chưa const token = localStorage.getItem('token'); const guestToken = localStorage.getItem('guest_token'); const isRealUser = token && !guestToken; let uploadToken = token; // 2. Nếu là khách, tạo tài khoản khách và lấy token if (!isRealUser) { let currentGuestToken = guestToken; let guestUser = JSON.parse(localStorage.getItem('guest_user') || 'null'); if (!currentGuestToken) { const guestRes = await fetch('/api/v1/auth/create-guest', { method: 'POST' }); if (!guestRes.ok) throw new Error('Không thể tạo phiên khách.'); const guestData = await guestRes.json(); currentGuestToken = guestData.access_token; guestUser = guestData.user; localStorage.setItem('guest_token', currentGuestToken!); localStorage.setItem('guest_user', JSON.stringify(guestUser)); } uploadToken = currentGuestToken; } // 3. Tải ảnh lên const formData = new FormData(); formData.append('images', pendingPhotoFile); if (pendingPhotoLocation) { formData.append('latitude', pendingPhotoLocation.coords.latitude.toString()); formData.append('longitude', pendingPhotoLocation.coords.longitude.toString()); } // Thêm tags vào formData if (selectedTags.length > 0) { formData.append('tags', JSON.stringify(selectedTags)); } let uploadRes = await fetch('/api/v1/photos/upload-anonymous', { method: 'POST', headers: { 'Authorization': `Bearer ${uploadToken!}` }, body: formData, }); if (uploadRes.status === 401 && !isRealUser) { console.warn('Guest token invalid or expired. Creating a new guest user and retrying...'); localStorage.removeItem('guest_token'); localStorage.removeItem('guest_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(); const newGuestToken = guestData.access_token; const guestUser = guestData.user; localStorage.setItem('guest_token', newGuestToken!); localStorage.setItem('guest_user', JSON.stringify(guestUser)); uploadRes = await fetch('/api/v1/photos/upload-anonymous', { method: 'POST', headers: { 'Authorization': `Bearer ${newGuestToken!}` }, body: formData, }); } if (!uploadRes.ok) { const errorData = await uploadRes.json(); throw new Error(errorData.message || 'Tải ảnh thất bại.'); } notify({ title: 'Thành công!', message: 'Ảnh của bạn đã được tải lên.', type: 'success' }); // Xóa pendingInviteToken và parameter token khỏi URL để tránh tự động chuyển sang JoinTourPage localStorage.removeItem('pendingInviteToken'); const url = new URL(window.location.href); url.searchParams.delete('token'); window.history.replaceState({}, '', url.toString()); // Cập nhật lại danh sách ảnh lập tức await fetchPublicPhotos(); setCurrentBgIndex(0); // Xóa pending data setPendingPhotoFile(null); setPendingPhotoLocation(null); if (photoPreviewUrl) { URL.revokeObjectURL(photoPreviewUrl); setPhotoPreviewUrl(''); } } catch (error: any) { notify({ title: 'Lỗi', message: error.message, type: 'error' }); // Cleanup on error too setPendingPhotoFile(null); setPendingPhotoLocation(null); if (photoPreviewUrl) { URL.revokeObjectURL(photoPreviewUrl); setPhotoPreviewUrl(''); } } }; return (
{/* Background Image with Horizontal Panning */}
{/* Active image for panning */}
{bg1 && ( { if (!isLoggedIn) e.preventDefault(); }} onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }} className={`h-full image-pan-element ${!isLoggedIn ? 'select-none pointer-events-none' : '' }`} alt="Travel Background 1" /> )}
{bg2 && ( { if (!isLoggedIn) e.preventDefault(); }} onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }} className={`h-full image-pan-element ${!isLoggedIn ? 'select-none pointer-events-none' : '' }`} alt="Travel Background 2" /> )}
{/* Style for horizontal panning */} {/* Swipe Indicator Arrows - for image switching */} {publicPhotos.length > 1 && ( <> )} {/* Photo Progress Dots */} {publicPhotos.length > 1 && (
{publicPhotos.slice(0, 20).map((_, idx) => (
)} {/* Top Bar - Thanh điều hướng trên cùng */}
YoTrip
{user && !localStorage.getItem('guest_token') && ( )} setIsProfileSettingsOpen(true)} onOpenCreateTour={() => onGoToMap?.()} onOpenReport={() => setIsReportModalOpen(true)} onOpenLogin={() => setIsLoginModalOpen(true)} onOpenMyPhotos={() => setIsMyPhotosOpen(true)} onOpenMyTours={() => setIsMyToursOpen(true)} onOpenFriends={() => setIsFriendsOpen(true)} />
{/* Floating Trusted Leaderboard Panel (Left Side on Desktop) */}

🏆 {t('trustedMembers')}

{trustedUsers.length === 0 ? (
Chưa có đánh giá nào.
) : ( trustedUsers.map((u, idx) => (
{u.avatar ? ( Avatar ) : ( {u.name.charAt(0)} )}
{idx + 1}
{u.name}
★ {u.averageScore} ({u.ratingCount})
)) )}
{/* Floating Blacklist Panel (Right Side on Desktop) */}

{t('blacklistTitle')}

{blacklist.length === 0 ? (
{t('emptyBlacklist')}
) : ( blacklist.map((item, idx) => (
{item.name}
{item.type}
{item.address && (
{item.address}
)} {item.phone && (
SĐT: {item.phone}
)}
Lý do: {item.reason}
)) )}
{/* Mobile Only: Trusted Members Top Bar */} {trustedUsers.length > 0 && (
🏆 {t('trustedMembers')}
{trustedUsers.slice(0, 5).map((u, idx) => (
#{idx + 1} {u.name} ★ {u.averageScore}
))}
)} {/* Input chọn file ẩn để chụp/chọn ảnh */} {/* Camera input - capture="environment" for rear camera */} {/* Gallery input - no capture attribute for file picker */} {/* Unified Bottom Container: Gallery thumbnails on top, action buttons below */}
{/* Community Gallery Previews */} {publicPhotos.length > 0 && (
{t('momentsTitle')} ({publicPhotos.length})
{publicPhotos.slice(0, 8).map((photo, index) => ( ))}
)} {/* Buttons */}
{/* Login Modal Component */} setIsLoginModalOpen(false)} onSwitchToSignup={onGoToSignup} onLoginSuccess={onLoginSuccess} /> {/* Report Business Modal */} setIsReportModalOpen(false)} /> {/* Tag Select Modal */} { setIsTagsModalOpen(false); setPendingPhotoFile(null); setPendingPhotoLocation(null); if (photoPreviewUrl) { URL.revokeObjectURL(photoPreviewUrl); setPhotoPreviewUrl(''); } }} onConfirm={handleConfirmTags} photoUrl={photoPreviewUrl} /> {/* Photo Source Selection Modal */} {isPhotoSourceModalOpen && (
{/* Header */}

Chụp hoặc tải ảnh

{/* Content */}
{/* Camera Button */} {/* Gallery Button */}
)} {/* Profile Settings Modal */} setIsProfileSettingsOpen(false)} user={user} onSaveSuccess={onLoginSuccess} /> {/* My Tours Modal */} setIsMyToursOpen(false)} user={user} onViewTour={(tourId) => { if (onGoToMap) { localStorage.setItem('viewTourOnLand', tourId); onGoToMap(); } }} onOpenNavigation={onOpenNavigation} /> {/* My Photos Modal */} setIsMyPhotosOpen(false)} user={user} /> {/* Live Chat Modal */} setIsChatOpen(false)} user={user} defaultChatUserId={chatTargetUserId} /> {/* Friends Manager Modal */} setIsFriendsOpen(false)} user={user} onOpenChatWithUser={(userId) => { setChatTargetUserId(userId); setIsChatOpen(true); }} />
); };