diff --git a/frontend/src/types/images.d.ts b/frontend/src/types/images.d.ts index c9ad21f..e33fbf4 100644 --- a/frontend/src/types/images.d.ts +++ b/frontend/src/types/images.d.ts @@ -19,8 +19,10 @@ declare module '*.gif' { } declare module '*.svg' { - const value: string; - export default value; + import React = require('react'); + export const ReactComponent: React.FC>; + const src: string; + export default src; } declare module '*.webp' { diff --git a/frontend/src/ui/common/assets/header/HeaderRope.png b/frontend/src/ui/common/assets/header/HeaderRope.png new file mode 100644 index 0000000..ecda57f Binary files /dev/null and b/frontend/src/ui/common/assets/header/HeaderRope.png differ diff --git a/frontend/src/ui/common/assets/header/MobileHeader.png b/frontend/src/ui/common/assets/header/MobileHeader.png new file mode 100644 index 0000000..a75649f Binary files /dev/null and b/frontend/src/ui/common/assets/header/MobileHeader.png differ diff --git a/frontend/src/ui/common/assets/header/Patch.png b/frontend/src/ui/common/assets/header/Patch.png new file mode 100644 index 0000000..50be755 Binary files /dev/null and b/frontend/src/ui/common/assets/header/Patch.png differ diff --git a/frontend/src/ui/common/assets/header/RegisterButton.png b/frontend/src/ui/common/assets/header/RegisterButton.png new file mode 100644 index 0000000..70b02b1 Binary files /dev/null and b/frontend/src/ui/common/assets/header/RegisterButton.png differ diff --git a/frontend/src/ui/common/assets/header/basicTree.svg b/frontend/src/ui/common/assets/header/basicTree.svg new file mode 100644 index 0000000..b4e4a53 --- /dev/null +++ b/frontend/src/ui/common/assets/header/basicTree.svg @@ -0,0 +1,21 @@ + + + + diff --git a/frontend/src/ui/common/assets/header/line.svg b/frontend/src/ui/common/assets/header/line.svg new file mode 100644 index 0000000..7e6c971 --- /dev/null +++ b/frontend/src/ui/common/assets/header/line.svg @@ -0,0 +1,21 @@ + + + + + + + diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1249.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1249.webp index a59713b..be70c6a 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1249.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1249.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1302.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1302.webp index ac952da..4281d3f 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1302.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1302.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1566.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1566.webp index a51f8bd..fa9a6a7 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1566.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1566.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1624.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1624.webp index f4bbe56..f8dc544 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1624.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1624.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1709.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1709.webp index a0a8f57..dacf64b 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1709.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1709.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1738.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1738.webp index 55b43c2..119bcb9 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1738.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1738.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/IMG_1834.webp b/frontend/src/ui/common/assets/photoCollageImages/IMG_1834.webp index 47c7be5..e3aa243 100644 Binary files a/frontend/src/ui/common/assets/photoCollageImages/IMG_1834.webp and b/frontend/src/ui/common/assets/photoCollageImages/IMG_1834.webp differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1249.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1249.webp deleted file mode 100644 index 5b12c97..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1249.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1302.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1302.webp deleted file mode 100644 index fc748bf..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1302.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1326.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1326.webp deleted file mode 100644 index aaf8946..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1326.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1353.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1353.webp deleted file mode 100644 index 3558d73..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1353.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1372.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1372.webp deleted file mode 100644 index 4628efe..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1372.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1382.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1382.webp deleted file mode 100644 index 0abf24b..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1382.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1566.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1566.webp deleted file mode 100644 index 439a15c..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1566.webp and /dev/null differ diff --git a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1749.webp b/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1749.webp deleted file mode 100644 index a1db172..0000000 Binary files a/frontend/src/ui/common/assets/photoCollageImages/tinified/IMG_1749.webp and /dev/null differ diff --git a/frontend/src/ui/pages/landing/components/Header.tsx b/frontend/src/ui/pages/landing/components/Header.tsx index 96ae8ea..2b37b0d 100644 --- a/frontend/src/ui/pages/landing/components/Header.tsx +++ b/frontend/src/ui/pages/landing/components/Header.tsx @@ -1,14 +1,564 @@ -import React from 'react'; -import BeeLogo from '../../../common/assets/BeeLogo.png'; +import React, { useState } from "react"; +import * as motion from 'motion/react-client'; +import { AnimatePresence, LayoutGroup, useMotionValue, useSpring } from 'motion/react'; +import BeeLogo from "../../../common/assets/BeeLogo.png"; +import Patch from "../../../common/assets/header/Patch.png"; +import HeaderRope from "../../../common/assets/header/HeaderRope.png"; +import MobileHeader from "../../../common/assets/header/MobileHeader.png"; +import RegisterButton from "../../../common/assets/header/RegisterButton.png"; +import { ReactComponent as TreeIcon } from "../../../common/assets/header/basicTree.svg"; +import { ReactComponent as LineIcon } from "../../../common/assets/header/line.svg"; + +interface HeaderProps { + showFinalElements: boolean; +} + +interface NavLinkProps { + href: string; + label: string; + activeSection: string; + hoveredSection: string | null; + onHover: (section: string) => void; + onLeave: () => void; +} + +const NavLink = React.forwardRef( + ({ href, label, activeSection, hoveredSection, onHover, onLeave }, ref) => { + const sectionId = href.replace('#', ''); + // Jump if this link matches the triangle position (hover takes priority, then active) + const shouldJump = (hoveredSection || activeSection) === sectionId; + + return ( + onHover(sectionId)} + onMouseLeave={onLeave} + data-section={sectionId} + layout + animate={{ + y: shouldJump ? -4 : 0, + scale: shouldJump ? 1.1 : 1, + }} + transition={{ + type: "spring", + stiffness: 300, + damping: 20, + mass: 0.8, + layout: { + type: "spring", + stiffness: 300, + damping: 20 + } + }} + > + {label} + + ); + } +); + +const Header: React.FC = ({ showFinalElements }) => { + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [activeSection, setActiveSection] = useState('top'); + const [hoveredSection, setHoveredSection] = useState(null); + const [isRegisterButtonVisible, setIsRegisterButtonVisible] = useState(true); // Start hidden + const [triangleMoveReason, setTriangleMoveReason] = useState<'layout' | 'navigation'>('navigation'); + const navRef = React.useRef(null); + const hoverTimeoutRef = React.useRef(null); + + // Track when REGISTER visibility changes (causes layout shift) + const prevRegisterVisible = React.useRef(isRegisterButtonVisible); + React.useEffect(() => { + if (prevRegisterVisible.current !== isRegisterButtonVisible) { + setTriangleMoveReason('layout'); + prevRegisterVisible.current = isRegisterButtonVisible; + } + }, [isRegisterButtonVisible]); + + // Track when user navigates (scroll or hover) + React.useEffect(() => { + setTriangleMoveReason('navigation'); + }, [hoveredSection, activeSection]); + + // MotionValue to track triangle position + const triangleXRaw = useMotionValue(0); + // Create spring (always called per React rules) + const triangleXSpring = useSpring(triangleXRaw, { + stiffness: 300, + damping: 20, + mass: 0.4 + }); + + // Choose which MotionValue to use: raw (instant) or spring (animated) + const triangleX = triangleMoveReason === 'layout' ? triangleXRaw : triangleXSpring; + + // Debounced hover handlers to prevent triangle jumping on diamond hovers + const handleNavHover = (sectionId: string) => { + // Clear any pending timeout + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + // Immediately set hover + setHoveredSection(sectionId); + }; + + const handleNavLeave = () => { + // Clear any pending timeout + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + // Delay clearing hover to avoid jumping during brief gaps (like diamonds) + hoverTimeoutRef.current = window.setTimeout(() => { + setHoveredSection(null); + }, 300); // 300ms delay + }; + + // Cleanup timeout on unmount + React.useEffect(() => { + return () => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + }; + }, []); + + // Track ALL nav links' positions in real-time and animate triangle + React.useEffect(() => { + const updateTrianglePosition = () => { + if (!navRef.current) return; + + const headerElement = document.querySelector('header'); + if (!headerElement) return; + + const headerRect = headerElement.getBoundingClientRect(); + const headerCenter = headerRect.left + headerRect.width / 2; + + // Find the active/hovered link + const targetSection = hoveredSection || activeSection; + const targetLink = navRef.current.querySelector(`[data-section="${targetSection}"]`); + + if (targetLink) { + const linkRect = targetLink.getBoundingClientRect(); + const linkCenter = linkRect.left + linkRect.width / 2; + const offsetFromCenter = linkCenter - headerCenter; + + // Update raw MotionValue (spring will smoothly follow) + triangleXRaw.set(offsetFromCenter - 10); + } + }; + + // Track continuously during animations + let frameId: number; + const trackLoop = () => { + updateTrianglePosition(); + frameId = requestAnimationFrame(trackLoop); + }; + + trackLoop(); + + return () => cancelAnimationFrame(frameId); + }, [hoveredSection, activeSection, isRegisterButtonVisible, triangleXRaw]); + + // Track active section based on scroll position + React.useEffect(() => { + const handleScroll = () => { + const sections = ['top', 'about', 'faq', 'sponsors', 'register']; + const scrollPosition = window.scrollY + window.innerHeight / 2; + + // Hide nav REGISTER at very top of page (< 100px scrolled) + if (window.scrollY < 100) { + setIsRegisterButtonVisible(true); // Hide nav link + return; + } + + // Check if "Register Now" button is visible in viewport + // Check both mobile and desktop buttons + const mobileButton = document.getElementById('register-button-mobile'); + const desktopButton = document.getElementById('register-button-desktop'); + + let buttonVisible = false; + + // Check mobile button (visible on screens < 600px) + if (mobileButton && window.innerWidth < 600) { + const rect = mobileButton.getBoundingClientRect(); + const computedStyle = window.getComputedStyle(mobileButton.parentElement || mobileButton); + const isActuallyVisible = computedStyle.opacity !== '0' && computedStyle.display !== 'none'; + buttonVisible = isActuallyVisible && rect.top < window.innerHeight && rect.bottom > 0; + } + + // Check desktop button (visible on screens >= 600px) + if (desktopButton && window.innerWidth >= 600) { + const rect = desktopButton.getBoundingClientRect(); + const computedStyle = window.getComputedStyle(desktopButton.parentElement || desktopButton); + const isActuallyVisible = computedStyle.opacity !== '0' && computedStyle.display !== 'none'; + buttonVisible = isActuallyVisible && rect.top < window.innerHeight && rect.bottom > 0; + } + + setIsRegisterButtonVisible(buttonVisible); + + // Check if sponsors section is in viewport + const sponsorsEl = document.getElementById('sponsors'); + if (sponsorsEl) { + const sponsorsRect = sponsorsEl.getBoundingClientRect(); + // Only activate sponsors if at least 40% of viewport shows sponsors + const sponsorsVisible = Math.max(0, Math.min(window.innerHeight, sponsorsRect.bottom) - Math.max(0, sponsorsRect.top)); + if (sponsorsVisible > window.innerHeight * 0.4) { + setActiveSection('sponsors'); + return; + } + } + + // Find the section that's currently in view (check from bottom to top) + for (let i = sections.length - 1; i >= 0; i--) { + const sectionId = sections[i]; + if (sectionId === 'sponsors') continue; // Already handled above + + const element = document.getElementById(sectionId); + if (element) { + const { offsetTop } = element; + if (scrollPosition >= offsetTop) { + setActiveSection(sectionId); + break; + } + } + } + }; + + window.addEventListener('scroll', handleScroll); + handleScroll(); // Initial check + return () => window.removeEventListener('scroll', handleScroll); + }, []); -const Header: React.FC = () => { return ( -
- Hacklahoma Bee Logo +
+ {/* Bee icon on the left */} + + Hacklahoma Bee Logo + + + {/* Background for nav section only */} +
+ {/* Rope pattern at the bottom of nav background */} +
+
+ + {/* Desktop Navigation - centered links */} + + + {/* Mobile Menu Button - centered on small screens */} + +
+ + {/* Animated triangle that smoothly flies to active/hovered nav link */} + + + {/* Mobile Menu Modal */} + + {isMobileMenuOpen && ( + + Mobile menu background + + + + )} +
); }; diff --git a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/NavigationButton.tsx b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/NavigationButton.tsx index ce689e9..1ae0429 100644 --- a/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/NavigationButton.tsx +++ b/frontend/src/ui/pages/landing/components/photoCollage/photoCollageComponents/NavigationButton.tsx @@ -133,7 +133,11 @@ export const NavigationButton: React.FC = ({ return (