fix: lỗi hiển thị ở frontend
This commit is contained in:
+229
-126
@@ -1,10 +1,12 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { LogIn, Compass, Map as MapIcon, Camera } from 'lucide-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;
|
||||
@@ -14,33 +16,7 @@ interface LandingPageProps {
|
||||
isInitialSetup?: boolean;
|
||||
}
|
||||
|
||||
// Component Animation cho người lữ hành
|
||||
const TravelerAnimation = ({ onClick }: { onClick?: () => void }) => {
|
||||
return <button
|
||||
onClick={onClick}
|
||||
className="relative w-48 h-48 flex items-center justify-center transition-transform active:scale-95 focus:outline-none"
|
||||
style={{
|
||||
animation: 'gentle-shake 5s ease-in-out infinite',
|
||||
}}
|
||||
aria-label="Khám phá bản đồ"
|
||||
>
|
||||
{/* Giảm kích thước icon xuống 75% (w-24 -> w-18, md:w-28 -> md:w-21) */}
|
||||
<Camera className="w-18 h-18 md:w-21 md:h-21 text-white/80 drop-shadow-2xl" strokeWidth={1.5} />
|
||||
{/* Keyframes cho animation rung lắc */}
|
||||
<style>{`
|
||||
@keyframes gentle-shake {
|
||||
0%, 100% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(2deg) scale(1.05);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</button>;
|
||||
};
|
||||
|
||||
export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSignup, onGoToMap, onLoginSuccess, isInitialSetup }) => {
|
||||
export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMap, onLoginSuccess }) => {
|
||||
const [isLoginModalOpen, setIsLoginModalOpen] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const notify = useNotification();
|
||||
@@ -49,6 +25,8 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
|
||||
const [publicPhotos, setPublicPhotos] = useState<any[]>([]);
|
||||
const [trustedUsers, setTrustedUsers] = useState<any[]>([]);
|
||||
const [blacklist, setBlacklist] = useState<any[]>([]);
|
||||
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||
const [currentBgIndex, setCurrentBgIndex] = useState(0);
|
||||
const [bg1, setBg1] = useState('/background.avif');
|
||||
const [bg2, setBg2] = useState('');
|
||||
@@ -56,6 +34,39 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
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;
|
||||
@@ -105,9 +116,22 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
}
|
||||
};
|
||||
|
||||
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(() => {
|
||||
@@ -125,8 +149,10 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
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(file);
|
||||
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;
|
||||
@@ -228,43 +254,67 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
|
||||
return (
|
||||
<div className="h-dvh w-full overflow-hidden font-sans bg-gray-900 relative">
|
||||
{/* Background Image - Hiển thị trên mọi thiết bị với hiệu ứng Crossfade & Panning */}
|
||||
<div className="absolute inset-0 z-0 bg-gray-950 overflow-hidden">
|
||||
{/* Slot 1 */}
|
||||
{bg1 && (
|
||||
<img
|
||||
key={bg1}
|
||||
src={bg1}
|
||||
className="absolute inset-y-0 left-0 h-full w-[120%] max-w-none object-cover transition-opacity duration-[1200ms] ease-in-out animate-panning"
|
||||
style={{
|
||||
opacity: fade1 ? 0.8 : 0,
|
||||
}}
|
||||
alt="Travel Background 1"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Slot 2 */}
|
||||
{bg2 && (
|
||||
<img
|
||||
key={bg2}
|
||||
src={bg2}
|
||||
className="absolute inset-y-0 left-0 h-full w-[120%] max-w-none object-cover transition-opacity duration-[1200ms] ease-in-out animate-panning"
|
||||
style={{
|
||||
opacity: fade2 ? 0.8 : 0,
|
||||
}}
|
||||
alt="Travel Background 2"
|
||||
/>
|
||||
)}
|
||||
{/* Background Image - Hiển thị trên mọi thiết bị với hiệu ứng Crossfade & Panning & Swiping */}
|
||||
<div
|
||||
className="absolute inset-0 z-0 bg-gray-950 overflow-hidden"
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0 left-[-15%] right-[-15%] cursor-grab active:cursor-grabbing"
|
||||
style={{
|
||||
transform: `translateX(${touchOffsetX}px)`,
|
||||
transition: touchOffsetX === 0 ? 'transform 0.3s ease-out' : 'none',
|
||||
touchAction: 'none'
|
||||
}}
|
||||
>
|
||||
{/* Slot 1 */}
|
||||
{bg1 && (
|
||||
<img
|
||||
key={bg1}
|
||||
src={bg1}
|
||||
draggable="false"
|
||||
onContextMenu={(e) => { 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 && (
|
||||
<img
|
||||
key={bg2}
|
||||
src={bg2}
|
||||
draggable="false"
|
||||
onContextMenu={(e) => { 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"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Keyframes cho hiệu ứng panning từ trái sang phải */}
|
||||
<style>{`
|
||||
@keyframes pan-left-to-right {
|
||||
0% {
|
||||
transform: translateX(0) translateZ(0);
|
||||
transform: scale(1.08) translate(0, 0) translateZ(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-16.667%) translateZ(0);
|
||||
transform: scale(1.16) translate(-1%, 0.5%) translateZ(0);
|
||||
}
|
||||
}
|
||||
.animate-panning {
|
||||
@@ -274,18 +324,18 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
`}</style>
|
||||
|
||||
{/* Top Bar - Thanh điều hướng trên cùng */}
|
||||
<div className="absolute top-0 left-0 right-0 z-20 p-4 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-2 text-white drop-shadow-lg">
|
||||
<Compass className="w-8 h-8" />
|
||||
<span className="text-xl font-black tracking-tighter uppercase hidden sm:block">YoTrip</span>
|
||||
<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">
|
||||
<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-3 py-1.5 text-xs font-bold focus:outline-none backdrop-blur-md cursor-pointer"
|
||||
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>
|
||||
@@ -296,7 +346,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
<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-3 py-1.5 text-xs font-bold focus:outline-none backdrop-blur-md cursor-pointer"
|
||||
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>
|
||||
@@ -304,11 +354,19 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={() => setIsLoginModalOpen(true)}
|
||||
className="flex items-center justify-center gap-2 bg-white/15 backdrop-blur-md text-white font-bold py-2 px-4 rounded-full border border-white/25 hover:bg-white/25 transition-all shadow-md active:scale-95"
|
||||
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"
|
||||
>
|
||||
<LogIn className="w-4 h-4" />
|
||||
<span className="text-sm">{t('login')}</span>
|
||||
<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>
|
||||
</div>
|
||||
@@ -349,6 +407,38 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
</div>
|
||||
</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">
|
||||
<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">
|
||||
Lý 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">
|
||||
@@ -367,12 +457,6 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body: Animation người lữ hành */}
|
||||
<div className="absolute inset-0 flex items-center justify-center z-10" >
|
||||
{/* Kích hoạt click vào file input để chọn hoặc chụp ảnh */}
|
||||
<TravelerAnimation onClick={() => fileInputRef.current?.click()} />
|
||||
</div>
|
||||
|
||||
{/* Input chọn file ẩn để chụp/chọn ảnh */}
|
||||
<input
|
||||
type="file"
|
||||
@@ -383,59 +467,72 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{/* Community Gallery Previews */}
|
||||
{publicPhotos.length > 0 && (
|
||||
<div className="absolute bottom-[calc(7.5rem+env(safe-area-inset-bottom,0px))] left-0 right-0 z-20 px-4 flex flex-col items-center gap-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-white/70 drop-shadow-md">
|
||||
{t('momentsTitle')} ({publicPhotos.length})
|
||||
</span>
|
||||
<div className="flex gap-3 overflow-x-auto max-w-full no-scrollbar pb-2 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" className="w-full h-full object-cover" />
|
||||
{/* Border Overlay absolute to prevent border clipping or corner overlap */}
|
||||
<div className={`absolute inset-0 rounded-xl border-2 pointer-events-none transition-colors ${
|
||||
currentBgIndex === index ? 'border-emerald-500' : 'border-white/20'
|
||||
}`} />
|
||||
</button>
|
||||
))}
|
||||
{/* 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>
|
||||
|
||||
<style>{`
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
`}</style>
|
||||
)}
|
||||
|
||||
{/* 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={() => fileInputRef.current?.click()}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* Bottom Bar - Nút hành động chính */}
|
||||
<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 sm:flex-row gap-3 items-center justify-center max-w-lg">
|
||||
<button
|
||||
onClick={onGoToMap}
|
||||
className="w-full flex items-center justify-center gap-3 bg-emerald-600/90 text-white font-bold py-3.5 px-6 rounded-2xl transition-all shadow-2xl border border-emerald-500/30 hover:bg-emerald-500 hover:scale-[1.02] active:scale-95 text-sm"
|
||||
>
|
||||
<MapIcon className="w-5 h-5" />
|
||||
{t('exploreToursBtn')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="w-full flex items-center justify-center gap-3 bg-white/20 backdrop-blur-lg text-white font-bold py-3.5 px-6 rounded-2xl transition-all shadow-2xl border border-white/30 hover:bg-white/30 hover:scale-[1.02] active:scale-95 text-sm"
|
||||
>
|
||||
<Camera className="w-5 h-5" />
|
||||
{t('quickCamera')}
|
||||
</button>
|
||||
|
||||
<style>{`
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
|
||||
{/* Login Modal Component */}
|
||||
@@ -445,6 +542,12 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
||||
onSwitchToSignup={onGoToSignup}
|
||||
onLoginSuccess={onLoginSuccess}
|
||||
/>
|
||||
|
||||
{/* Report Business Modal */}
|
||||
<ReportBusinessModal
|
||||
isOpen={isReportModalOpen}
|
||||
onClose={() => setIsReportModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user