From 90dbc751238d3fd4a0cf414bc2fd2a03f917d2bc Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 22 Sep 2025 23:33:19 +0200 Subject: [PATCH] Replace main page with contact page - Contact page is now the homepage - Removed simple landing page - All animations and optimizations included --- src/app/page.tsx | 692 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 680 insertions(+), 12 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index ff348c6..2f0cf28 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,18 +1,686 @@ -import Link from 'next/link'; +'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'; + +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(1, 'Phone is required'), + message: z.string().optional(), +}); export default function Home() { + 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 ( -
-
-

PORT AMADOR

-

PANAMA

- + {/* Hero Section - Full Viewport Height */} +
+ {/* Single Port Amador Logo with dynamic positioning - Always rendered */} +
- VIEW CONTACT PAGE - -
+ 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 +

+
+ + ( + + + + + + )} + /> + ( + + + + + + )} + /> + ( + + + + + + )} + /> + ( + + + + + + )} + /> + ( + + +