Standard.site landing page built in Next.js
at main 70 lines 2.8 kB view raw
1'use client' 2 3import { useState } from 'react' 4import { generateSlug } from '@/app/lib/slug' 5 6interface ClickableHeadingProps { 7 level: 1 | 2 | 3 | 4 8 children: React.ReactNode 9} 10 11export function ClickableHeading({ level, children }: ClickableHeadingProps) { 12 const [copied, setCopied] = useState(false) 13 14 const text = typeof children === 'string' ? children : extractText(children) 15 const id = generateSlug(text) 16 17 const handleClick = async () => { 18 const url = `${window.location.origin}${window.location.pathname}#${id}` 19 await navigator.clipboard.writeText(url) 20 setCopied(true) 21 22 // Update URL without scrolling 23 window.history.pushState({}, '', `#${id}`) 24 25 setTimeout(() => setCopied(false), 2000) 26 } 27 28 const Tag = `h${level}` as keyof React.JSX.IntrinsicElements 29 30 const baseClasses = "font-display font-semibold leading-tight tracking-tighter text-base-content group cursor-pointer hover:text-muted transition-colors relative scroll-mt-32" 31 const levelClasses = { 32 1: "text-3xl sm:text-4xl mb-8", 33 2: "text-2xl sm:text-3xl mt-12 mb-4", 34 3: "text-xl sm:text-2xl tracking-tight mt-8 mb-3", 35 4: "text-lg sm:text-xl tracking-tight mt-6 mb-2" 36 } 37 38 return ( 39 <Tag 40 id={id} 41 className={`${baseClasses} ${levelClasses[level]}`} 42 onClick={handleClick} 43 > 44 <span className="inline-flex items-center gap-2"> 45 {children} 46 <span className="opacity-0 group-hover:opacity-100 transition-opacity text-muted"> 47 {copied ? ( 48 <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 49 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> 50 </svg> 51 ) : ( 52 <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 53 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /> 54 </svg> 55 )} 56 </span> 57 </span> 58 </Tag> 59 ) 60} 61 62function extractText(node: React.ReactNode): string { 63 if (typeof node === 'string') return node 64 if (typeof node === 'number') return String(node) 65 if (Array.isArray(node)) return node.map(extractText).join('') 66 if (node && typeof node === 'object' && 'props' in node) { 67 return extractText((node as { props: { children: React.ReactNode } }).props.children) 68 } 69 return '' 70}