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 [contactTop, setContactTop] = useState(0);
|
||||||
const [windowHeight, setWindowHeight] = 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
|
// Animation values as refs to avoid re-renders
|
||||||
const logoPositionRef = useRef('center');
|
const logoPositionRef = useRef('center');
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
@ -50,6 +60,7 @@ export default function Home() {
|
||||||
const imageContainerRef = useRef<HTMLDivElement>(null);
|
const imageContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||||
|
const isTablet = useMediaQuery("(min-width: 769px) and (max-width: 1024px)");
|
||||||
const isDesktop = useMediaQuery("(min-width: 1280px)");
|
const isDesktop = useMediaQuery("(min-width: 1280px)");
|
||||||
|
|
||||||
// Manage form state
|
// Manage form state
|
||||||
|
|
@ -93,13 +104,13 @@ export default function Home() {
|
||||||
|
|
||||||
// Memoized logo dimensions based on screen size
|
// Memoized logo dimensions based on screen size
|
||||||
const logoWidth = useMemo(() =>
|
const logoWidth = useMemo(() =>
|
||||||
isMobile ? 240 : isDesktop ? 316 : 280,
|
isMobile ? 240 : isTablet ? 280 : isDesktop ? 316 : 280,
|
||||||
[isMobile, isDesktop]
|
[isMobile, isTablet, isDesktop]
|
||||||
);
|
);
|
||||||
|
|
||||||
const logoHeight = useMemo(() =>
|
const logoHeight = useMemo(() =>
|
||||||
isMobile ? 115 : isDesktop ? 151 : 134,
|
isMobile ? 115 : isTablet ? 134 : isDesktop ? 151 : 134,
|
||||||
[isMobile, isDesktop]
|
[isMobile, isTablet, isDesktop]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Memoized animation constants
|
// Memoized animation constants
|
||||||
|
|
@ -110,6 +121,14 @@ export default function Home() {
|
||||||
finalScale: 0.5,
|
finalScale: 0.5,
|
||||||
fadeThreshold: 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: {
|
desktop: {
|
||||||
targetTopPosition: 20,
|
targetTopPosition: 20,
|
||||||
logoSpeed: 0.4,
|
logoSpeed: 0.4,
|
||||||
|
|
@ -121,13 +140,25 @@ export default function Home() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
// Set initial dimensions on mount
|
// Set initial dimensions and orientation on mount
|
||||||
setWindowHeight(window.innerHeight);
|
setWindowHeight(window.innerHeight);
|
||||||
|
const initialOrientation = window.innerHeight > window.innerWidth ? 'portrait' : 'landscape';
|
||||||
|
setOrientation(initialOrientation);
|
||||||
|
orientationRef.current = initialOrientation;
|
||||||
if (contactSectionRef.current) {
|
if (contactSectionRef.current) {
|
||||||
setContactTop(contactSectionRef.current.offsetTop);
|
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
|
// Helper function to update element styles directly
|
||||||
const updateElementStyle = (element: HTMLElement | null, styles: Partial<CSSStyleDeclaration>) => {
|
const updateElementStyle = (element: HTMLElement | null, styles: Partial<CSSStyleDeclaration>) => {
|
||||||
if (!element) return;
|
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
|
// Update button and chevron opacity
|
||||||
const updateControlsOpacity = (scrollY: number, isMobile: boolean) => {
|
const updateControlsOpacity = (scrollY: number, isMobile: boolean) => {
|
||||||
const fadeThreshold = isMobile ? animationConstants.mobile.fadeThreshold : animationConstants.desktop.fadeThreshold;
|
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
|
// Calculate and apply logo styles based on device type
|
||||||
const logoStyles = isMobile
|
let logoStyles;
|
||||||
? calculateMobileLogoStyles(scrollY, currentWindowHeight, currentContactTop, logoHeight)
|
if (isMobile) {
|
||||||
: calculateDesktopLogoStyles(scrollY, currentWindowHeight, logoHeight);
|
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);
|
updateElementStyle(logoRef.current, logoStyles);
|
||||||
|
|
||||||
// Update controls opacity
|
// Update controls opacity
|
||||||
updateControlsOpacity(scrollY, isMobile);
|
updateControlsOpacity(scrollY, isMobile || isTablet);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Animated scroll to form
|
// Animated scroll to form
|
||||||
|
|
@ -310,6 +446,33 @@ export default function Home() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = () => {
|
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);
|
setWindowHeight(window.innerHeight);
|
||||||
if (contactSectionRef.current) {
|
if (contactSectionRef.current) {
|
||||||
setContactTop(contactSectionRef.current.offsetTop);
|
setContactTop(contactSectionRef.current.offsetTop);
|
||||||
|
|
@ -328,8 +491,11 @@ export default function Home() {
|
||||||
if (animationFrameRef.current) {
|
if (animationFrameRef.current) {
|
||||||
cancelAnimationFrame(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
|
// Dynamic height adjustment for image alignment
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -658,7 +824,7 @@ export default function Home() {
|
||||||
{/* Image positioned to align with heading */}
|
{/* Image positioned to align with heading */}
|
||||||
<div ref={imageContainerRef} className="relative h-full lg:h-auto">
|
<div ref={imageContainerRef} className="relative h-full lg:h-auto">
|
||||||
<Image
|
<Image
|
||||||
src="/marina_cropped.jpg"
|
src="/marina_cropped_v3.jpg"
|
||||||
alt="Port Amador Marina"
|
alt="Port Amador Marina"
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue