fix: lướt xem ảnh ở màn hình index của người dùng public

This commit is contained in:
2026-06-22 22:16:51 +07:00
parent ac91f6e4c8
commit a65be48d36
8 changed files with 202 additions and 115 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -6,8 +6,8 @@
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<script src="https://accounts.google.com/gsi/client" async defer></script>
<title>Travel Planner</title>
<script type="module" crossorigin src="/assets/index-BX4qY0bE.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-pT9caalc.css">
<script type="module" crossorigin src="/assets/index-Chfn55jq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D_50XTpY.css">
</head>
<body>
<div id="root"></div>
+93 -95
View File
@@ -34,36 +34,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGoToSignup, onGoToMa
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 panScale = 1.25;
const maxPanX = typeof window !== 'undefined' ? -(window.innerWidth * (panScale - 1)) : -250;
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;
// Allow panning within bounds (for panorama viewing)
const clampedX = Math.max(maxPanX, Math.min(0, diffX));
setTouchOffsetX(clampedX);
};
const handleTouchEnd = () => {
if (!isSwipingRef.current) return;
isSwipingRef.current = false;
// Reset panning position when touch ends
setTouchOffsetX(0);
};
const isLoggedIn = !!localStorage.getItem('token') && !localStorage.getItem('guest_token');
useEffect(() => {
const nextUrl = publicPhotos.length > 0 ? publicPhotos[currentBgIndex]?.imageUrl : '/background.avif';
@@ -253,79 +224,106 @@ notify({ title: 'Thành công!', message: 'Ảnh của bạn đã được tải
};
return (
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 & 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-0 right-0 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 ${
!isLoggedIn ? 'select-none pointer-events-none' : ''
}`}
style={{
opacity: fade1 ? 0.8 : 0,
transform: `scale(${panScale}) translateX(-${(panScale - 1) * 50}%)`,
}}
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 ${
!isLoggedIn ? 'select-none pointer-events-none' : ''
}`}
style={{
opacity: fade2 ? 0.8 : 0,
transform: `scale(${panScale}) translateX(-${(panScale - 1) * 50}%)`,
}}
alt="Travel Background 2"
/>
)}
</div>
{/* 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>
{/* Keyframes cho hiệu ứng panning từ trái sang phải */}
{/* Style for horizontal panning */}
<style>{`
@keyframes pan-left-to-right {
0% {
transform: scale(1.08) translate(0, 0) translateZ(0);
}
100% {
transform: scale(1.16) translate(-1%, 0.5%) translateZ(0);
}
.image-pan-container {
width: 100%;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
.animate-panning {
animation: pan-left-to-right 13500ms linear forwards;
will-change: transform;
.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">