Update marina image to use v3 across all devices
Build And Push Image / docker (push) Successful in 2m6s
Details
Build And Push Image / docker (push) Successful in 2m6s
Details
Switched to marina_cropped_v3.jpg for both mobile and desktop layouts, showcasing the premium evening marina view with luxury yachts and resort amenities. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7c9aedb92d
commit
2d5ffcb268
Binary file not shown.
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 346 KiB |
188
src/app/page.tsx
188
src/app/page.tsx
|
|
@ -32,6 +32,16 @@ export default function Home() {
|
|||
const [contactTop, setContactTop] = useState(0);
|
||||
const [windowHeight, setWindowHeight] = useState(0);
|
||||
|
||||
// Orientation state management for smooth transitions
|
||||
const [orientation, setOrientation] = useState<'portrait' | 'landscape'>('landscape');
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
const transitionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const animationProgressRef = useRef(0);
|
||||
|
||||
// Refs for orientation to avoid stale closures in animation loops
|
||||
const orientationRef = useRef<'portrait' | 'landscape'>('landscape');
|
||||
const isTransitioningRef = useRef(false);
|
||||
|
||||
// Animation values as refs to avoid re-renders
|
||||
const logoPositionRef = useRef('center');
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
|
@ -50,6 +60,7 @@ export default function Home() {
|
|||
const imageContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const isTablet = useMediaQuery("(min-width: 769px) and (max-width: 1024px)");
|
||||
const isDesktop = useMediaQuery("(min-width: 1280px)");
|
||||
|
||||
// Manage form state
|
||||
|
|
@ -93,13 +104,13 @@ export default function Home() {
|
|||
|
||||
// Memoized logo dimensions based on screen size
|
||||
const logoWidth = useMemo(() =>
|
||||
isMobile ? 240 : isDesktop ? 316 : 280,
|
||||
[isMobile, isDesktop]
|
||||
isMobile ? 240 : isTablet ? 280 : isDesktop ? 316 : 280,
|
||||
[isMobile, isTablet, isDesktop]
|
||||
);
|
||||
|
||||
const logoHeight = useMemo(() =>
|
||||
isMobile ? 115 : isDesktop ? 151 : 134,
|
||||
[isMobile, isDesktop]
|
||||
isMobile ? 115 : isTablet ? 134 : isDesktop ? 151 : 134,
|
||||
[isMobile, isTablet, isDesktop]
|
||||
);
|
||||
|
||||
// Memoized animation constants
|
||||
|
|
@ -110,6 +121,14 @@ export default function Home() {
|
|||
finalScale: 0.5,
|
||||
fadeThreshold: 5
|
||||
},
|
||||
tablet: {
|
||||
targetTopPosition: 10, // Same as mobile - position higher up
|
||||
startYFactor: 0.3, // Start lower than center for smoother animation
|
||||
logoSpeed: 0.35, // Slightly slower than desktop
|
||||
finalScale: 0.5, // Same as mobile for consistency
|
||||
fadeThreshold: 8, // Between mobile and desktop
|
||||
maxScroll: 450 // Shorter than desktop
|
||||
},
|
||||
desktop: {
|
||||
targetTopPosition: 20,
|
||||
logoSpeed: 0.4,
|
||||
|
|
@ -121,13 +140,25 @@ export default function Home() {
|
|||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
// Set initial dimensions on mount
|
||||
// Set initial dimensions and orientation on mount
|
||||
setWindowHeight(window.innerHeight);
|
||||
const initialOrientation = window.innerHeight > window.innerWidth ? 'portrait' : 'landscape';
|
||||
setOrientation(initialOrientation);
|
||||
orientationRef.current = initialOrientation;
|
||||
if (contactSectionRef.current) {
|
||||
setContactTop(contactSectionRef.current.offsetTop);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Sync refs with state to avoid stale closures
|
||||
useEffect(() => {
|
||||
orientationRef.current = orientation;
|
||||
}, [orientation]);
|
||||
|
||||
useEffect(() => {
|
||||
isTransitioningRef.current = isTransitioning;
|
||||
}, [isTransitioning]);
|
||||
|
||||
// Helper function to update element styles directly
|
||||
const updateElementStyle = (element: HTMLElement | null, styles: Partial<CSSStyleDeclaration>) => {
|
||||
if (!element) return;
|
||||
|
|
@ -228,6 +259,106 @@ export default function Home() {
|
|||
};
|
||||
};
|
||||
|
||||
const calculateTabletLogoStyles = (
|
||||
scrollY: number,
|
||||
windowHeight: number,
|
||||
logoHeight: number,
|
||||
contactSection: HTMLElement | null
|
||||
): Partial<CSSStyleDeclaration> => {
|
||||
const { targetTopPosition, startYFactor, logoSpeed, finalScale, maxScroll } = animationConstants.tablet;
|
||||
|
||||
// Use ref-based orientation to avoid stale closures
|
||||
const isPortrait = orientationRef.current === 'portrait';
|
||||
|
||||
// Start position - adjust based on orientation for smoother animation
|
||||
// Portrait mode starts lower for a longer, smoother animation path
|
||||
const startY = windowHeight * (isPortrait ? 0.4 : startYFactor);
|
||||
|
||||
// End position - adjust based on orientation to prevent cutoff
|
||||
// Portrait needs more space from top due to taller viewport
|
||||
const endY = isPortrait ? 30 : targetTopPosition; // 30px for portrait (higher up), 10px for landscape
|
||||
|
||||
// Total distance to travel (simpler calculation like mobile)
|
||||
const totalDistance = startY - endY;
|
||||
|
||||
// Calculate Y position during animation
|
||||
let currentY: number;
|
||||
|
||||
if (contactSection) {
|
||||
const contactTop = contactSection.offsetTop;
|
||||
|
||||
// Adjust multiplier based on orientation
|
||||
// Portrait mode - complete animation closer to the contact section
|
||||
// This makes the logo become sticky lower on the page
|
||||
const multiplier = isPortrait ? 1.0 : 0.7; // Use 100% for portrait (like mobile), 70% for landscape
|
||||
|
||||
// Use dynamic contact position with orientation-aware multiplier
|
||||
const animationEndScroll = contactTop * multiplier;
|
||||
|
||||
if (scrollY < animationEndScroll) {
|
||||
// During animation - use progress-based positioning like mobile
|
||||
const progress = scrollY / animationEndScroll;
|
||||
currentY = startY - (totalDistance * progress);
|
||||
|
||||
// Calculate scale based on animation progress
|
||||
const scale = 1 - ((1 - finalScale) * progress);
|
||||
|
||||
// Update logo position state
|
||||
if (scrollY > 10) {
|
||||
logoPositionRef.current = 'animating';
|
||||
} else {
|
||||
logoPositionRef.current = 'center';
|
||||
}
|
||||
|
||||
return {
|
||||
position: 'fixed',
|
||||
top: `${currentY}px`,
|
||||
left: '50%',
|
||||
transform: `translate3d(-50%, 0, 0) scale3d(${scale}, ${scale}, 1) translateZ(0)`,
|
||||
transformOrigin: 'center',
|
||||
willChange: 'transform',
|
||||
backfaceVisibility: 'hidden',
|
||||
transition: isTransitioningRef.current ? 'all 0.3s ease-out' : 'none',
|
||||
zIndex: '50'
|
||||
};
|
||||
} else {
|
||||
// After animation - implement scrollPastEnd logic like mobile
|
||||
logoPositionRef.current = 'top';
|
||||
const scrollPastEnd = scrollY - animationEndScroll;
|
||||
currentY = endY - scrollPastEnd;
|
||||
|
||||
return {
|
||||
position: 'fixed',
|
||||
top: `${currentY}px`,
|
||||
left: '50%',
|
||||
transform: `translate3d(-50%, 0, 0) scale3d(${finalScale}, ${finalScale}, 1) translateZ(0)`,
|
||||
transformOrigin: 'center',
|
||||
willChange: scrollY > 0 ? 'transform' : 'auto',
|
||||
backfaceVisibility: 'hidden',
|
||||
transition: isTransitioningRef.current ? 'all 0.3s ease-out' : 'none',
|
||||
zIndex: '50'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Fallback if contact section not found
|
||||
const progress = Math.min(scrollY / 800, 1); // Use reasonable fallback
|
||||
currentY = startY - (totalDistance * progress);
|
||||
const scale = 1 - ((1 - finalScale) * progress);
|
||||
|
||||
return {
|
||||
position: 'fixed',
|
||||
top: `${currentY}px`,
|
||||
left: '50%',
|
||||
transform: `translate3d(-50%, 0, 0) scale3d(${scale}, ${scale}, 1) translateZ(0)`,
|
||||
transformOrigin: 'center',
|
||||
willChange: scrollY > 0 ? 'transform' : 'auto',
|
||||
backfaceVisibility: 'hidden',
|
||||
transition: isTransitioning ? 'all 0.3s ease-out' : 'none',
|
||||
zIndex: '50'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Update button and chevron opacity
|
||||
const updateControlsOpacity = (scrollY: number, isMobile: boolean) => {
|
||||
const fadeThreshold = isMobile ? animationConstants.mobile.fadeThreshold : animationConstants.desktop.fadeThreshold;
|
||||
|
|
@ -265,14 +396,19 @@ export default function Home() {
|
|||
}
|
||||
|
||||
// Calculate and apply logo styles based on device type
|
||||
const logoStyles = isMobile
|
||||
? calculateMobileLogoStyles(scrollY, currentWindowHeight, currentContactTop, logoHeight)
|
||||
: calculateDesktopLogoStyles(scrollY, currentWindowHeight, logoHeight);
|
||||
let logoStyles;
|
||||
if (isMobile) {
|
||||
logoStyles = calculateMobileLogoStyles(scrollY, currentWindowHeight, currentContactTop, logoHeight);
|
||||
} else if (isTablet) {
|
||||
logoStyles = calculateTabletLogoStyles(scrollY, currentWindowHeight, logoHeight, contactSectionRef.current);
|
||||
} else {
|
||||
logoStyles = calculateDesktopLogoStyles(scrollY, currentWindowHeight, logoHeight);
|
||||
}
|
||||
|
||||
updateElementStyle(logoRef.current, logoStyles);
|
||||
|
||||
// Update controls opacity
|
||||
updateControlsOpacity(scrollY, isMobile);
|
||||
updateControlsOpacity(scrollY, isMobile || isTablet);
|
||||
};
|
||||
|
||||
// Animated scroll to form
|
||||
|
|
@ -310,6 +446,33 @@ export default function Home() {
|
|||
};
|
||||
|
||||
const handleResize = () => {
|
||||
const newOrientation = window.innerHeight > window.innerWidth ? 'portrait' : 'landscape';
|
||||
|
||||
// Only trigger transition if orientation actually changed and we're on tablet
|
||||
if (newOrientation !== orientationRef.current && isTablet) {
|
||||
// Store current animation progress
|
||||
const scrollY = window.scrollY;
|
||||
const contactTop = contactSectionRef.current?.offsetTop || 0;
|
||||
animationProgressRef.current = Math.min(scrollY / contactTop, 1);
|
||||
|
||||
// Enable transition mode
|
||||
setIsTransitioning(true);
|
||||
setOrientation(newOrientation);
|
||||
|
||||
// Clear any existing timeout
|
||||
if (transitionTimeoutRef.current) {
|
||||
clearTimeout(transitionTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Disable transition after animation completes
|
||||
transitionTimeoutRef.current = setTimeout(() => {
|
||||
setIsTransitioning(false);
|
||||
}, 300);
|
||||
} else if (newOrientation !== orientation) {
|
||||
// Update orientation for non-tablet devices without transition
|
||||
setOrientation(newOrientation);
|
||||
}
|
||||
|
||||
setWindowHeight(window.innerHeight);
|
||||
if (contactSectionRef.current) {
|
||||
setContactTop(contactSectionRef.current.offsetTop);
|
||||
|
|
@ -328,8 +491,11 @@ export default function Home() {
|
|||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
if (transitionTimeoutRef.current) {
|
||||
clearTimeout(transitionTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [isMobile, isDesktop, logoHeight, windowHeight]);
|
||||
}, [isMobile, isTablet, isDesktop, logoHeight, windowHeight]);
|
||||
|
||||
// Dynamic height adjustment for image alignment
|
||||
useEffect(() => {
|
||||
|
|
@ -658,7 +824,7 @@ export default function Home() {
|
|||
{/* Image positioned to align with heading */}
|
||||
<div ref={imageContainerRef} className="relative h-full lg:h-auto">
|
||||
<Image
|
||||
src="/marina_cropped.jpg"
|
||||
src="/marina_cropped_v3.jpg"
|
||||
alt="Port Amador Marina"
|
||||
fill
|
||||
className="object-cover"
|
||||
|
|
|
|||
Loading…
Reference in New Issue