Standard.site landing page built in Next.js
0

Configure Feed

Select the types of activity you want to include in your feed.

at dev 6.9 kB View raw
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}