forked from
standard.site/standard.site
Standard.site landing page built in Next.js
1'use client'
2
3import { useEffect, useState } from 'react'
4import Link from 'next/link'
5import { ArrowUpRightIcon, MenuIcon, XIcon } from 'lucide-react'
6import BlurEffect from 'react-progressive-blur'
7import { AnimateIn, StandardSiteLogo } from '@/app/components'
8import { EXTERNAL_LINKS, NAV_ITEMS, SECONDARY_NAV_ITEMS } from '@/app/data/content'
9import { scrollToElement } from '@/app/lib/scroll'
10
11export function MobileNav() {
12 const [isOpen, setIsOpen] = useState(false)
13 const [activeSection, setActiveSection] = useState<string>('#')
14
15 useEffect(() => {
16 if (isOpen) {
17 document.body.style.overflow = 'hidden'
18 } else {
19 document.body.style.overflow = ''
20 }
21
22 return () => {
23 document.body.style.overflow = ''
24 }
25 }, [isOpen])
26
27 useEffect(() => {
28 const sectionIds = NAV_ITEMS
29 .map(item => item.href)
30 .filter(href => href !== '#')
31 .map(href => href.slice(1))
32
33 const observers: IntersectionObserver[] = []
34
35 sectionIds.forEach(id => {
36 const element = document.getElementById(id)
37 if (!element) return
38
39 const observer = new IntersectionObserver(
40 (entries) => {
41 entries.forEach(entry => {
42 if (entry.isIntersecting) {
43 setActiveSection(`#${id}`)
44 }
45 })
46 },
47 {
48 rootMargin: '-20% 0px -70% 0px',
49 threshold: 0
50 }
51 )
52
53 observer.observe(element)
54 observers.push(observer)
55 })
56
57 const handleScroll = () => {
58 if (window.scrollY < 100) {
59 setActiveSection('#')
60 }
61 }
62
63 window.addEventListener('scroll', handleScroll)
64 handleScroll()
65
66 return () => {
67 observers.forEach(observer => observer.disconnect())
68 window.removeEventListener('scroll', handleScroll)
69 }
70 }, [])
71
72 const handleNavClick = (href: string) => {
73 setIsOpen(false)
74 setTimeout(() => {
75 scrollToElement(href)
76 }, 100)
77 }
78
79 return (
80 <>
81 <AnimateIn
82 as="header"
83 direction="down"
84 delay={ 0.5 }
85 onScroll={ false }
86 className="fixed left-0 right-0 top-0 z-30 px-4 py-2 md:hidden"
87 >
88 <div
89 className={ `p-4 relative flex flex-col gap-6 z-40 mx-auto max-w-[38rem] w-full min-h-0 overflow-hidden rounded-2xl transition-all duration-300 ease-in-out ${
90 isOpen
91 ? 'bg-zinc-950 dark:bg-zinc-50 text-zinc-50 dark:text-zinc-950 h-[31rem]'
92 : 'text-base-content h-15'
93 }` }>
94 <div className="flex justify-between items-center">
95 <StandardSiteLogo className="size-7" />
96 { !isOpen && (
97 <button
98 onClick={ () => setIsOpen(true) }
99 aria-label="Open menu"
100 >
101 <MenuIcon className="size-6" />
102 </button>
103 )}
104 { isOpen && (
105 <button
106 onClick={ () => setIsOpen(false) }
107 aria-label="Close menu"
108 >
109 <XIcon className="size-6" />
110 </button>
111 )}
112 </div>
113 <nav className="flex flex-col gap-4">
114 { NAV_ITEMS.map((item) => (
115 <a
116 key={ item.label }
117 href={ item.href }
118 onClick={ (e) => {
119 e.preventDefault()
120 handleNavClick(item.href)
121 } }
122 className={ `font-medium text-lg tracking-tight ${
123 activeSection === item.href ? 'text-zinc-50 dark:text-zinc-950' : 'text-muted-content'
124 } hover:text-zinc-50 dark:hover:text-zinc-950 transition-colors` }
125 >
126 { item.label }
127 </a>
128 )) }
129
130 <div className="h-px w-full bg-border/10" />
131
132 { SECONDARY_NAV_ITEMS.map((item) => (
133 <Link
134 key={ item.label }
135 href={ item.href }
136 onClick={ () => setIsOpen(false) }
137 className="font-medium text-lg tracking-tight text-muted-content hover:text-zinc-50 dark:hover:text-zinc-950 transition-colors"
138 >
139 { item.label }
140 </Link>
141 )) }
142
143 <div className="h-px w-full bg-border/10" />
144
145 <nav className="flex flex-col gap-4">
146 { EXTERNAL_LINKS.map((link) => (
147 <a
148 key={ link.label }
149 href={ link.href }
150 target="_blank"
151 rel="noopener noreferrer"
152 className="flex font-medium text-lg tracking-tight text-muted-content hover:text-zinc-50 dark:hover:text-zinc-950 transition-colors"
153 >
154 { link.label }
155 <ArrowUpRightIcon className="size-6 ml-auto"/>
156 </a>
157 )) }
158 </nav>
159 </nav>
160 </div>
161 <BlurEffect
162 className="bg-gradient-to-b from-base-100 to-transparent absolute inset-0 w-full h-full -z-10"
163 position="top"
164 intensity={ 75 }
165 />
166 </AnimateIn>
167
168 {/* Overlay */}
169 <div
170 className={ `fixed inset-0 z-10 bg-base-100/50 backdrop-blur-sm transition-opacity duration-300 md:hidden ${
171 isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
172 }` }
173 onClick={ () => setIsOpen(false) }
174 />
175 </>
176 )
177}