|
1 | | -import { useEffect } from 'react' |
| 1 | +import { useEffect, useState } from 'react' |
| 2 | +import useDocusaurusContext from '@docusaurus/useDocusaurusContext' |
| 3 | + |
| 4 | +const DISCOURSE_URL = 'https://builder.metamask.io' |
2 | 5 |
|
3 | 6 | export default function DiscourseComment(props) { |
4 | 7 | // eslint-disable-next-line react/prop-types |
5 | | - const { postUrl } = props |
| 8 | + const { postUrl, discourseTopicId, metadata = {} } = props |
| 9 | + const { siteConfig } = useDocusaurusContext() |
| 10 | + const { customFields } = siteConfig |
| 11 | + |
| 12 | + const DISCOURSE_API_KEY = customFields.DISCOURSE_API_KEY |
| 13 | + const DISCOURSE_API_USERNAME = customFields.DISCOURSE_API_USERNAME || 'system' |
| 14 | + const DISCOURSE_CATEGORY_ID = customFields.DISCOURSE_CATEGORY_ID || '6' |
| 15 | + |
| 16 | + const [loading, setLoading] = useState(false) |
| 17 | + const [error, setError] = useState(null) |
| 18 | + const [topicId, setTopicId] = useState(discourseTopicId) |
| 19 | + |
| 20 | + // Extract content from page for topic creation |
| 21 | + const extractContentExcerpt = () => { |
| 22 | + try { |
| 23 | + // Find main content area |
| 24 | + const contentSelector = 'article .markdown, main .markdown, .theme-doc-markdown' |
| 25 | + const contentElement = document.querySelector(contentSelector) |
| 26 | + |
| 27 | + if (!contentElement) { |
| 28 | + return metadata.description || '' |
| 29 | + } |
| 30 | + |
| 31 | + // Get text from first few paragraphs, skip imports and components |
| 32 | + const paragraphs = contentElement.querySelectorAll('p') |
| 33 | + let excerpt = '' |
| 34 | + let wordCount = 0 |
| 35 | + |
| 36 | + for (const p of paragraphs) { |
| 37 | + const text = p.textContent.trim() |
| 38 | + |
| 39 | + // Skip imports, empty paragraphs, and components |
| 40 | + if (!text || text.startsWith('import ') || text.includes('</') || text.includes('>{')) { |
| 41 | + continue |
| 42 | + } |
| 43 | + |
| 44 | + const words = text.split(' ') |
| 45 | + if (wordCount + words.length <= 200) { |
| 46 | + excerpt += (excerpt ? '\n\n' : '') + text |
| 47 | + wordCount += words.length |
| 48 | + } else { |
| 49 | + const remainingWords = 200 - wordCount |
| 50 | + if (remainingWords > 10) { |
| 51 | + excerpt += (excerpt ? '\n\n' : '') + words.slice(0, remainingWords).join(' ') + '...' |
| 52 | + } |
| 53 | + break |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + return excerpt || metadata.description || '' |
| 58 | + } catch (e) { |
| 59 | + console.warn('Failed to extract content:', e) |
| 60 | + return metadata.description || '' |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + // Format post content for Discourse |
| 65 | + const formatDiscourseContent = excerpt => { |
| 66 | + const { title, image, description, tags = [], author, date } = metadata |
| 67 | + const imageUrl = image?.startsWith('http') ? image : `https://docs.metamask.io/${image || ''}` |
| 68 | + |
| 69 | + let content = '' |
| 70 | + |
| 71 | + // Add banner image if available |
| 72 | + if (image) { |
| 73 | + content += `\n\n` |
| 74 | + } |
| 75 | + |
| 76 | + // Add description |
| 77 | + if (description) { |
| 78 | + content += `${description}\n\n` |
| 79 | + } |
| 80 | + |
| 81 | + // Add metadata |
| 82 | + const metaInfo = [] |
| 83 | + if (date) metaInfo.push(`**Published:** ${date}`) |
| 84 | + if (author) metaInfo.push(`**Author:** ${author}`) |
| 85 | + if (tags.length > 0) metaInfo.push(`**Tags:** ${tags.join(', ')}`) |
| 86 | + |
| 87 | + if (metaInfo.length > 0) { |
| 88 | + content += `${metaInfo.join(' | ')}\n\n---\n\n` |
| 89 | + } |
| 90 | + |
| 91 | + // Add excerpt |
| 92 | + if (excerpt) { |
| 93 | + content += `${excerpt}\n\n---\n\n` |
| 94 | + } |
| 95 | + |
| 96 | + // Add link back to docs |
| 97 | + content += `📚 **[Continue reading the full tutorial →](${postUrl})**` |
| 98 | + |
| 99 | + return content |
| 100 | + } |
| 101 | + |
| 102 | + // Search for existing topic by URL using Discourse search API |
| 103 | + const searchExistingTopic = async url => { |
| 104 | + try { |
| 105 | + // Use Discourse search API to find topics with matching embed_url |
| 106 | + const searchQuery = encodeURIComponent(url) |
| 107 | + const response = await fetch(`${DISCOURSE_URL}/search.json?q=${searchQuery}`, { |
| 108 | + method: 'GET', |
| 109 | + headers: { |
| 110 | + 'Api-Key': DISCOURSE_API_KEY, |
| 111 | + 'Api-Username': DISCOURSE_API_USERNAME, |
| 112 | + 'Content-Type': 'application/json', |
| 113 | + }, |
| 114 | + mode: 'cors', |
| 115 | + }) |
| 116 | + |
| 117 | + if (response.ok) { |
| 118 | + const data = await response.json() |
| 119 | + // Look for topics that might match our URL |
| 120 | + const matchingTopic = data.topics?.find( |
| 121 | + topic => topic.title?.includes(metadata.title) || topic.excerpt?.includes(url) |
| 122 | + ) |
| 123 | + return matchingTopic?.id || null |
| 124 | + } |
| 125 | + return null |
| 126 | + } catch (e) { |
| 127 | + console.warn('Failed to search for existing topic:', e) |
| 128 | + return null |
| 129 | + } |
| 130 | + } |
| 131 | + |
| 132 | + // Create new Discourse topic |
| 133 | + const createDiscourseTopic = async () => { |
| 134 | + if (!DISCOURSE_API_KEY) { |
| 135 | + throw new Error('Discourse API key not configured') |
| 136 | + } |
| 137 | + |
| 138 | + const excerpt = extractContentExcerpt() |
| 139 | + const content = formatDiscourseContent(excerpt) |
| 140 | + |
| 141 | + const response = await fetch(`${DISCOURSE_URL}/posts.json`, { |
| 142 | + method: 'POST', |
| 143 | + headers: { |
| 144 | + 'Api-Key': DISCOURSE_API_KEY, |
| 145 | + 'Api-Username': DISCOURSE_API_USERNAME, |
| 146 | + 'Content-Type': 'application/json', |
| 147 | + }, |
| 148 | + mode: 'cors', |
| 149 | + body: JSON.stringify({ |
| 150 | + title: metadata.title || 'Tutorial Discussion', |
| 151 | + raw: content, |
| 152 | + category: DISCOURSE_CATEGORY_ID, |
| 153 | + tags: metadata.tags || [], |
| 154 | + embed_url: postUrl, |
| 155 | + }), |
| 156 | + }) |
| 157 | + |
| 158 | + if (!response.ok) { |
| 159 | + const errorData = await response.json().catch(() => ({})) |
| 160 | + throw new Error(errorData.errors?.[0] || `HTTP ${response.status}`) |
| 161 | + } |
| 162 | + |
| 163 | + const data = await response.json() |
| 164 | + return data.topic_id |
| 165 | + } |
| 166 | + |
| 167 | + // Load or create Discourse embed |
| 168 | + const loadDiscourseEmbed = finalTopicId => { |
| 169 | + // Clean up any existing embed |
| 170 | + const existingScript = document.querySelector('script[src*="embed.js"]') |
| 171 | + if (existingScript) { |
| 172 | + existingScript.remove() |
| 173 | + } |
| 174 | + |
| 175 | + const existingComments = document.getElementById('discourse-comments') |
| 176 | + if (existingComments) { |
| 177 | + existingComments.innerHTML = '' |
| 178 | + } |
| 179 | + |
| 180 | + // Set up Discourse embed configuration |
| 181 | + window.DiscourseEmbed = { |
| 182 | + discourseUrl: `${DISCOURSE_URL}/`, |
| 183 | + discourseEmbedUrl: postUrl, |
| 184 | + ...(finalTopicId && { topicId: finalTopicId }), |
| 185 | + } |
| 186 | + |
| 187 | + // Load embed script |
| 188 | + const script = document.createElement('script') |
| 189 | + script.type = 'text/javascript' |
| 190 | + script.async = true |
| 191 | + script.src = `${DISCOURSE_URL}/javascripts/embed.js` |
| 192 | + |
| 193 | + const targetElement = |
| 194 | + document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0] |
| 195 | + targetElement.appendChild(script) |
| 196 | + } |
| 197 | + |
| 198 | + // Main effect to handle topic creation/loading |
6 | 199 | useEffect(() => { |
7 | | - const url = window.location.href |
8 | | - if (!url.includes('https://metamask.io/')) { |
| 200 | + if (topicId) { |
| 201 | + // Topic ID provided in frontmatter, use it directly |
| 202 | + loadDiscourseEmbed(topicId) |
9 | 203 | return |
10 | | - } else { |
11 | | - window.DiscourseEmbed = { |
12 | | - discourseUrl: 'https://builder.metamask.io/', |
13 | | - discourseEmbedUrl: postUrl, |
14 | | - } |
| 204 | + } |
| 205 | + |
| 206 | + // No topic ID, need to search or create |
| 207 | + const handleTopicCreation = async () => { |
| 208 | + setLoading(true) |
| 209 | + setError(null) |
| 210 | + |
| 211 | + try { |
| 212 | + // Check if API key is configured |
| 213 | + if (!DISCOURSE_API_KEY) { |
| 214 | + console.log('No Discourse API key configured, using basic embed') |
| 215 | + loadDiscourseEmbed() |
| 216 | + return |
| 217 | + } |
15 | 218 |
|
16 | | - const d = document.createElement('script') |
17 | | - d.type = 'text/javascript' |
18 | | - d.async = true |
19 | | - d.src = `${window.DiscourseEmbed.discourseUrl}javascripts/embed.js` |
20 | | - ;( |
21 | | - document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0] |
22 | | - ).appendChild(d) |
| 219 | + console.log('Searching for existing topic...') |
| 220 | + // First, search for existing topic |
| 221 | + let foundTopicId = await searchExistingTopic(postUrl) |
| 222 | + |
| 223 | + if (!foundTopicId) { |
| 224 | + console.log('No existing topic found, creating new one...') |
| 225 | + // No existing topic found, create new one |
| 226 | + foundTopicId = await createDiscourseTopic() |
| 227 | + console.log('Created topic with ID:', foundTopicId) |
| 228 | + } else { |
| 229 | + console.log('Found existing topic with ID:', foundTopicId) |
| 230 | + } |
| 231 | + |
| 232 | + setTopicId(foundTopicId) |
| 233 | + loadDiscourseEmbed(foundTopicId) |
| 234 | + } catch (e) { |
| 235 | + console.error('Discourse integration error:', e) |
| 236 | + |
| 237 | + // Check if it's a CORS or authentication error |
| 238 | + if (e.message.includes('CORS') || e.message.includes('Failed to fetch')) { |
| 239 | + setError('CORS configuration needed - check Discourse admin settings') |
| 240 | + } else { |
| 241 | + setError(e.message) |
| 242 | + } |
| 243 | + |
| 244 | + // Fallback to basic embed without topic creation |
| 245 | + console.log('Falling back to basic embed...') |
| 246 | + loadDiscourseEmbed() |
| 247 | + } finally { |
| 248 | + setLoading(false) |
| 249 | + } |
23 | 250 | } |
24 | | - }, []) |
| 251 | + |
| 252 | + // Add small delay to ensure DOM is ready |
| 253 | + const timer = setTimeout(handleTopicCreation, 100) |
| 254 | + return () => clearTimeout(timer) |
| 255 | + }, [postUrl, topicId]) |
| 256 | + |
| 257 | + if (error && !DISCOURSE_API_KEY) { |
| 258 | + return ( |
| 259 | + <div style={{ padding: '20px', textAlign: 'center', color: '#666' }}> |
| 260 | + <p>💬 Discussion integration not configured.</p> |
| 261 | + <p style={{ fontSize: '0.9em' }}> |
| 262 | + To enable automatic topic creation, configure Discourse API credentials. |
| 263 | + </p> |
| 264 | + </div> |
| 265 | + ) |
| 266 | + } |
25 | 267 |
|
26 | 268 | return ( |
27 | 269 | <> |
28 | | - <meta name="discourse-username" content="shahbaz"></meta> |
| 270 | + <meta name="discourse-username" content="system" /> |
| 271 | + {loading && ( |
| 272 | + <div style={{ padding: '20px', textAlign: 'center', color: '#666' }}> |
| 273 | + <p>🔄 Loading discussion...</p> |
| 274 | + </div> |
| 275 | + )} |
| 276 | + {error && ( |
| 277 | + <div style={{ padding: '20px', textAlign: 'center', color: '#e74c3c' }}> |
| 278 | + <p>⚠️ Failed to load discussion: {error}</p> |
| 279 | + <p style={{ fontSize: '0.9em', color: '#666' }}> |
| 280 | + Comments may still appear below if the topic exists. |
| 281 | + </p> |
| 282 | + </div> |
| 283 | + )} |
29 | 284 | <div id="discourse-comments" /> |
30 | 285 | </> |
31 | 286 | ) |
|
0 commit comments