import React, { useState, useRef, useEffect } from 'react'; import { LogIn, Compass, Map as MapIcon, Camera, ShieldAlert } from 'lucide-react'; import { LoginModal } from '../components/LoginModal'; import { ReportBusinessModal } from '../components/ReportBusinessModal'; import { useNotification } from '@/hooks/useNotification'; import { processImageModeration } from '../hooks/useImageModeration'; import { useTranslation } from '../hooks/useTranslation'; import { useTheme } from '../hooks/useTheme'; import { compressImage } from '../utils/image'; interface LandingPageProps { onContinue?: () => void; onGoToSignup?: () => void; onGoToMap?: () => void; onLoginSuccess?: (user: any) => void; isInitialSetup?: boolean; } export const LandingPage: React.FC = ({ onGoToSignup, onGoToMap, onLoginSuccess }) => { const [isLoginModalOpen, setIsLoginModalOpen] = useState(false); const fileInputRef = useRef(null); const notify = useNotification(); const { t, lang, changeLanguage } = useTranslation(); const { theme, changeTheme } = useTheme(); const [publicPhotos, setPublicPhotos] = useState([]); const [trustedUsers, setTrustedUsers] = useState([]); const [blacklist, setBlacklist] = useState([]); const [isReportModalOpen, setIsReportModalOpen] = useState(false); 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 [touchOffsetX, setTouchOffsetX] = useState(0); const touchStartXRef = useRef(0); const isSwipingRef = useRef(false); const isLoggedIn = !!localStorage.getItem('token') && !localStorage.getItem('guest_token'); const handleTouchStart = (e: React.TouchEvent) => { touchStartXRef.current = e.touches[0].clientX; isSwipingRef.current = true; }; const handleTouchMove = (e: React.TouchEvent) => { if (!isSwipingRef.current) return; const currentX = e.touches[0].clientX; const diffX = currentX - touchStartXRef.current; // Clamp the translation to [-70, 70] to ensure the background never shows white/black gaps const clampedX = Math.max(-70, Math.min(70, diffX)); setTouchOffsetX(clampedX); }; const handleTouchEnd = () => { if (!isSwipingRef.current) return; isSwipingRef.current = false; // Threshold lowered to 50px for better swipe responsiveness on mobile screens if (touchOffsetX > 50 && publicPhotos.length > 0) { setCurrentBgIndex((prev) => (prev - 1 + publicPhotos.length) % publicPhotos.length); } else if (touchOffsetX < -50 && publicPhotos.length > 0) { setCurrentBgIndex((prev) => (prev + 1) % publicPhotos.length); } setTouchOffsetX(0); }; 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); } }; 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)) ]); // 1. Tạo tài khoản khách và lấy token let guestToken = localStorage.getItem('guest_token'); let guestUser = JSON.parse(localStorage.getItem('guest_user') || 'null'); if (!guestToken) { 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(); guestToken = guestData.access_token; guestUser = guestData.user; localStorage.setItem('guest_token', guestToken!); localStorage.setItem('guest_user', JSON.stringify(guestUser)); } // 2. Tải ảnh lên const formData = new FormData(); formData.append('images', processedFile); if (location) { formData.append('latitude', location.coords.latitude.toString()); formData.append('longitude', location.coords.longitude.toString()); } let uploadRes = await fetch('/api/v1/photos/upload-anonymous', { method: 'POST', headers: { 'Authorization': `Bearer ${guestToken!}` }, body: formData, }); if (uploadRes.status === 401) { console.warn('Guest token invalid or expired. Creating a new guest user and retrying...'); 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(); guestToken = guestData.access_token; guestUser = guestData.user; localStorage.setItem('guest_token', guestToken!); localStorage.setItem('guest_user', JSON.stringify(guestUser)); uploadRes = await fetch('/api/v1/photos/upload-anonymous', { method: 'POST', headers: { 'Authorization': `Bearer ${guestToken!}` }, 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' }); // Cập nhật lại danh sách ảnh lập tức await fetchPublicPhotos(); setCurrentBgIndex(0); // Đăng nhập luôn bằng tài khoản khách này để người dùng xem được ảnh của mình trên bản đồ localStorage.setItem('token', guestToken!); localStorage.setItem('user', JSON.stringify(guestUser)); if (onLoginSuccess) { onLoginSuccess(guestUser); } } 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 = ''; } }; return (
{/* Background Image - Hiển thị trên mọi thiết bị với hiệu ứng Crossfade & Panning & Swiping */}
{/* Slot 1 */} {bg1 && ( { if (!isLoggedIn) e.preventDefault(); }} onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }} className={`absolute inset-0 h-full w-full object-cover transition-opacity duration-[1200ms] ease-in-out animate-panning ${ !isLoggedIn ? 'select-none pointer-events-none' : '' }`} style={{ opacity: fade1 ? 0.8 : 0, }} alt="Travel Background 1" /> )} {/* Slot 2 */} {bg2 && ( { if (!isLoggedIn) e.preventDefault(); }} onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }} className={`absolute inset-0 h-full w-full object-cover transition-opacity duration-[1200ms] ease-in-out animate-panning ${ !isLoggedIn ? 'select-none pointer-events-none' : '' }`} style={{ opacity: fade2 ? 0.8 : 0, }} alt="Travel Background 2" /> )}
{/* Keyframes cho hiệu ứng panning từ trái sang phải */} {/* Top Bar - Thanh điều hướng trên cùng */}
YoTrip
{/* Language Selector */} {/* Theme Selector */}
{/* 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 */} {/* 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)} />
); };