180 lines
5.9 KiB
JavaScript
180 lines
5.9 KiB
JavaScript
// Blog Post Viewer
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function getPostId() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
return params.get('id');
|
|
}
|
|
|
|
async function fetchPost(postId) {
|
|
try {
|
|
const response = await fetch(`/api/blog/post/${postId}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.warn(`Post API request failed: ${response.status}`);
|
|
return null;
|
|
}
|
|
|
|
const post = await response.json();
|
|
console.log(`✓ Fetched post: ${post.name}`);
|
|
return post;
|
|
|
|
} catch (error) {
|
|
console.error('Post API fetch error:', error.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format date to readable string
|
|
*/
|
|
function formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
const options = { year: 'numeric', month: 'long', day: 'numeric' };
|
|
return date.toLocaleDateString('en-US', options);
|
|
}
|
|
|
|
function parseMarkdown(markdown) {
|
|
if (!markdown) return '<p>No content available.</p>';
|
|
|
|
if (typeof marked !== 'undefined' && typeof hljs !== 'undefined') {
|
|
marked.setOptions({
|
|
highlight: function(code, lang) {
|
|
if (lang && hljs.getLanguage(lang)) {
|
|
return hljs.highlight(code, { language: lang }).value;
|
|
}
|
|
return hljs.highlightAuto(code).value;
|
|
},
|
|
breaks: true,
|
|
gfm: true
|
|
});
|
|
return marked.parse(markdown);
|
|
}
|
|
|
|
let html = markdown
|
|
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>')
|
|
.replace(/^#### (.*$)/gim, '<h4>$1</h4>')
|
|
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
|
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
|
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
|
.replace(/^---$/gm, '<hr>')
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
|
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width: 100%; height: auto;">')
|
|
.replace(/^\* (.+)$/gm, '<li>$1</li>')
|
|
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
|
.replace(/^\d+\. (.+)$/gm, '<li>$1</li>')
|
|
.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
|
|
.split('\n\n')
|
|
.map(para => {
|
|
para = para.trim();
|
|
if (para.match(/^<(h[1-6]|ul|ol|pre|blockquote|hr)/)) {
|
|
return para;
|
|
}
|
|
return para ? `<p>${para}</p>` : '';
|
|
})
|
|
.join('\n');
|
|
|
|
return html;
|
|
}
|
|
|
|
/**
|
|
* Render post to DOM
|
|
*/
|
|
function renderPost(post) {
|
|
const loading = document.getElementById('post-loading');
|
|
const content = document.getElementById('post-content');
|
|
const error = document.getElementById('post-error');
|
|
|
|
if (!post) {
|
|
loading.style.display = 'none';
|
|
error.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
loading.style.display = 'none';
|
|
content.style.display = 'block';
|
|
|
|
// Update page title
|
|
document.getElementById('page-title').textContent = `${post.name} - pyserve`;
|
|
document.getElementById('breadcrumb-title').textContent = post.name;
|
|
|
|
// Update meta
|
|
const date = formatDate(post.published_at || post.created_at);
|
|
document.getElementById('post-date').textContent = date;
|
|
document.getElementById('post-tag').textContent = post.tag_name || 'post';
|
|
|
|
// Update title
|
|
document.getElementById('post-title').textContent = post.name;
|
|
|
|
// Parse and render body
|
|
const bodyHtml = parseMarkdown(post.body);
|
|
document.getElementById('post-body').innerHTML = bodyHtml;
|
|
|
|
// Update author info
|
|
if (post.author) {
|
|
const authorSection = document.getElementById('post-author-full');
|
|
const avatarImg = document.getElementById('author-avatar');
|
|
const authorName = document.getElementById('author-name');
|
|
const authorLogin = document.getElementById('author-login');
|
|
|
|
if (avatarImg) {
|
|
avatarImg.src = post.author.avatar_url || 'https://via.placeholder.com/80';
|
|
avatarImg.alt = post.author.full_name || post.author.login;
|
|
}
|
|
if (authorName) authorName.textContent = post.author.full_name || post.author.login;
|
|
if (authorLogin) authorLogin.textContent = `@${post.author.login}`;
|
|
|
|
authorSection.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show error state
|
|
*/
|
|
function showError() {
|
|
const loading = document.getElementById('post-loading');
|
|
const error = document.getElementById('post-error');
|
|
|
|
loading.style.display = 'none';
|
|
error.style.display = 'block';
|
|
}
|
|
|
|
/**
|
|
* Main initialization
|
|
*/
|
|
async function init() {
|
|
const postId = getPostId();
|
|
|
|
if (!postId) {
|
|
showError();
|
|
return;
|
|
}
|
|
|
|
const post = await fetchPost(postId);
|
|
|
|
if (post) {
|
|
renderPost(post);
|
|
} else {
|
|
showError();
|
|
}
|
|
}
|
|
|
|
// Start when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
})();
|