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