Skip to content

Commit 1f26e2c

Browse files
feat(ai): Add copy page button to all doc pages
1 parent d49e1c1 commit 1f26e2c

File tree

2 files changed

+160
-11
lines changed

2 files changed

+160
-11
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
'use client';
2+
3+
import {Fragment, useEffect, useRef, useState} from 'react';
4+
import {createPortal} from 'react-dom';
5+
import {Clipboard} from 'react-feather';
6+
import Link from 'next/link';
7+
8+
import Chevron from 'sentry-docs/icons/Chevron';
9+
import Markdown from 'sentry-docs/icons/Markdown';
10+
11+
interface CopyMarkdownButtonProps {
12+
pathname: string;
13+
}
14+
15+
export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
16+
const [isLoading, setIsLoading] = useState(false);
17+
const [copied, setCopied] = useState(false);
18+
const [isOpen, setIsOpen] = useState(false);
19+
const [isMounted, setIsMounted] = useState(false);
20+
const buttonRef = useRef<HTMLDivElement>(null);
21+
const dropdownRef = useRef<HTMLDivElement>(null);
22+
23+
const copyMarkdownToClipboard = async () => {
24+
setIsLoading(true);
25+
setCopied(false);
26+
setIsOpen(false);
27+
28+
try {
29+
const response = await fetch(`https://docs.sentry.io/${pathname}.md`);
30+
if (!response.ok) {
31+
throw new Error(`Failed to fetch markdown content: ${response.status}`);
32+
}
33+
34+
await navigator.clipboard.writeText(await response.text());
35+
setCopied(true);
36+
setTimeout(() => setCopied(false), 2000);
37+
} catch (error) {
38+
console.error('Failed to copy markdown to clipboard:', error);
39+
} finally {
40+
setIsLoading(false);
41+
}
42+
};
43+
44+
// Set mounted state and close dropdown on outside click
45+
useEffect(() => {
46+
setIsMounted(true);
47+
48+
const handleClickOutside = (event: MouseEvent) => {
49+
if (
50+
isOpen &&
51+
buttonRef.current &&
52+
!buttonRef.current.contains(event.target as Node) &&
53+
dropdownRef.current &&
54+
!dropdownRef.current.contains(event.target as Node)
55+
) {
56+
setIsOpen(false);
57+
}
58+
};
59+
60+
document.addEventListener('mousedown', handleClickOutside);
61+
return () => {
62+
setIsMounted(false);
63+
document.removeEventListener('mousedown', handleClickOutside);
64+
};
65+
}, [isOpen]);
66+
67+
const getDropdownPosition = () => {
68+
if (!buttonRef.current) return {top: 0, left: 0};
69+
const rect = buttonRef.current.getBoundingClientRect();
70+
return {
71+
top: rect.bottom + 8,
72+
left: rect.right - 320,
73+
};
74+
};
75+
76+
const buttonClass = "inline-flex items-center h-full text-gray-700 bg-transparent border-none cursor-pointer transition-colors duration-150 hover:bg-gray-50 active:bg-gray-100 focus:bg-gray-50 outline-none";
77+
const dropdownItemClass = "flex items-center gap-3 w-full p-2 px-3 text-left bg-transparent rounded-md transition-colors hover:bg-gray-100 font-sans text-gray-900";
78+
const iconContainerClass = "flex items-center justify-center w-7 h-7 bg-gray-100 rounded shrink-0";
79+
80+
return (
81+
<Fragment>
82+
<div className="relative inline-block" ref={buttonRef}>
83+
<div className="inline-flex items-center h-9 border border-gray-200 rounded-full overflow-hidden bg-white">
84+
<button
85+
onClick={copyMarkdownToClipboard}
86+
className={`${buttonClass} gap-2 px-3.5 text-sm font-medium disabled:opacity-50`}
87+
style={{borderRadius: '9999px 0 0 9999px'}}
88+
disabled={isLoading}
89+
>
90+
<Clipboard size={16} />
91+
<span>{copied ? 'Copied!' : 'Copy page'}</span>
92+
</button>
93+
94+
<div className="w-px h-full bg-gray-200" />
95+
96+
<button
97+
onClick={() => setIsOpen(!isOpen)}
98+
className={`${buttonClass} px-3`}
99+
style={{borderRadius: '0 9999px 9999px 0'}}
100+
>
101+
<Chevron
102+
width={16}
103+
height={16}
104+
direction="down"
105+
className={`transition-transform duration-200 ${isOpen ? 'rotate-180' : 'rotate-0'}`}
106+
/>
107+
</button>
108+
</div>
109+
</div>
110+
111+
{isMounted && isOpen && createPortal(
112+
<div
113+
ref={dropdownRef}
114+
className="fixed w-80 bg-white rounded-xl shadow-lg overflow-hidden z-[9999]"
115+
style={{...getDropdownPosition(), border: '1px solid #d1d5db'}}
116+
>
117+
<div className="p-1">
118+
<button
119+
onClick={() => {
120+
copyMarkdownToClipboard();
121+
setIsOpen(false);
122+
}}
123+
className={`${dropdownItemClass} border-none cursor-pointer disabled:opacity-50`}
124+
disabled={isLoading}
125+
>
126+
<div className={iconContainerClass}>
127+
<Clipboard size={14} />
128+
</div>
129+
<div className="flex-1">
130+
<div className="font-medium text-gray-900 text-sm leading-5">Copy page</div>
131+
<div className="text-xs text-gray-500 leading-4">Copy page as Markdown for LLMs</div>
132+
</div>
133+
</button>
134+
135+
<Link
136+
href={`/${pathname}.md`}
137+
target="_blank"
138+
rel="noopener noreferrer"
139+
className={`${dropdownItemClass} no-underline`}
140+
onClick={() => setIsOpen(false)}
141+
>
142+
<div className={iconContainerClass}>
143+
<Markdown width={14} height={14} />
144+
</div>
145+
<div className="flex-1">
146+
<div className="font-medium text-gray-900 text-sm leading-5">View as Markdown</div>
147+
<div className="text-xs text-gray-500 leading-4">View this page as plain text</div>
148+
</div>
149+
</Link>
150+
</div>
151+
</div>,
152+
document.body
153+
)}
154+
</Fragment>
155+
);
156+
}

