Files
travelplanning/frontend/src/pages/LandingPage.tsx
T

792 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<LandingPageProps> = ({
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<string | null>(null);
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false);
const [isPhotoSourceModalOpen, setIsPhotoSourceModalOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const cameraInputRef = useRef<HTMLInputElement>(null);
const galleryInputRef = useRef<HTMLInputElement>(null);
const [pendingPhotoFile, setPendingPhotoFile] = useState<File | null>(null);
const [pendingPhotoLocation, setPendingPhotoLocation] = useState<GeolocationPosition | null>(null);
const [photoPreviewUrl, setPhotoPreviewUrl] = useState<string>('');
const notify = useNotification();
const { t } = useTranslation();
const [publicPhotos, setPublicPhotos] = useState<any[]>([]);
const [trustedUsers, setTrustedUsers] = useState<any[]>([]);
const [blacklist, setBlacklist] = useState<any[]>([]);
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<HTMLInputElement>) => {
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<GeolocationPosition | null>((resolve) => {
if (!navigator.geolocation) {
resolve(null);
} else {
navigator.geolocation.getCurrentPosition(
(pos) => resolve(pos),
() => resolve(null),
{ timeout: 4000, enableHighAccuracy: true }
);
}
}),
new Promise<null>((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 (
<div className="h-dvh w-full overflow-hidden font-sans bg-[var(--background)] relative">
{/* Background Image with Horizontal Panning */}
<div className="absolute inset-0 z-0">
{/* Active image for panning */}
<div className={`absolute inset-0 image-pan-container ${activeSlot === 1 && fade1 ? 'block' : 'hidden'}`}>
{bg1 && (
<img
key={bg1}
src={bg1}
draggable="false"
onContextMenu={(e) => { 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"
/>
)}
</div>
<div className={`absolute inset-0 image-pan-container ${activeSlot === 2 && fade2 ? 'block' : 'hidden'}`}>
{bg2 && (
<img
key={bg2}
src={bg2}
draggable="false"
onContextMenu={(e) => { 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"
/>
)}
</div>
</div>
{/* Style for horizontal panning */}
<style>{`
.image-pan-container {
width: 100%;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
.image-pan-container::-webkit-scrollbar {
display: none;
}
.image-pan-container {
-ms-overflow-style: none;
scrollbar-width: none;
}
.image-pan-element {
width: auto;
max-width: none !important;
height: 100%;
display: block;
min-width: 100%;
}
`}</style>
{/* Swipe Indicator Arrows - for image switching */}
{publicPhotos.length > 1 && (
<>
<button
onClick={() => setCurrentBgIndex((prev) => (prev - 1 + publicPhotos.length) % publicPhotos.length)}
className="absolute left-4 top-1/2 -translate-y-1/2 z-10 p-2 bg-black/30 hover:bg-black/50 backdrop-blur-md rounded-full text-white transition-all md:hidden"
aria-label="Previous photo"
>
<span className="text-xl"></span>
</button>
<button
onClick={() => setCurrentBgIndex((prev) => (prev + 1) % publicPhotos.length)}
className="absolute right-4 top-1/2 -translate-y-1/2 z-10 p-2 bg-black/30 hover:bg-black/50 backdrop-blur-md rounded-full text-white transition-all md:hidden"
aria-label="Next photo"
>
<span className="text-xl"></span>
</button>
</>
)}
{/* Photo Progress Dots */}
{publicPhotos.length > 1 && (
<div className="absolute bottom-20 left-1/2 -translate-x-1/2 z-10 flex gap-1.5">
{publicPhotos.slice(0, 20).map((_, idx) => (
<button
key={idx}
onClick={() => setCurrentBgIndex(idx)}
className={`w-2 h-2 rounded-full transition-all ${currentBgIndex === idx
? 'bg-emerald-500 w-6'
: 'bg-white/40 hover:bg-white/60'
}`}
/>
))}
</div>
)}
{/* Top Bar - Thanh điều hướng trên cùng */}
<div className="absolute top-0 left-0 right-0 z-20 p-3 sm:p-4 pt-[calc(0.75rem+env(safe-area-inset-top,0px))] sm:pt-[calc(1rem+env(safe-area-inset-top,0px))] flex justify-between items-center bg-gradient-to-b from-slate-950/40 to-transparent">
<div className="flex items-center gap-1.5 sm:gap-2 text-white drop-shadow-lg">
<Compass className="w-7 h-7 sm:w-8 sm:h-8" />
<span className="text-lg sm:text-xl font-black tracking-tighter uppercase hidden sm:block">YoTrip</span>
</div>
<div className="flex items-center gap-3 pointer-events-auto">
{user && !localStorage.getItem('guest_token') && (
<button
onClick={() => {
setChatTargetUserId(null);
setIsChatOpen(true);
}}
className="w-11 h-11 bg-slate-900 border border-slate-800 rounded-full flex items-center justify-center shadow-xl hover:bg-slate-805 text-slate-300 hover:text-white transition-all active:scale-95 cursor-pointer shrink-0 relative group"
title="Trò chuyện trực tiếp"
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 group-hover:scale-105 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-blue-500 rounded-full" />
</button>
)}
<MapProfileDropdown
user={user}
onLogout={onLogout}
onOpenSettings={() => setIsProfileSettingsOpen(true)}
onOpenCreateTour={() => onGoToMap?.()}
onOpenReport={() => setIsReportModalOpen(true)}
onOpenLogin={() => setIsLoginModalOpen(true)}
onOpenMyPhotos={() => setIsMyPhotosOpen(true)}
onOpenMyTours={() => setIsMyToursOpen(true)}
onOpenFriends={() => setIsFriendsOpen(true)}
/>
</div>
</div>
{/* Floating Trusted Leaderboard Panel (Left Side on Desktop) */}
<div className="absolute left-6 top-24 bottom-36 z-20 hidden md:flex flex-col w-72 bg-slate-900/60 backdrop-blur-md border border-white/10 rounded-[32px] p-5 text-white overflow-hidden shadow-2xl animate-in slide-in-from-left duration-500">
<h3 className="text-base font-black flex items-center gap-2 mb-3 border-b border-white/10 pb-2">
🏆 {t('trustedMembers')}
</h3>
<div className="flex-1 overflow-y-auto space-y-3 pr-1 no-scrollbar">
{trustedUsers.length === 0 ? (
<div className="text-xs text-white/50 italic py-4">Chưa đánh giá nào.</div>
) : (
trustedUsers.map((u, idx) => (
<div key={idx} className="flex items-center gap-3 bg-white/5 hover:bg-white/10 p-2.5 rounded-2xl border border-white/5 transition-all">
<div className="relative">
<div className="w-10 h-10 rounded-full bg-slate-800 border border-white/10 flex items-center justify-center font-bold overflow-hidden">
{u.avatar ? (
<img src={u.avatar} alt="Avatar" className="w-full h-full object-cover" />
) : (
<span>{u.name.charAt(0)}</span>
)}
</div>
<span className="absolute -top-1 -left-1 bg-amber-500 text-[10px] text-white font-black px-1.5 py-0.5 rounded-full border border-slate-950">
{idx + 1}
</span>
</div>
<div className="flex-1 min-w-0">
<div className="text-xs font-bold truncate">{u.name}</div>
<div className="flex items-center gap-1.5 mt-0.5">
<span className="text-[10px] text-amber-400 font-bold"> {u.averageScore}</span>
<span className="text-[9px] text-white/40 font-semibold">({u.ratingCount})</span>
</div>
</div>
</div>
))
)}
</div>
</div>
{/* Floating Blacklist Panel (Right Side on Desktop) */}
<div className="absolute right-6 top-24 bottom-36 z-10 hidden md:flex flex-col w-72 bg-slate-900/60 backdrop-blur-md border border-white/10 rounded-[32px] p-5 text-white overflow-hidden shadow-2xl animate-in slide-in-from-right duration-500 animate-out duration-300">
<h3 className="text-base font-black flex items-center gap-2 mb-3 border-b border-white/10 pb-2 text-red-400">
<ShieldAlert className="w-5 h-5 text-red-500" /> {t('blacklistTitle')}
</h3>
<div className="flex-1 overflow-y-auto space-y-3 pr-1 no-scrollbar">
{blacklist.length === 0 ? (
<div className="text-xs text-white/50 italic py-4 text-center">{t('emptyBlacklist')}</div>
) : (
blacklist.map((item, idx) => (
<div key={idx} className="flex flex-col gap-1.5 bg-white/5 hover:bg-white/10 p-3 rounded-2xl border border-white/5 transition-all text-left">
<div className="flex items-start justify-between gap-2">
<div className="text-xs font-bold text-red-300 truncate flex-1">{item.name}</div>
<span className="text-[8px] bg-red-950/80 text-red-400 border border-red-900/50 px-1.5 py-0.5 rounded font-black shrink-0">
{item.type}
</span>
</div>
{item.address && (
<div className="text-[10px] text-white/60 truncate">{item.address}</div>
)}
{item.phone && (
<div className="text-[10px] text-white/40">SĐT: {item.phone}</div>
)}
<div className="text-[10px] text-red-400/90 italic bg-red-950/30 p-1.5 rounded-lg border border-red-950/40">
do: {item.reason}
</div>
</div>
))
)}
</div>
</div>
{/* Mobile Only: Trusted Members Top Bar */}
{trustedUsers.length > 0 && (
<div className="absolute top-24 left-4 right-4 z-20 md:hidden flex flex-col gap-1 bg-slate-950/45 backdrop-blur-sm p-2 rounded-2xl border border-white/5">
<span className="text-[9px] font-black uppercase tracking-wider text-white/70 px-1">
🏆 {t('trustedMembers')}
</span>
<div className="flex gap-2 overflow-x-auto no-scrollbar py-0.5">
{trustedUsers.slice(0, 5).map((u, idx) => (
<div key={idx} className="flex items-center gap-1.5 bg-slate-950/60 border border-white/10 px-2.5 py-1 rounded-full shrink-0">
<span className="text-[9px] font-bold text-amber-400">#{idx + 1}</span>
<span className="text-[10px] font-bold text-white truncate max-w-[70px]">{u.name}</span>
<span className="text-[9px] text-amber-400 font-bold"> {u.averageScore}</span>
</div>
))}
</div>
</div>
)}
{/* Input chọn file ẩn để chụp/chọn ảnh */}
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
accept="image/*"
capture="environment"
className="hidden"
/>
{/* Camera input - capture="environment" for rear camera */}
<input
type="file"
ref={cameraInputRef}
onChange={handleFileChange}
accept="image/*"
capture="environment"
className="hidden"
/>
{/* Gallery input - no capture attribute for file picker */}
<input
type="file"
ref={galleryInputRef}
onChange={handleFileChange}
accept="image/*"
className="hidden"
/>
{/* Unified Bottom Container: Gallery thumbnails on top, action buttons below */}
<div className="absolute bottom-[calc(1.5rem+env(safe-area-inset-bottom,0px))] left-1/2 -translate-x-1/2 z-20 w-full px-4 flex flex-col items-center gap-4 max-w-md">
{/* Community Gallery Previews */}
{publicPhotos.length > 0 && (
<div className="w-full flex flex-col items-center gap-2">
<span className="text-[10px] font-black uppercase tracking-wider text-white/75 drop-shadow-md">
{t('momentsTitle')} ({publicPhotos.length})
</span>
<div className="flex gap-3 overflow-x-auto max-w-full no-scrollbar pb-1 px-4 justify-center">
{publicPhotos.slice(0, 8).map((photo, index) => (
<button
key={photo.id}
onClick={() => setCurrentBgIndex(index)}
className={`relative w-14 h-14 rounded-xl overflow-hidden transition-all active:scale-95 shrink-0 shadow-inner bg-slate-800 ${currentBgIndex === index ? 'scale-110 shadow-lg' : 'hover:scale-105'
}`}
>
<img
src={photo.imageUrl}
alt="Community thumbnail"
draggable="false"
onContextMenu={(e) => { if (!isLoggedIn) e.preventDefault(); }}
onDragStart={(e) => { if (!isLoggedIn) e.preventDefault(); }}
className={`w-full h-full object-cover ${!isLoggedIn ? 'select-none pointer-events-none' : ''
}`}
/>
{/* Border Overlay absolute to prevent border clipping or corner overlap */}
{/* Inset by 1.5px so it does not get clipped by parent overflow-hidden border */}
<div className={`absolute inset-[1.5px] rounded-[10px] border-2 pointer-events-none transition-colors ${currentBgIndex === index ? 'border-emerald-500' : 'border-white/20'
}`} />
</button>
))}
</div>
</div>
)}
{/* Buttons */}
<div className="w-full flex gap-3 items-center justify-center">
<button
onClick={onGoToMap}
className="flex-1 flex items-center justify-center gap-2 bg-emerald-600/90 hover:bg-emerald-500 text-white font-bold py-3.5 rounded-2xl transition-all shadow-2xl border border-emerald-500/30 active:scale-95 text-xs sm:text-sm whitespace-nowrap"
>
<MapIcon className="w-4.5 h-4.5" />
<span>{t('shortExplore') || 'Khám phá'}</span>
</button>
<button
onClick={() => setIsPhotoSourceModalOpen(true)}
className="flex-1 flex items-center justify-center gap-2 bg-white/20 backdrop-blur-lg hover:bg-white/30 text-white font-bold py-3.5 rounded-2xl transition-all shadow-2xl border border-white/30 active:scale-95 text-xs sm:text-sm whitespace-nowrap"
>
<Camera className="w-4.5 h-4.5" />
<span>{t('shortCamera') || 'Chụp ảnh'}</span>
</button>
</div>
<style>{`
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
`}</style>
</div>
{/* Login Modal Component */}
<LoginModal
isOpen={isLoginModalOpen}
onClose={() => setIsLoginModalOpen(false)}
onSwitchToSignup={onGoToSignup}
onLoginSuccess={onLoginSuccess}
/>
{/* Report Business Modal */}
<ReportBusinessModal
isOpen={isReportModalOpen}
onClose={() => setIsReportModalOpen(false)}
/>
{/* Tag Select Modal */}
<TagSelectModal
isOpen={isTagsModalOpen}
onClose={() => {
setIsTagsModalOpen(false);
setPendingPhotoFile(null);
setPendingPhotoLocation(null);
if (photoPreviewUrl) {
URL.revokeObjectURL(photoPreviewUrl);
setPhotoPreviewUrl('');
}
}}
onConfirm={handleConfirmTags}
photoUrl={photoPreviewUrl}
/>
{/* Photo Source Selection Modal */}
{isPhotoSourceModalOpen && (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div className="bg-[var(--surface)] rounded-3xl shadow-2xl max-w-sm w-full overflow-hidden animate-in fade-in zoom-in-95 duration-200">
{/* Header */}
<div className="bg-gradient-to-r from-blue-600 to-blue-500 px-6 py-6 flex items-center justify-between">
<h2 className="text-xl font-bold text-white">Chụp hoặc tải nh</h2>
<button
onClick={() => setIsPhotoSourceModalOpen(false)}
className="p-1.5 hover:bg-white/20 rounded-full transition-colors"
>
<X className="w-5 h-5 text-white" />
</button>
</div>
{/* Content */}
<div className="p-6 space-y-3">
{/* Camera Button */}
<button
onClick={() => {
setIsPhotoSourceModalOpen(false);
cameraInputRef.current?.click();
}}
className="w-full flex items-center gap-4 p-4 bg-[var(--background)] hover:bg-[var(--background-alt)] border border-[var(--border)] rounded-2xl transition-all active:scale-95"
>
<div className="flex-shrink-0 p-3 bg-blue-100 rounded-full">
<Camera className="w-6 h-6 text-blue-600" />
</div>
<div className="flex-1 text-left">
<div className="font-bold text-[var(--text-primary)]">Chụp nh bằng camera</div>
<div className="text-sm text-[var(--text-muted)]">Dùng camera thiết bị của bạn</div>
</div>
</button>
{/* Gallery Button */}
<button
onClick={() => {
setIsPhotoSourceModalOpen(false);
galleryInputRef.current?.click();
}}
className="w-full flex items-center gap-4 p-4 bg-[var(--background)] hover:bg-[var(--background-alt)] border border-[var(--border)] rounded-2xl transition-all active:scale-95"
>
<div className="flex-shrink-0 p-3 bg-emerald-100 rounded-full">
<ImageIcon className="w-6 h-6 text-emerald-600" />
</div>
<div className="flex-1 text-left">
<div className="font-bold text-[var(--text-primary)]">Tải nh từ thư viện</div>
<div className="text-sm text-[var(--text-muted)]">Chọn nh từ thiết bị của bạn</div>
</div>
</button>
</div>
</div>
</div>
)}
{/* Profile Settings Modal */}
<ProfileSettingsModal
isOpen={isProfileSettingsOpen}
onClose={() => setIsProfileSettingsOpen(false)}
user={user}
onSaveSuccess={onLoginSuccess}
/>
{/* My Tours Modal */}
<MyToursModal
isOpen={isMyToursOpen}
onClose={() => setIsMyToursOpen(false)}
user={user}
onViewTour={(tourId) => {
if (onGoToMap) {
localStorage.setItem('viewTourOnLand', tourId);
onGoToMap();
}
}}
onOpenNavigation={onOpenNavigation}
/>
{/* My Photos Modal */}
<MyPhotosModal
isOpen={isMyPhotosOpen}
onClose={() => setIsMyPhotosOpen(false)}
user={user}
/>
{/* Live Chat Modal */}
<LiveChatModal
isOpen={isChatOpen}
onClose={() => setIsChatOpen(false)}
user={user}
defaultChatUserId={chatTargetUserId}
/>
{/* Friends Manager Modal */}
<FriendsManagerModal
isOpen={isFriendsOpen}
onClose={() => setIsFriendsOpen(false)}
user={user}
onOpenChatWithUser={(userId) => {
setChatTargetUserId(userId);
setIsChatOpen(true);
}}
/>
</div>
);
};