|
| 1 | +'use client' |
| 2 | + |
| 3 | +import { useEffect, useRef, useState } from 'react' |
| 4 | +import { usePathname } from 'next/navigation' |
| 5 | + |
| 6 | +const BASE = process.env.NEXT_PUBLIC_BASE_PATH ?? '' |
| 7 | + |
| 8 | +function slugFrom(pathname) { |
| 9 | + const seg = (pathname || '/').replace(/\/+$/, '').split('/').filter(Boolean) |
| 10 | + return seg.length ? seg[seg.length - 1] : 'index' |
| 11 | +} |
| 12 | + |
| 13 | +const Ico = ({ d, size = 14 }) => ( |
| 14 | + <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }} aria-hidden> |
| 15 | + <path d={d} /> |
| 16 | + </svg> |
| 17 | +) |
| 18 | +const I = { |
| 19 | + copy: 'M9 9h10a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2zM5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1', |
| 20 | + check: 'M20 6 9 17l-5-5', |
| 21 | + md: 'M3 5h18v14H3zM7 15V9l3 3 3-3v6M17 9v4m0 0-2-2m2 2 2-2', |
| 22 | + chevron: 'm6 9 6 6 6-6', |
| 23 | + ext: 'M15 3h6v6M10 14 21 3M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5', |
| 24 | + chat: 'M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z', |
| 25 | +} |
| 26 | + |
| 27 | +const AIS = [ |
| 28 | + { name: 'ChatGPT', url: 'https://chatgpt.com/?q=', domain: 'chatgpt.com' }, |
| 29 | + { name: 'Claude', url: 'https://claude.ai/new?q=', domain: 'claude.ai' }, |
| 30 | + { name: 'Perplexity', url: 'https://www.perplexity.ai/search?q=', domain: 'perplexity.ai' }, |
| 31 | + { name: 'Grok', url: 'https://grok.com/?q=', domain: 'grok.com' }, |
| 32 | + { name: 'Gemini', url: 'https://gemini.google.com/app?q=', domain: 'gemini.google.com' }, |
| 33 | +] |
| 34 | +const favicon = (domain) => `https://www.google.com/s2/favicons?domain=${domain}&sz=64` |
| 35 | + |
| 36 | +const BORDER = 'light-dark(rgba(0,0,0,0.12), rgba(255,255,255,0.14))' |
| 37 | +const HOVER = 'light-dark(rgba(0,0,0,0.05), rgba(255,255,255,0.07))' |
| 38 | +const MENU_BG = 'light-dark(#ffffff, #1c1c1f)' |
| 39 | +const MUTED = 'light-dark(#52525b, #a1a1aa)' |
| 40 | + |
| 41 | +export default function CopyPage() { |
| 42 | + const pathname = usePathname() |
| 43 | + const [open, setOpen] = useState(false) |
| 44 | + const [copied, setCopied] = useState(false) |
| 45 | + const ref = useRef(null) |
| 46 | + |
| 47 | + useEffect(() => { |
| 48 | + const onDoc = (e) => ref.current && !ref.current.contains(e.target) && setOpen(false) |
| 49 | + document.addEventListener('click', onDoc) |
| 50 | + return () => document.removeEventListener('click', onDoc) |
| 51 | + }, []) |
| 52 | + |
| 53 | + const slug = slugFrom(pathname) |
| 54 | + const mdUrl = `${BASE}/md/${slug}.md` |
| 55 | + const pageUrl = typeof window !== 'undefined' ? window.location.href : '' |
| 56 | + const absMd = typeof window !== 'undefined' ? `${window.location.origin}${mdUrl}` : mdUrl |
| 57 | + const prompt = `Read ${pageUrl} (raw markdown: ${absMd}) — a Zaileys documentation page — then help me with it.` |
| 58 | + |
| 59 | + const copy = async () => { |
| 60 | + try { |
| 61 | + const text = await (await fetch(mdUrl)).text() |
| 62 | + await navigator.clipboard.writeText(text) |
| 63 | + } catch { |
| 64 | + await navigator.clipboard.writeText(pageUrl) |
| 65 | + } |
| 66 | + setCopied(true) |
| 67 | + setTimeout(() => setCopied(false), 1800) |
| 68 | + setOpen(false) |
| 69 | + } |
| 70 | + |
| 71 | + const item = { |
| 72 | + display: 'flex', |
| 73 | + alignItems: 'center', |
| 74 | + gap: '0.6rem', |
| 75 | + width: '100%', |
| 76 | + padding: '0.5rem 0.65rem', |
| 77 | + fontSize: '0.82rem', |
| 78 | + lineHeight: 1.2, |
| 79 | + background: 'transparent', |
| 80 | + border: 'none', |
| 81 | + color: 'inherit', |
| 82 | + cursor: 'pointer', |
| 83 | + textDecoration: 'none', |
| 84 | + textAlign: 'left', |
| 85 | + borderRadius: 7, |
| 86 | + whiteSpace: 'nowrap', |
| 87 | + } |
| 88 | + const hov = (e, on) => (e.currentTarget.style.background = on ? HOVER : 'transparent') |
| 89 | + |
| 90 | + return ( |
| 91 | + <div ref={ref} style={{ position: 'relative', display: 'inline-flex', colorScheme: 'light dark' }}> |
| 92 | + <div |
| 93 | + style={{ |
| 94 | + display: 'inline-flex', |
| 95 | + alignItems: 'stretch', |
| 96 | + border: `1px solid ${BORDER}`, |
| 97 | + borderRadius: 8, |
| 98 | + overflow: 'hidden', |
| 99 | + fontSize: '0.78rem', |
| 100 | + fontWeight: 500, |
| 101 | + }} |
| 102 | + > |
| 103 | + <button |
| 104 | + onClick={copy} |
| 105 | + title="Copy this page as Markdown for LLMs" |
| 106 | + style={{ display: 'inline-flex', alignItems: 'center', gap: '0.4rem', padding: '0.34rem 0.65rem', background: 'transparent', border: 'none', color: 'inherit', cursor: 'pointer', transition: 'background 0.12s' }} |
| 107 | + onMouseEnter={(e) => hov(e, true)} |
| 108 | + onMouseLeave={(e) => hov(e, false)} |
| 109 | + > |
| 110 | + <Ico d={copied ? I.check : I.copy} size={13} /> |
| 111 | + {copied ? 'Copied!' : 'Copy page'} |
| 112 | + </button> |
| 113 | + <button |
| 114 | + onClick={() => setOpen((v) => !v)} |
| 115 | + aria-label="More options" |
| 116 | + style={{ display: 'inline-flex', alignItems: 'center', padding: '0.34rem 0.4rem', background: 'transparent', border: 'none', borderLeft: `1px solid ${BORDER}`, color: MUTED, cursor: 'pointer', transition: 'background 0.12s' }} |
| 117 | + onMouseEnter={(e) => hov(e, true)} |
| 118 | + onMouseLeave={(e) => hov(e, false)} |
| 119 | + > |
| 120 | + <Ico d={I.chevron} size={13} /> |
| 121 | + </button> |
| 122 | + </div> |
| 123 | + {open && ( |
| 124 | + <div |
| 125 | + style={{ |
| 126 | + position: 'absolute', |
| 127 | + top: 'calc(100% + 7px)', |
| 128 | + right: 0, |
| 129 | + zIndex: 50, |
| 130 | + minWidth: 215, |
| 131 | + padding: 5, |
| 132 | + background: MENU_BG, |
| 133 | + border: `1px solid ${BORDER}`, |
| 134 | + borderRadius: 11, |
| 135 | + boxShadow: '0 10px 34px rgba(0,0,0,0.18)', |
| 136 | + }} |
| 137 | + > |
| 138 | + <button style={item} onMouseEnter={(e) => hov(e, true)} onMouseLeave={(e) => hov(e, false)} onClick={copy}> |
| 139 | + <Ico d={I.copy} /> Copy page as Markdown |
| 140 | + </button> |
| 141 | + <a style={item} onMouseEnter={(e) => hov(e, true)} onMouseLeave={(e) => hov(e, false)} href={mdUrl} target="_blank" rel="noreferrer"> |
| 142 | + <Ico d={I.md} /> View as Markdown |
| 143 | + <span style={{ marginLeft: 'auto', color: MUTED }}><Ico d={I.ext} size={12} /></span> |
| 144 | + </a> |
| 145 | + <div style={{ height: 1, background: BORDER, margin: '4px 6px' }} /> |
| 146 | + <div style={{ padding: '0.3rem 0.65rem 0.2rem', fontSize: '0.68rem', fontWeight: 600, letterSpacing: '0.04em', textTransform: 'uppercase', color: MUTED }}>Open in</div> |
| 147 | + {AIS.map((ai) => ( |
| 148 | + <a key={ai.name} style={item} onMouseEnter={(e) => hov(e, true)} onMouseLeave={(e) => hov(e, false)} href={`${ai.url}${encodeURIComponent(prompt)}`} target="_blank" rel="noreferrer"> |
| 149 | + <img src={favicon(ai.domain)} alt="" width={15} height={15} style={{ borderRadius: 3, flexShrink: 0 }} /> |
| 150 | + {ai.name} |
| 151 | + <span style={{ marginLeft: 'auto', color: MUTED }}><Ico d={I.ext} size={12} /></span> |
| 152 | + </a> |
| 153 | + ))} |
| 154 | + </div> |
| 155 | + )} |
| 156 | + </div> |
| 157 | + ) |
| 158 | +} |
0 commit comments