/** * @file BlogPostNode.js * @UCLASS(BlueprintType, Blueprintable) */ import { NodeBase } from './NodeBase.js'; import { NodeRegistry } from './NodeRegistry.js'; /** * @UCLASS(BlueprintType) * Displays a blog post with metadata (title, date, author) and markdown content. */ export class BlogPostNode extends NodeBase { /** @UCLASS(BlueprintType) */ static NodeType = "blog_post"; /** * @UFUNCTION(BlueprintPure) */ static BlueprintPure_GetDefaultPins() { return { inputs: [], outputs: [] }; } /** * @UFUNCTION(BlueprintNativeEvent) * @param {HTMLElement} article * @param {import('../GraphNode.js').GraphNode} graphNode * @param {import('./NodeRenderContext.js').NodeRenderContext} renderCtx */ BlueprintNativeEvent_OnRender(article, graphNode, renderCtx) { const body = document.createElement("div"); body.className = "node-body"; body.setAttribute("aria-live", "polite"); // Load blog data const loadBlogData = async () => { try { const response = await fetch('data/blogs.json'); const blogs = await response.json(); // Find the blog post by slug const post = blogs.find(b => b.slug === graphNode.blogSlug); if (!post) { body.innerHTML = `
Blog post "${graphNode.blogSlug}" not found.
`; return; } // Render metadata header const metaDiv = document.createElement("div"); metaDiv.className = "blog-meta"; metaDiv.style.cssText = "margin-bottom: 1em; padding-bottom: 0.5em; border-bottom: 1px solid rgba(255,255,255,0.1);"; if (post.title) { const titleEl = document.createElement("h2"); titleEl.textContent = post.title; titleEl.style.cssText = "margin: 0 0 0.5em 0; font-size: 1.5em;"; metaDiv.appendChild(titleEl); } const metaInfo = []; if (post.date) { const dateObj = new Date(post.date); metaInfo.push(`📅 ${dateObj.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}`); } if (post.author) { metaInfo.push(`✍️ ${post.author}`); } if (metaInfo.length > 0) { const infoEl = document.createElement("div"); infoEl.style.cssText = "color: rgba(255,255,255,0.6); font-size: 0.9em;"; infoEl.textContent = metaInfo.join(' • '); metaDiv.appendChild(infoEl); } if (post.tags && post.tags.length > 0) { const tagsDiv = document.createElement("div"); tagsDiv.style.cssText = "margin-top: 0.5em;"; post.tags.forEach(tag => { const tagEl = document.createElement("span"); tagEl.textContent = `#${tag}`; tagEl.style.cssText = "display: inline-block; margin-right: 0.5em; padding: 0.2em 0.5em; background: rgba(100,150,255,0.2); border-radius: 3px; font-size: 0.85em;"; tagsDiv.appendChild(tagEl); }); metaDiv.appendChild(tagsDiv); } body.appendChild(metaDiv); // Render markdown content directly const contentDiv = document.createElement("div"); contentDiv.className = "blog-content"; body.appendChild(contentDiv); // Import and use MarkdownRenderer directly const { MarkdownRenderer } = await import('../MarkdownRenderer.js'); contentDiv.innerHTML = MarkdownRenderer.render(post.content); MarkdownRenderer.applyImageScale(contentDiv); // Add copy button handlers contentDiv.querySelectorAll('.md-copy-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const code = btn.getAttribute('data-code'); if (code) { navigator.clipboard.writeText(code).then(() => { const originalText = btn.textContent; btn.textContent = '✓'; btn.classList.add('copied'); setTimeout(() => { btn.textContent = originalText; btn.classList.remove('copied'); }, 2000); }).catch(err => { console.error('Failed to copy:', err); }); } }); }); } catch (error) { body.innerHTML = `Error loading blog post: ${error.message}
`; console.error('Failed to load blog post:', error); } }; article.appendChild(body); loadBlogData(); } } NodeRegistry.UCLASS_Register(BlogPostNode);