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
+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">