Update marina image to use v3 across all devices
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:
Matt 2025-10-14 21:21:28 +02:00
parent 7c9aedb92d
commit 2d5ffcb268
2 changed files with 177 additions and 11 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

After

Width:  |  Height:  |  Size: 346 KiB

View File

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