'use client'; import { useState, useEffect, useRef, useMemo } from 'react'; import Image from 'next/image'; import { useMediaQuery } from '@react-hook/media-query'; import { ChevronDown, Phone, Mail } from 'lucide-react'; import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@/components/ui/button'; import { Form, FormControl, FormField, FormItem, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { PhoneField } from '@/components/forms/PhoneField'; const formSchema = z.object({ firstName: z.string().min(1, 'First name is required'), lastName: z.string().min(1, 'Last name is required'), email: z.string().email('Invalid email address'), phone: z.string().min(10, 'Please enter a valid phone number'), message: z.string().optional(), }); export default function ContactPage() { const [mounted, setMounted] = useState(false); const [contactTop, setContactTop] = useState(0); const [windowHeight, setWindowHeight] = useState(0); // Animation values as refs to avoid re-renders const logoPositionRef = useRef('center'); const buttonRef = useRef(null); const chevronRef = useRef(null); const contactSectionRef = useRef(null); const logoRef = useRef(null); const animationFrameRef = useRef(null); const lastScrollY = useRef(0); const lastFrameTime = useRef(0); const ticking = useRef(false); const isMobile = useMediaQuery("(max-width: 768px)"); const isDesktop = useMediaQuery("(min-width: 1280px)"); // Manage form state const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { firstName: "", lastName: "", email: "", phone: "", message: "", }, }); async function onSubmit(values: z.infer) { console.log(values); } // Memoized logo dimensions based on screen size const logoWidth = useMemo(() => isMobile ? 240 : isDesktop ? 316 : 280, [isMobile, isDesktop] ); const logoHeight = useMemo(() => isMobile ? 115 : isDesktop ? 151 : 134, [isMobile, isDesktop] ); // Memoized animation constants const animationConstants = useMemo(() => ({ mobile: { targetTopPosition: 10, startYFactor: 0.30, // Moved up from 0.35 to account for single-line button finalScale: 0.5, fadeThreshold: 5 }, desktop: { targetTopPosition: 20, logoSpeed: 0.4, maxScroll: 500, scaleReduction: 0.6, fadeThreshold: 10 } }), []); useEffect(() => { setMounted(true); // Set initial dimensions on mount setWindowHeight(window.innerHeight); if (contactSectionRef.current) { setContactTop(contactSectionRef.current.offsetTop); } }, []); // Helper function to update element styles directly const updateElementStyle = (element: HTMLElement | null, styles: Partial) => { if (!element) return; Object.assign(element.style, styles); }; // Separated mobile animation logic const calculateMobileLogoStyles = ( scrollY: number, windowHeight: number, contactTop: number, logoHeight: number ): Partial => { const { targetTopPosition, startYFactor, finalScale } = animationConstants.mobile; const startY = windowHeight * startYFactor; const endY = targetTopPosition + (logoHeight * finalScale) / 2; const totalDistance = startY - endY; const animationEndScroll = contactTop; if (scrollY >= animationEndScroll) { logoPositionRef.current = 'top'; const scrollPastEnd = scrollY - animationEndScroll; const fixedY = endY - scrollPastEnd; const mobileScale = finalScale; return { position: 'fixed', top: `${fixedY}px`, left: '50%', transform: `translate3d(-50%, 0, 0) scale3d(${mobileScale}, ${mobileScale}, 1) translateZ(0)`, transformOrigin: 'center', willChange: scrollY > 0 ? 'transform' : 'auto', backfaceVisibility: 'hidden', transition: 'none', zIndex: '50' }; } else if (scrollY > 0) { logoPositionRef.current = 'animating'; const progress = scrollY / animationEndScroll; const currentY = startY - (totalDistance * progress); const mobileScale = 1 - ((1 - finalScale) * progress); return { position: 'fixed', top: `${currentY}px`, left: '50%', transform: `translate3d(-50%, 0, 0) scale3d(${mobileScale}, ${mobileScale}, 1) translateZ(0)`, transformOrigin: 'center', willChange: 'transform', backfaceVisibility: 'hidden', transition: 'none', zIndex: '50' }; } else { logoPositionRef.current = 'center'; return { position: 'fixed', top: `${startY}px`, left: '50%', transform: 'translate3d(-50%, 0, 0) scale3d(1, 1, 1) translateZ(0)', transformOrigin: 'center', willChange: 'auto', backfaceVisibility: 'hidden', transition: 'none', zIndex: '50' }; } }; // Separated desktop animation logic const calculateDesktopLogoStyles = ( scrollY: number, windowHeight: number, logoHeight: number ): Partial => { const { targetTopPosition, logoSpeed, maxScroll, scaleReduction } = animationConstants.desktop; const centerY = windowHeight / 2; const totalDistance = centerY - targetTopPosition - logoHeight / 2; const logoYPosition = -(scrollY * logoSpeed); const maxUpwardMovement = -totalDistance; const animatedY = Math.max(logoYPosition, maxUpwardMovement); const scrollProgress = Math.min(scrollY / maxScroll, 1); const scale = 1 - (scaleReduction * scrollProgress); logoPositionRef.current = scrollY > 10 ? 'animating' : 'center'; return { position: 'fixed', top: '50%', left: '50%', transform: `translate3d(-50%, calc(-50% + ${animatedY}px), 0) scale3d(${scale}, ${scale}, 1) translateZ(0)`, transformOrigin: 'center', willChange: scrollY > 0 ? 'transform' : 'auto', backfaceVisibility: 'hidden', transition: 'none', zIndex: '50' }; }; // Update button and chevron opacity const updateControlsOpacity = (scrollY: number, isMobile: boolean) => { const fadeThreshold = isMobile ? animationConstants.mobile.fadeThreshold : animationConstants.desktop.fadeThreshold; const opacity = scrollY > fadeThreshold ? 0 : 1; if (buttonRef.current) { buttonRef.current.style.opacity = String(opacity); buttonRef.current.style.pointerEvents = opacity > 0 ? 'auto' : 'none'; } if (chevronRef.current) { chevronRef.current.style.opacity = String(opacity); } }; const updateLogoPosition = (timestamp: number = performance.now()) => { // Throttle to 60fps (16ms minimum between frames) if (timestamp - lastFrameTime.current < 16) { return; } lastFrameTime.current = timestamp; if (!contactSectionRef.current) return; const scrollY = window.scrollY; const currentWindowHeight = window.innerHeight; const currentContactTop = contactSectionRef.current.offsetTop; // Update cached values if needed if (currentContactTop !== contactTop) { setContactTop(currentContactTop); } if (currentWindowHeight !== windowHeight) { setWindowHeight(currentWindowHeight); } // Calculate and apply logo styles based on device type const logoStyles = isMobile ? calculateMobileLogoStyles(scrollY, currentWindowHeight, currentContactTop, logoHeight) : calculateDesktopLogoStyles(scrollY, currentWindowHeight, logoHeight); updateElementStyle(logoRef.current, logoStyles); // Update controls opacity updateControlsOpacity(scrollY, isMobile); }; // Animated scroll to form const scrollToForm = () => { if (!contactSectionRef.current) { console.error('Contact section ref not found'); return; } console.log('Starting scroll animation to:', contactSectionRef.current.offsetTop); // Use native smooth scrolling window.scrollTo({ top: contactSectionRef.current.offsetTop, behavior: 'smooth' }); }; // Add scroll listener for bidirectional animation with RAF useEffect(() => { const handleScroll = () => { lastScrollY.current = window.scrollY; if (!ticking.current) { ticking.current = true; animationFrameRef.current = requestAnimationFrame((timestamp) => { updateLogoPosition(timestamp); ticking.current = false; }); } }; const handleResize = () => { setWindowHeight(window.innerHeight); if (contactSectionRef.current) { setContactTop(contactSectionRef.current.offsetTop); } updateLogoPosition(); }; // Initial position update updateLogoPosition(); window.addEventListener('scroll', handleScroll, { passive: true }); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('scroll', handleScroll); window.removeEventListener('resize', handleResize); if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, [isMobile, isDesktop, logoHeight, windowHeight]); // Store if initial position has been set const initialPositionSet = useRef(false); // Calculate initial logo position to prevent teleport and flicker const initialLogoStyle = useMemo(() => { // Only set initial styles once and before JS takes over if (!mounted || initialPositionSet.current) return {}; // For mobile, position at the calculated start position if (isMobile && windowHeight > 0) { const initialTop = windowHeight * animationConstants.mobile.startYFactor; initialPositionSet.current = true; return { position: 'fixed' as const, top: `${initialTop}px`, left: '50%', transform: 'translate3d(-50%, 0, 0) translateZ(0)', willChange: 'auto', backfaceVisibility: 'hidden' as const, zIndex: 50 }; } // For desktop, center the logo properly if (!isMobile && mounted) { initialPositionSet.current = true; return { position: 'fixed' as const, top: '50%', left: '50%', transform: 'translate3d(-50%, -50%, 0) translateZ(0)', willChange: 'auto', backfaceVisibility: 'hidden' as const, zIndex: 50 }; } return {}; }, [isMobile, mounted, windowHeight, animationConstants.mobile.startYFactor]); // Don't render until mounted to avoid hydration mismatch if (!mounted) { return null; } return (
{/* Hero Section - Full Viewport Height */}
{/* Single Port Amador Logo with dynamic positioning - Always rendered */}
Port Amador
{/* Button with fade out on scroll */} {/* Chevron Down - with fade out on scroll */}
{/* Contact Section - Desktop Layout with Marina Image */}
{isMobile ? ( // Mobile Layout - Stacked with Image
{/* Form Section */}

Connect with us

( )} /> ( )} /> ( )} /> ( field.onChange(value)} onBlur={field.onBlur} error={fieldState.error?.message} required placeholder="Phone number" /> )} /> (