|
1 | | -"use client" |
| 1 | +"use client"; |
2 | 2 |
|
3 | | -import { useState, useEffect, useRef } from "react" |
4 | | -import { motion } from "framer-motion" |
5 | | -import { Button } from "@/components/ui/button" |
6 | | -import { useIsMobile } from "@/components/ui/use-mobile" |
| 3 | +import { useState, useEffect, useRef } from "react"; |
| 4 | +import { motion } from "framer-motion"; |
| 5 | +import { Button } from "@/components/ui/button"; |
| 6 | +import { useIsMobile } from "@/components/ui/use-mobile"; |
7 | 7 |
|
8 | 8 | const navItems = [ |
9 | | - { name: "Home", href: "#home" }, |
10 | | - { name: "About", href: "#about" }, |
11 | | - { name: "Experience", href: "#experience" }, |
12 | | - { name: "Projects", href: "#projects" }, |
13 | | - { name: "Contact", href: "#contact" }, |
14 | | -] |
| 9 | + { name: "Home", href: "#home" }, |
| 10 | + { name: "About", href: "#about" }, |
| 11 | + { name: "Experience", href: "#experience" }, |
| 12 | + { name: "Projects", href: "#projects" }, |
| 13 | + { name: "Contact", href: "#contact" }, |
| 14 | +]; |
15 | 15 |
|
16 | | -export function FloatingNav({ mobileOffset = false }: { mobileOffset?: boolean } = {}) { |
17 | | - const isMobile = useIsMobile(); |
18 | | - const [isAtBottom, setIsAtBottom] = useState(false) |
19 | | - const [activeSection, setActiveSection] = useState("home") |
20 | | - const [isFloating, setIsFloating] = useState(true) |
21 | | - const navRef = useRef(null) |
| 16 | +export function FloatingNav({ |
| 17 | + mobileOffset = false, |
| 18 | +}: { mobileOffset?: boolean } = {}) { |
| 19 | + const isMobile = useIsMobile(); |
| 20 | + const [isAtBottom, setIsAtBottom] = useState(false); |
| 21 | + const [activeSection, setActiveSection] = useState("home"); |
| 22 | + const [isFloating, setIsFloating] = useState(true); |
| 23 | + const navRef = useRef(null); |
22 | 24 |
|
23 | | - useEffect(() => { |
24 | | - const handleScroll = () => { |
25 | | - const scrollHeight = document.documentElement.scrollHeight |
26 | | - const scrollTop = document.documentElement.scrollTop |
27 | | - const clientHeight = document.documentElement.clientHeight |
| 25 | + useEffect(() => { |
| 26 | + const handleScroll = () => { |
| 27 | + const scrollHeight = document.documentElement.scrollHeight; |
| 28 | + const scrollTop = document.documentElement.scrollTop; |
| 29 | + const clientHeight = document.documentElement.clientHeight; |
28 | 30 |
|
29 | | - const bottomThreshold = 100 |
30 | | - const isBottom = scrollTop + clientHeight >= scrollHeight - bottomThreshold |
31 | | - setIsAtBottom(isBottom) |
32 | | - setIsFloating(!isBottom) |
| 31 | + const bottomThreshold = 100; |
| 32 | + const isBottom = |
| 33 | + scrollTop + clientHeight >= scrollHeight - bottomThreshold; |
| 34 | + setIsAtBottom(isBottom); |
| 35 | + setIsFloating(!isBottom); |
33 | 36 |
|
34 | | - // Update active section based on scroll position |
35 | | - const sections = navItems.map((item) => item.href.slice(1)) |
36 | | - const currentSection = sections.find((section) => { |
37 | | - const element = document.getElementById(section) |
38 | | - if (element) { |
39 | | - const rect = element.getBoundingClientRect() |
40 | | - return rect.top <= 100 && rect.bottom >= 100 |
41 | | - } |
42 | | - return false |
43 | | - }) |
| 37 | + // Update active section based on scroll position |
| 38 | + const sections = navItems.map((item) => item.href.slice(1)); |
| 39 | + const currentSection = sections.find((section) => { |
| 40 | + const element = document.getElementById(section); |
| 41 | + if (element) { |
| 42 | + const rect = element.getBoundingClientRect(); |
| 43 | + return rect.top <= 100 && rect.bottom >= 100; |
| 44 | + } |
| 45 | + return false; |
| 46 | + }); |
44 | 47 |
|
45 | | - if (currentSection) { |
46 | | - setActiveSection(currentSection) |
47 | | - } |
48 | | - } |
| 48 | + if (currentSection) { |
| 49 | + setActiveSection(currentSection); |
| 50 | + } |
| 51 | + }; |
49 | 52 |
|
50 | | - window.addEventListener("scroll", handleScroll) |
51 | | - return () => window.removeEventListener("scroll", handleScroll) |
52 | | - }, []) |
| 53 | + window.addEventListener("scroll", handleScroll); |
| 54 | + return () => window.removeEventListener("scroll", handleScroll); |
| 55 | + }, []); |
53 | 56 |
|
54 | | - const scrollToSection = (href: string) => { |
55 | | - const element = document.getElementById(href.slice(1)) |
56 | | - if (element) { |
57 | | - element.scrollIntoView({ behavior: "smooth" }) |
58 | | - } |
59 | | - } |
| 57 | + const scrollToSection = (href: string) => { |
| 58 | + const element = document.getElementById(href.slice(1)); |
| 59 | + if (element) { |
| 60 | + element.scrollIntoView({ behavior: "smooth" }); |
| 61 | + } |
| 62 | + }; |
60 | 63 |
|
61 | | - return ( |
62 | | - <motion.nav |
63 | | - ref={navRef} |
64 | | - className={`fixed z-40 left-1/2 -translate-x-1/2 transition-all duration-500 pointer-events-auto |
65 | | - ${isAtBottom ? `bottom-0 w-full max-w-6xl` : `${mobileOffset ? 'bottom-4' : 'bottom-4'} w-auto`} |
66 | | - ${mobileOffset ? ' md:bottom-8 md:mb-0' : ''} |
| 64 | + return ( |
| 65 | + <motion.nav |
| 66 | + ref={navRef} |
| 67 | + className={`fixed z-40 left-1/2 -translate-x-1/2 transition-all duration-500 pointer-events-auto |
| 68 | + ${ |
| 69 | + isAtBottom |
| 70 | + ? `bottom-0 w-full max-w-6xl` |
| 71 | + : `${mobileOffset ? "bottom-4" : "bottom-4"} w-auto` |
| 72 | + } |
| 73 | + ${mobileOffset ? " md:bottom-8 md:mb-0" : ""} |
67 | 74 | `} |
68 | | - > |
69 | | - <motion.div |
70 | | - layout |
71 | | - className={`backdrop-blur-md bg-background/95 border border-border shadow-lg dark:bg-gray-900/95 dark:border-gray-700 transition-all duration-300 ${ |
72 | | - isAtBottom |
73 | | - ? "rounded-tl-2xl rounded-tr-2xl rounded-b-none w-full h-full" |
74 | | - : "rounded-full w-auto h-full" |
75 | | - }`} |
76 | | - > |
77 | | - <div className="flex items-center justify-center h-full px-4 md:px-6"> |
78 | | - <div className="flex items-center space-x-1"> |
79 | | - <span className="text-sm font-semibold mr-2 md:mr-4 hidden sm:block">Grace Yuen</span> |
80 | | - <div className="h-4 w-px bg-border mr-2 md:mr-4 hidden sm:block" /> |
| 75 | + > |
| 76 | + <motion.div |
| 77 | + layout |
| 78 | + className={`backdrop-blur-md bg-background/95 border border-border shadow-lg dark:bg-gray-900/95 dark:border-gray-700 transition-all duration-300 ${ |
| 79 | + isAtBottom |
| 80 | + ? "rounded-tl-2xl rounded-tr-2xl rounded-b-none w-full h-full" |
| 81 | + : "rounded-full w-auto h-full" |
| 82 | + }`} |
| 83 | + > |
| 84 | + <div className="flex items-center justify-center h-full px-4 md:px-6"> |
| 85 | + <div className="flex items-center space-x-1"> |
| 86 | + <span className="text-sm font-semibold mr-2 md:mr-4 hidden sm:block"> |
| 87 | + Grace Yuen |
| 88 | + </span> |
| 89 | + <div className="h-4 w-px bg-border mr-2 md:mr-4 hidden sm:block" /> |
81 | 90 |
|
82 | | - {navItems.map((item) => ( |
83 | | - <motion.div key={item.name}> |
84 | | - <Button |
85 | | - variant="ghost" |
86 | | - size="sm" |
87 | | - onClick={() => scrollToSection(item.href)} |
88 | | - className={`relative transition-all duration-300 touch-manipulation text-xs md:text-sm px-2 md:px-3 ${ |
89 | | - activeSection === item.href.slice(1) |
90 | | - ? "text-primary" |
91 | | - : "text-muted-foreground" |
92 | | - }${ |
93 | | - !isMobile ? " hover:bg-accent/80 hover:text-foreground active:bg-accent/80" : "" |
94 | | - }`} |
95 | | - tabIndex={0} |
96 | | - // Remove hover/active styles on touch devices |
97 | | - onTouchStart={e => { |
98 | | - e.currentTarget.classList.remove("hover:bg-accent", "active:bg-accent/80") |
99 | | - }} |
100 | | - > |
101 | | - <motion.span whileHover={!isMobile ? { scale: 1.05 } : {}} whileTap={{ scale: 0.95 }}> |
102 | | - {item.name} |
103 | | - </motion.span> |
| 91 | + {navItems.map((item) => ( |
| 92 | + <motion.div key={item.name}> |
| 93 | + <Button |
| 94 | + variant="ghost" |
| 95 | + size="sm" |
| 96 | + onClick={() => scrollToSection(item.href)} |
| 97 | + className={`relative transition-all duration-300 touch-manipulation text-xs md:text-sm px-2 md:px-3 ${ |
| 98 | + activeSection === item.href.slice(1) |
| 99 | + ? "text-primary" |
| 100 | + : "text-muted-foreground" |
| 101 | + }${ |
| 102 | + !isMobile |
| 103 | + ? " hover:bg-accent/80 hover:text-foreground active:bg-accent/80" |
| 104 | + : "" |
| 105 | + }`} |
| 106 | + tabIndex={0} |
| 107 | + // Remove hover/active styles on touch devices |
| 108 | + onTouchStart={(e) => { |
| 109 | + e.currentTarget.classList.remove( |
| 110 | + "hover:bg-accent", |
| 111 | + "active:bg-accent/80" |
| 112 | + ); |
| 113 | + }} |
| 114 | + > |
| 115 | + <motion.span |
| 116 | + whileHover={!isMobile ? { scale: 1.05 } : {}} |
| 117 | + whileTap={{ scale: 0.95 }} |
| 118 | + > |
| 119 | + {item.name} |
| 120 | + </motion.span> |
104 | 121 |
|
105 | | - {activeSection === item.href.slice(1) && ( |
106 | | - isFloating ? ( |
107 | | - <motion.div |
108 | | - className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary rounded-full" |
109 | | - initial={false} |
110 | | - transition={{ ease: "easeInOut", duration: 0.35 }} |
111 | | - style={{ y: 0, x: 0 }} |
112 | | - layoutId={item.name} |
113 | | - /> |
114 | | - ) : ( |
115 | | - <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary rounded-full" /> |
116 | | - ) |
117 | | - )} |
118 | | - </Button> |
119 | | - </motion.div> |
120 | | - ))} |
121 | | - </div> |
122 | | - </div> |
123 | | - </motion.div> |
124 | | - </motion.nav> |
125 | | - ) |
| 122 | + {activeSection === item.href.slice(1) && |
| 123 | + (isFloating ? ( |
| 124 | + <motion.div |
| 125 | + className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary rounded-full" |
| 126 | + initial={false} |
| 127 | + transition={{ ease: "easeInOut", duration: 0.35 }} |
| 128 | + style={{ y: 0, x: 0 }} |
| 129 | + layoutId={item.name} |
| 130 | + /> |
| 131 | + ) : ( |
| 132 | + <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary rounded-full" /> |
| 133 | + ))} |
| 134 | + </Button> |
| 135 | + </motion.div> |
| 136 | + ))} |
| 137 | + </div> |
| 138 | + </div> |
| 139 | + </motion.div> |
| 140 | + </motion.nav> |
| 141 | + ); |
126 | 142 | } |
0 commit comments