Standard.site landing page built in Next.js

Replace MarkdownButton with DocsPageMenu dropdown

aka.dad d9085f2d e4fdafb6

verified
+101 -43
+1 -5
app/components/Footer.tsx
··· 1 - import { BlueskyLogo, PdslsLogo, StandardSiteLogo, TangledLogo, MarkdownButton } from '@/app/components' 1 + import { BlueskyLogo, PdslsLogo, StandardSiteLogo, TangledLogo } from '@/app/components' 2 2 3 3 export function Footer() { 4 4 return ( ··· 42 42 </footer> 43 43 44 44 <section className="hidden md:flex shrink-0 w-32"></section> 45 - 46 - <div className="absolute bottom-8 right-8"> 47 - <MarkdownButton /> 48 - </div> 49 45 </div> 50 46 ) 51 47 }
+99
app/components/docs/DocsPageMenu.tsx
··· 1 + 'use client' 2 + 3 + import { useState, useRef, useEffect } from 'react' 4 + import { usePathname } from 'next/navigation' 5 + 6 + export function DocsPageMenu() { 7 + const [open, setOpen] = useState(false) 8 + const [copied, setCopied] = useState(false) 9 + const menuRef = useRef<HTMLDivElement>(null) 10 + const pathname = usePathname() 11 + 12 + const slug = (() => { 13 + const path = pathname.split(/[?#]/)[0] 14 + if (!path.startsWith('/docs/')) return null 15 + const parts = path.split('/').filter(Boolean) 16 + if (parts.length <= 1) return null 17 + return parts.slice(1).join('/') 18 + })() 19 + 20 + useEffect(() => { 21 + if (!open) return 22 + 23 + function handleClickOutside(e: MouseEvent) { 24 + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { 25 + setOpen(false) 26 + } 27 + } 28 + 29 + document.addEventListener('mousedown', handleClickOutside) 30 + return () => document.removeEventListener('mousedown', handleClickOutside) 31 + }, [open]) 32 + 33 + if (!slug) return null 34 + 35 + const handleCopyMarkdown = async () => { 36 + try { 37 + const res = await fetch(`/api/docs/markdown/${slug}`) 38 + const markdown = await res.text() 39 + await navigator.clipboard.writeText(markdown) 40 + setCopied(true) 41 + setTimeout(() => { 42 + setCopied(false) 43 + setOpen(false) 44 + }, 1500) 45 + } catch { 46 + setOpen(false) 47 + } 48 + } 49 + 50 + return ( 51 + <div ref={menuRef} className="relative"> 52 + <button 53 + onClick={() => setOpen(!open)} 54 + className="flex items-center justify-center text-muted hover:text-base-content transition-colors p-1 rounded-md hover:bg-base-200 mt-1 size-8" 55 + aria-label="Page options" 56 + > 57 + <svg className="size-4" fill="currentColor" viewBox="0 0 20 20"> 58 + <path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" /> 59 + </svg> 60 + </button> 61 + 62 + {open && ( 63 + <div className="absolute right-0 top-full mt-2 p-0.5 bg-base-200 border border-border grid gap-px rounded-xl shadow-lg z-20 min-w-48"> 64 + <button 65 + onClick={handleCopyMarkdown} 66 + className="w-full flex items-center gap-2 px-3 py-2 text-sm text-muted hover:text-base-content hover:bg-base-300 rounded-lg transition-colors" 67 + > 68 + {copied ? ( 69 + <> 70 + <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 71 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> 72 + </svg> 73 + Copied! 74 + </> 75 + ) : ( 76 + <> 77 + <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 78 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> 79 + </svg> 80 + Copy as Markdown 81 + </> 82 + )} 83 + </button> 84 + <a 85 + href={`/api/docs/markdown/${slug}`} 86 + target="_blank" 87 + rel="noopener noreferrer" 88 + className="w-full flex items-center gap-2 px-3 py-2 text-sm text-muted hover:text-base-content hover:bg-base-300 rounded-lg transition-colors" 89 + > 90 + <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 91 + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> 92 + </svg> 93 + View raw Markdown 94 + </a> 95 + </div> 96 + )} 97 + </div> 98 + ) 99 + }
-37
app/components/docs/MarkdownButton.tsx
··· 1 - 'use client' 2 - 3 - import { usePathname } from 'next/navigation' 4 - 5 - interface MarkdownButtonProps { 6 - slug?: string 7 - } 8 - 9 - export function MarkdownButton({ slug: providedSlug }: MarkdownButtonProps) { 10 - const pathname = usePathname() 11 - 12 - // Detect slug from pathname if not provided 13 - const slug = providedSlug || (() => { 14 - const path = pathname.split(/[?#]/)[0] // Remove query and hash 15 - if (!path.startsWith('/docs/')) return null 16 - const parts = path.split('/').filter(Boolean) 17 - if (parts.length <= 1) return null // Only /docs/ with no slug 18 - return parts.slice(1).join('/') 19 - })() 20 - 21 - if (!slug) return null 22 - 23 - return ( 24 - <a 25 - href={`/api/docs/markdown/${slug}`} 26 - target="_blank" 27 - rel="noopener noreferrer" 28 - className="bg-base-100 text-muted hover:text-base-content hover:bg-base-300 px-3 py-2 rounded-lg border border-border transition-all flex items-center gap-2 text-xs h-fit" 29 - title="View raw markdown" 30 - > 31 - <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 32 - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> 33 - </svg> 34 - Markdown 35 - </a> 36 - ) 37 - }
+1 -1
app/components/docs/index.ts
··· 3 3 export { Table } from './Table' 4 4 export { LinkCard } from './LinkCard' 5 5 export { StandardSite } from './StandardSite' 6 - export { MarkdownButton } from './MarkdownButton' 7 6 export { ClickableHeading } from './ClickableHeading' 7 + export { DocsPageMenu } from './DocsPageMenu'