Skip to content

Commit 73db2f6

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

File tree

3 files changed

+182
-11
lines changed

3 files changed

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

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)