Skip to content

Commit dbd8668

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

File tree

3 files changed

+186
-11
lines changed

3 files changed

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

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>

src/hooks/usePlausibleEvent.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ type PlausibleEventProps = {
88
page: string;
99
title: string;
1010
};
11+
['Copy Page']: {
12+
page: string;
13+
source: string;
14+
};
15+
['Copy Page Dropdown']: {
16+
page: string;
17+
action: string;
18+
};
1119
['Doc Feedback']: {
1220
helpful: boolean;
1321
page: string;
@@ -20,6 +28,10 @@ type PlausibleEventProps = {
2028
page: string;
2129
readProgress: ReadProgressMilestone;
2230
};
31+
['View Markdown']: {
32+
page: string;
33+
source: string;
34+
};
2335
};
2436

2537
/**

0 commit comments

Comments
 (0)