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