fix: gộp các nút thành viên và bỏ MemberDashboard
This commit is contained in:
+195
-153
@@ -1,12 +1,17 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { LogIn, Compass, Map as MapIcon, Camera, ShieldAlert, Image as ImageIcon, X } from 'lucide-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 { useTheme } from '../hooks/useTheme';
|
||||
import { compressImage } from '../utils/image';
|
||||
|
||||
interface LandingPageProps {
|
||||
@@ -15,10 +20,27 @@ interface LandingPageProps {
|
||||
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 }) => {
|
||||
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);
|
||||
@@ -29,8 +51,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
||||
const [pendingPhotoLocation, setPendingPhotoLocation] = useState<GeolocationPosition | null>(null);
|
||||
const [photoPreviewUrl, setPhotoPreviewUrl] = useState<string>('');
|
||||
const notify = useNotification();
|
||||
const { t, lang, changeLanguage } = useTranslation();
|
||||
const { theme, changeTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [publicPhotos, setPublicPhotos] = useState<any[]>([]);
|
||||
const [trustedUsers, setTrustedUsers] = useState<any[]>([]);
|
||||
@@ -112,7 +133,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
||||
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) {
|
||||
@@ -121,7 +142,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
||||
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) {
|
||||
@@ -130,7 +151,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
||||
document.head.appendChild(ogTitle);
|
||||
}
|
||||
ogTitle.setAttribute('content', photoTitle);
|
||||
|
||||
|
||||
// Update og:description
|
||||
let ogDescription = document.querySelector('meta[property="og:description"]');
|
||||
if (!ogDescription) {
|
||||
@@ -139,7 +160,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
||||
document.head.appendChild(ogDescription);
|
||||
}
|
||||
ogDescription.setAttribute('content', photoDescription);
|
||||
|
||||
|
||||
// Update twitter:image
|
||||
let twitterImage = document.querySelector('meta[name="twitter:image"]');
|
||||
if (!twitterImage) {
|
||||
@@ -168,9 +189,9 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
||||
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);
|
||||
@@ -201,11 +222,11 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
||||
// 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' });
|
||||
@@ -222,24 +243,30 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
||||
notify({ title: 'Đang tải lên...', message: 'Vui lòng chờ trong giây lát.', type: 'info' });
|
||||
|
||||
try {
|
||||
// 1. Xóa token cũ nếu có để đảm bảo guest không bị nhầm lẫn
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
|
||||
// 2. 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));
|
||||
// 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);
|
||||
@@ -251,31 +278,29 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
||||
if (selectedTags.length > 0) {
|
||||
formData.append('tags', JSON.stringify(selectedTags));
|
||||
}
|
||||
|
||||
|
||||
let uploadRes = await fetch('/api/v1/photos/upload-anonymous', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${guestToken!}` },
|
||||
headers: { 'Authorization': `Bearer ${uploadToken!}` },
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (uploadRes.status === 401) {
|
||||
|
||||
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');
|
||||
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!);
|
||||
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 ${guestToken!}` },
|
||||
headers: { 'Authorization': `Bearer ${newGuestToken!}` },
|
||||
body: formData,
|
||||
});
|
||||
}
|
||||
@@ -284,7 +309,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
||||
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
|
||||
@@ -317,38 +342,36 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
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
|
||||
<img
|
||||
key={bg1}
|
||||
src={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"
|
||||
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
|
||||
<img
|
||||
key={bg2}
|
||||
src={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"
|
||||
className={`h-full image-pan-element ${!isLoggedIn ? 'select-none pointer-events-none' : ''
|
||||
}`}
|
||||
alt="Travel Background 2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -407,11 +430,10 @@ return (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setCurrentBgIndex(idx)}
|
||||
className={`w-2 h-2 rounded-full transition-all ${
|
||||
currentBgIndex === idx
|
||||
? 'bg-emerald-500 w-6'
|
||||
className={`w-2 h-2 rounded-full transition-all ${currentBgIndex === idx
|
||||
? 'bg-emerald-500 w-6'
|
||||
: 'bg-white/40 hover:bg-white/60'
|
||||
}`}
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -423,58 +445,34 @@ return (
|
||||
<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-1.5 sm:gap-3">
|
||||
{/* Language Selector */}
|
||||
<select
|
||||
value={lang}
|
||||
onChange={(e) => changeLanguage(e.target.value as any)}
|
||||
className="bg-black/40 hover:bg-black/60 border border-white/20 text-white rounded-full px-2 py-1 sm:px-3 sm:py-1.5 text-[11px] sm:text-xs font-bold focus:outline-none backdrop-blur-md cursor-pointer max-w-[80px] sm:max-w-none"
|
||||
>
|
||||
<option value="vi" className="text-black">Tiếng Việt</option>
|
||||
<option value="en" className="text-black">English</option>
|
||||
<option value="zh" className="text-black">中文</option>
|
||||
</select>
|
||||
|
||||
{/* Theme Selector */}
|
||||
<select
|
||||
value={theme}
|
||||
onChange={(e) => changeTheme(e.target.value as any)}
|
||||
className="bg-black/40 hover:bg-black/60 border border-white/20 text-white rounded-full px-2 py-1 sm:px-3 sm:py-1.5 text-[11px] sm:text-xs font-bold focus:outline-none backdrop-blur-md cursor-pointer max-w-[80px] sm:max-w-none"
|
||||
>
|
||||
<option value="light" className="text-black">{t('themeLight') || 'Sáng'}</option>
|
||||
<option value="dark" className="text-black">{t('themeDark') || 'Tối'}</option>
|
||||
<option value="system" className="text-black">{t('themeSystem') || 'Hệ thống'}</option>
|
||||
</select>
|
||||
|
||||
{/* Android APK Download Button */}
|
||||
<a
|
||||
href={`${import.meta.env.VITE_BACKEND_URL || 'https://yotrip.labz.io.vn'}/downloads/yotrip-latest.apk`}
|
||||
download="yotrip.apk"
|
||||
className="flex items-center justify-center gap-1.5 bg-green-600/80 backdrop-blur-md text-white font-bold py-1.5 px-2.5 sm:py-2 sm:px-4 rounded-full border border-green-500/30 hover:bg-green-500 transition-all shadow-md active:scale-95 text-xs sm:text-sm cursor-pointer shrink-0"
|
||||
title="Tải ứng dụng Android (.APK)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="w-3.5 h-3.5 sm:w-4 sm:h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Tải bản Android (.APK)</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={() => setIsReportModalOpen(true)}
|
||||
className="flex items-center justify-center gap-1.5 bg-red-600/80 backdrop-blur-md text-white font-bold py-1.5 px-2.5 sm:py-2 sm:px-3.5 rounded-full border border-red-500/30 hover:bg-red-500 transition-all shadow-md active:scale-95 text-xs sm:text-sm cursor-pointer"
|
||||
>
|
||||
<ShieldAlert className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
<span className="hidden sm:inline">{t('reportBusinessBtn')}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setIsLoginModalOpen(true)}
|
||||
className="flex items-center justify-center gap-1 sm:gap-2 bg-white/15 backdrop-blur-md text-white font-bold py-1.5 px-2.5 sm:py-2 sm:px-4 rounded-full border border-white/25 hover:bg-white/25 transition-all shadow-md active:scale-95 text-xs sm:text-sm cursor-pointer"
|
||||
>
|
||||
<LogIn className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
<span>{t('login')}</span>
|
||||
</button>
|
||||
<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>
|
||||
|
||||
@@ -515,7 +513,7 @@ return (
|
||||
</div>
|
||||
|
||||
{/* Floating Blacklist Panel (Right Side on Desktop) */}
|
||||
<div className="absolute right-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-right duration-500 animate-out duration-300">
|
||||
<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>
|
||||
@@ -565,32 +563,32 @@ return (
|
||||
)}
|
||||
|
||||
{/* 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}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
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 */}
|
||||
@@ -606,25 +604,22 @@ return (
|
||||
<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'
|
||||
}`}
|
||||
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"
|
||||
<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' : ''
|
||||
}`}
|
||||
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'
|
||||
}`} />
|
||||
<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>
|
||||
@@ -633,7 +628,7 @@ return (
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="w-full flex gap-3 items-center justify-center">
|
||||
<button
|
||||
<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"
|
||||
>
|
||||
@@ -641,7 +636,7 @@ return (
|
||||
<span>{t('shortExplore') || 'Khám phá'}</span>
|
||||
</button>
|
||||
|
||||
<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"
|
||||
>
|
||||
@@ -649,7 +644,7 @@ return (
|
||||
<span>{t('shortCamera') || 'Chụp ảnh'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<style>{`
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
@@ -662,9 +657,9 @@ return (
|
||||
</div>
|
||||
|
||||
{/* Login Modal Component */}
|
||||
<LoginModal
|
||||
isOpen={isLoginModalOpen}
|
||||
onClose={() => setIsLoginModalOpen(false)}
|
||||
<LoginModal
|
||||
isOpen={isLoginModalOpen}
|
||||
onClose={() => setIsLoginModalOpen(false)}
|
||||
onSwitchToSignup={onGoToSignup}
|
||||
onLoginSuccess={onLoginSuccess}
|
||||
/>
|
||||
@@ -745,6 +740,53 @@ return (
|
||||
</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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user