forked from
standard.site/standard.site
Standard.site landing page built in Next.js
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}