src/components/docPage/index.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import {ReactNode} from 'react';
2-
import Link from 'next/link';
32

43
import {getCurrentGuide, getCurrentPlatform, nodeForPath} from 'sentry-docs/docTree';
5-
import Markdown from 'sentry-docs/icons/Markdown';
64
import {serverContext} from 'sentry-docs/serverContext';
75
import {FrontMatter} from 'sentry-docs/types';
86
import {PaginationNavNode} from 'sentry-docs/types/paginationNavNode';
@@ -14,6 +12,7 @@ import './type.scss';
1412
import {Banner} from '../banner';
1513
import {Breadcrumbs} from '../breadcrumbs';
1614
import {CodeContextProvider} from '../codeContext';
15+
import {CopyMarkdownButton} from '../copyMarkdownButton';
1716
import {DocFeedback} from '../docFeedback';
1817
import {GitHubCTA} from '../githubCTA';
1918
import {Header} from '../header';
@@ -85,15 +84,9 @@ export function DocPage({
8584
</div>
8685
<div className="overflow-hidden">
8786
{leafNode && <Breadcrumbs leafNode={leafNode} />}{' '}
88-
<Link
89-
rel="nofollow"
90-
className="float-right"
91-
href={`/${pathname}.md`}
92-
data-mdast="ignore"
93-
title="Markdown version of this page"
94-
>
95-
<Markdown className="flex p-0 flex-wrap" width={24} height={24} />
96-
</Link>
87+
<div className="float-right mt-4 sm:mt-0">
88+
<CopyMarkdownButton pathname={pathname} />
89+
</div>
9790
</div>
9891
<div>
9992
<hgroup>

0 commit comments

Comments
 (0)