Skip to content

Commit 84161db

Browse files
authored
Merge pull request #3180 from replicatedhq/copy-markdown
Add copy markdown element
2 parents 15157f0 + a67a6dd commit 84161db

File tree

93 files changed

+1284
-65023
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+1284
-65023
lines changed

.gitignore

+9-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@
88
.docusaurus
99
.cache-loader
1010
.history
11-
llms.txt
12-
llms-full.txt
11+
static/llms.txt
12+
static/llms-full.txt
13+
static/intro-kots.md
14+
static/intro-replicated.md
15+
static/intro.md
16+
static/enterprise/*
17+
static/reference/*
18+
static/release-notes/*
19+
static/vendor/*
1320

1421
# Misc
1522
.DS_Store

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
"scripts": {
66
"docusaurus": "docusaurus",
77
"start": "docusaurus start",
8+
"dev": "docusaurus clear && docusaurus start",
89
"prebuild": "npm run generate-llms",
910
"build": "docusaurus build",
11+
"rebuild-serve": "npm run build && npm run serve",
1012
"swizzle": "docusaurus swizzle",
1113
"deploy": "docusaurus deploy",
1214
"clear": "docusaurus clear",

src/components/CopyMarkdown.js

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import React, { useState, useEffect, useCallback, useRef } from 'react';
2+
import styles from './CopyMarkdown.module.css';
3+
4+
function CopyMarkdown() {
5+
const [isOpen, setIsOpen] = useState(false);
6+
const [hasContent, setHasContent] = useState(false);
7+
const [isDarkTheme, setIsDarkTheme] = useState(false);
8+
const [isCopied, setIsCopied] = useState(false);
9+
const buttonRef = useRef(null);
10+
const dropdownRef = useRef(null);
11+
12+
// Toggle dropdown
13+
const toggleDropdown = useCallback(() => {
14+
setIsOpen(prev => !prev);
15+
}, []);
16+
17+
// Copy markdown to clipboard
18+
const copyMarkdown = useCallback(async () => {
19+
try {
20+
// Get the current page path
21+
const currentPath = window.location.pathname;
22+
23+
// Remove trailing slash if it exists
24+
const normalizedPath = currentPath.endsWith('/') && currentPath !== '/'
25+
? currentPath.slice(0, -1)
26+
: currentPath;
27+
28+
// For the homepage/intro, use /intro.md specifically
29+
const markdownPath = normalizedPath === '/' ? '/intro.md' : `${normalizedPath}.md`;
30+
31+
// Fetch the markdown content
32+
const response = await fetch(markdownPath);
33+
34+
if (!response.ok) {
35+
throw new Error(`Failed to fetch markdown: ${response.status}`);
36+
}
37+
38+
const markdown = await response.text();
39+
40+
// Copy to clipboard
41+
await navigator.clipboard.writeText(markdown);
42+
43+
// Close dropdown and show copied feedback
44+
setIsOpen(false);
45+
setIsCopied(true);
46+
47+
// Reset the button state after 2 seconds
48+
setTimeout(() => {
49+
setIsCopied(false);
50+
}, 2000);
51+
} catch (error) {
52+
console.error('Failed to copy markdown:', error);
53+
}
54+
}, []);
55+
56+
// View as plain text
57+
const viewAsMarkdown = useCallback(() => {
58+
try {
59+
// Get the current page path
60+
const currentPath = window.location.pathname;
61+
62+
// Remove trailing slash if it exists
63+
const normalizedPath = currentPath.endsWith('/') && currentPath !== '/'
64+
? currentPath.slice(0, -1)
65+
: currentPath;
66+
67+
// For the homepage/intro, use /intro.md specifically
68+
const markdownPath = normalizedPath === '/' ? '/intro.md' : `${normalizedPath}.md`;
69+
70+
// Open in a new tab
71+
window.open(markdownPath, '_blank');
72+
73+
// Close dropdown
74+
setIsOpen(false);
75+
} catch (error) {
76+
console.error('Failed to view markdown:', error);
77+
}
78+
}, []);
79+
80+
// Open in ChatGPT
81+
const openInChatGpt = useCallback(async () => {
82+
try {
83+
// Get the current page path
84+
const currentPath = window.location.pathname;
85+
86+
// Remove trailing slash if it exists
87+
const normalizedPath = currentPath.endsWith('/') && currentPath !== '/'
88+
? currentPath.slice(0, -1)
89+
: currentPath;
90+
91+
// For the homepage/intro, use /intro specifically
92+
const docPath = normalizedPath === '/' ? '/intro' : normalizedPath;
93+
94+
// Construct the full markdown URL with domain
95+
const fullMarkdownUrl = `https://docs.replicated.com${docPath}.md`;
96+
97+
// Create the prompt to send to ChatGPT
98+
const prompt = `Read ${fullMarkdownUrl} so I can ask questions about it`;
99+
100+
// URL encode the prompt for the ChatGPT URL
101+
const encodedPrompt = encodeURIComponent(prompt);
102+
103+
// Create the ChatGPT URL with the prompt
104+
const chatGptUrl = `https://chat.openai.com/?prompt=${encodedPrompt}`;
105+
106+
// Open ChatGPT with the prompt
107+
window.open(chatGptUrl, '_blank');
108+
109+
// Close the dropdown
110+
setIsOpen(false);
111+
} catch (error) {
112+
console.error('Failed to open ChatGPT:', error);
113+
}
114+
}, []);
115+
116+
// Handle click outside to close dropdown
117+
const handleClickOutside = useCallback((event) => {
118+
// Only close if clicking outside both the button and dropdown
119+
if (
120+
buttonRef.current &&
121+
!buttonRef.current.contains(event.target) &&
122+
dropdownRef.current &&
123+
!dropdownRef.current.contains(event.target)
124+
) {
125+
setIsOpen(false);
126+
}
127+
}, []);
128+
129+
// Initialize on client side
130+
useEffect(() => {
131+
// Check if we have markdown content
132+
const hasMarkdownContent = !!document.querySelector('.theme-doc-markdown.markdown h1');
133+
setHasContent(hasMarkdownContent);
134+
135+
// Set up click outside handler
136+
document.addEventListener('click', handleClickOutside);
137+
138+
return () => {
139+
// Clean up event listener
140+
document.removeEventListener('click', handleClickOutside);
141+
};
142+
}, [handleClickOutside]);
143+
144+
// Check for dark theme
145+
useEffect(() => {
146+
const checkTheme = () => {
147+
setIsDarkTheme(document.documentElement.getAttribute('data-theme') === 'dark');
148+
};
149+
150+
checkTheme();
151+
152+
// Listen for theme changes
153+
const observer = new MutationObserver(checkTheme);
154+
observer.observe(document.documentElement, {
155+
attributes: true,
156+
attributeFilter: ['data-theme']
157+
});
158+
159+
return () => observer.disconnect();
160+
}, []);
161+
162+
// Don't render on pages that don't have markdown content
163+
if (!hasContent) {
164+
return null;
165+
}
166+
167+
return (
168+
<div className={styles.container}>
169+
<button
170+
ref={buttonRef}
171+
className={`${styles.button} ${isCopied ? styles.copied : ''}`}
172+
onClick={!isCopied ? toggleDropdown : undefined}
173+
aria-expanded={isOpen}
174+
aria-haspopup="true"
175+
disabled={isCopied}
176+
>
177+
{isCopied ? (
178+
<span className={styles.buttonText}>Copied!</span>
179+
) : (
180+
<>
181+
<img
182+
src={isDarkTheme ? "/images/icons/copy-white.svg" : "/images/icons/copy.svg"}
183+
alt="Copy"
184+
className={styles.copyIcon}
185+
/>
186+
<span className={styles.buttonText}>Open in ChatGPT</span>
187+
<svg className={styles.icon} viewBox="0 0 20 20" fill="currentColor">
188+
<path
189+
fillRule="evenodd"
190+
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
191+
clipRule="evenodd"
192+
/>
193+
</svg>
194+
</>
195+
)}
196+
</button>
197+
198+
{isOpen && (
199+
<div
200+
ref={dropdownRef}
201+
className={styles.dropdown}
202+
role="menu"
203+
aria-orientation="vertical"
204+
>
205+
<ul className={styles.list}>
206+
<li className={styles.item}>
207+
<button
208+
className={styles.actionButton}
209+
onClick={openInChatGpt}
210+
role="menuitem"
211+
>
212+
<div className={styles.actionContent}>
213+
<span className={styles.actionTitle}>Open in ChatGPT</span>
214+
<span className={styles.actionDescription}>Ask questions about this page</span>
215+
</div>
216+
</button>
217+
</li>
218+
<li className={styles.item}>
219+
<button
220+
className={styles.actionButton}
221+
onClick={copyMarkdown}
222+
role="menuitem"
223+
>
224+
<div className={styles.actionContent}>
225+
<span className={styles.actionTitle}>Copy Markdown</span>
226+
<span className={styles.actionDescription}>Copy page source to clipboard</span>
227+
</div>
228+
</button>
229+
</li>
230+
<li className={styles.item}>
231+
<button
232+
className={styles.actionButton}
233+
onClick={viewAsMarkdown}
234+
role="menuitem"
235+
>
236+
<div className={styles.actionContent}>
237+
<span className={styles.actionTitle}>View Markdown</span>
238+
<span className={styles.actionDescription}>Open this page in plain text format</span>
239+
</div>
240+
</button>
241+
</li>
242+
</ul>
243+
</div>
244+
)}
245+
</div>
246+
);
247+
}
248+
249+
export default CopyMarkdown;

0 commit comments

Comments
 (0)