219 lines
7.4 KiB
JavaScript
219 lines
7.4 KiB
JavaScript
// Blog System - Powered by Gitea Releases
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
const API_URL = '/api/blog/posts';
|
|
const CACHE_KEY = 'pyserve_blog_cache';
|
|
const CACHE_DURATION = 1800000;
|
|
|
|
async function fetchBlogPosts() {
|
|
try {
|
|
const response = await fetch(API_URL, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.warn(`Blog API request failed: ${response.status}`);
|
|
return null;
|
|
}
|
|
|
|
const posts = await response.json();
|
|
console.log(`✓ Fetched ${posts.length} blog posts`);
|
|
return posts;
|
|
|
|
} catch (error) {
|
|
console.error('Blog API fetch error:', error.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getCachedPosts() {
|
|
try {
|
|
const cached = localStorage.getItem(CACHE_KEY);
|
|
if (!cached) return null;
|
|
|
|
const data = JSON.parse(cached);
|
|
const now = Date.now();
|
|
|
|
if (now - data.timestamp < CACHE_DURATION) {
|
|
return data.posts;
|
|
}
|
|
|
|
localStorage.removeItem(CACHE_KEY);
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Cache read error:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function cachePosts(posts) {
|
|
try {
|
|
const data = {
|
|
posts: posts,
|
|
timestamp: Date.now()
|
|
};
|
|
localStorage.setItem(CACHE_KEY, JSON.stringify(data));
|
|
} catch (error) {
|
|
console.error('Cache write error:', error);
|
|
}
|
|
}
|
|
|
|
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 '';
|
|
|
|
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(/^### (.*$)/gim, '<h3>$1</h3>')
|
|
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
|
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
|
|
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>')
|
|
.replace(/\n\n/g, '</p><p>')
|
|
.replace(/\n/g, '<br>');
|
|
|
|
return '<p>' + html + '</p>';
|
|
}
|
|
|
|
function extractExcerpt(markdown, maxLength = 200) {
|
|
if (!markdown) return 'No description available.';
|
|
|
|
// Remove markdown formatting and get first paragraph
|
|
const plain = markdown
|
|
.replace(/^#+\s+.*/gm, '') // Remove headers
|
|
.replace(/\*\*(.+?)\*\*/g, '$1') // Remove bold
|
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links
|
|
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
|
.replace(/`([^`]+)`/g, '$1') // Remove inline code
|
|
.trim();
|
|
|
|
const firstParagraph = plain.split('\n\n')[0] || plain.split('\n')[0];
|
|
|
|
if (firstParagraph.length > maxLength) {
|
|
return firstParagraph.substring(0, maxLength) + '...';
|
|
}
|
|
|
|
return firstParagraph || 'No description available.';
|
|
}
|
|
|
|
function createPostCard(post) {
|
|
const excerpt = extractExcerpt(post.body);
|
|
const date = formatDate(post.published_at || post.created_at);
|
|
const tag = post.tag_name || 'post';
|
|
const author = post.author || {};
|
|
const authorName = author.full_name || author.login || 'Unknown';
|
|
const authorLogin = author.login || '';
|
|
const authorAvatar = author.avatar_url || '';
|
|
|
|
return `
|
|
<article class="blog-post-card">
|
|
<div class="post-meta">
|
|
<span class="post-date">${date}</span>
|
|
<span class="post-tag">${tag}</span>
|
|
</div>
|
|
<h3 class="post-title">
|
|
<a href="blog-post.html?id=${tag}">${post.name || tag}</a>
|
|
</h3>
|
|
<div class="post-excerpt">${excerpt}</div>
|
|
<div class="post-author">
|
|
<img src="${authorAvatar}" alt="${authorName}" class="author-avatar" onerror="this.style.display='none'">
|
|
<div class="author-info">
|
|
<span class="author-name">${authorName}</span>
|
|
${authorLogin ? `<span class="author-login">@${authorLogin}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
<a href="blog-post.html?id=${tag}" class="read-more">Read more →</a>
|
|
</article>
|
|
`;
|
|
}
|
|
|
|
function renderPosts(posts) {
|
|
const container = document.getElementById('blog-posts');
|
|
const loading = document.getElementById('blog-loading');
|
|
const error = document.getElementById('blog-error');
|
|
|
|
if (!container) return;
|
|
|
|
loading.style.display = 'none';
|
|
|
|
if (!posts || posts.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="note">
|
|
<strong>No posts yet</strong>
|
|
<p>Check back soon for updates and articles!</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Sort by date (newest first)
|
|
const sortedPosts = posts.sort((a, b) => {
|
|
const dateA = new Date(a.published_at || a.created_at);
|
|
const dateB = new Date(b.published_at || b.created_at);
|
|
return dateB - dateA;
|
|
});
|
|
|
|
// Render posts
|
|
container.innerHTML = sortedPosts.map(post => createPostCard(post)).join('');
|
|
}
|
|
|
|
function showError() {
|
|
const loading = document.getElementById('blog-loading');
|
|
const error = document.getElementById('blog-error');
|
|
|
|
if (loading) loading.style.display = 'none';
|
|
if (error) error.style.display = 'block';
|
|
}
|
|
|
|
async function init() {
|
|
// Try cached posts first
|
|
const cachedPosts = getCachedPosts();
|
|
if (cachedPosts) {
|
|
renderPosts(cachedPosts);
|
|
}
|
|
|
|
// Fetch fresh posts in background
|
|
const posts = await fetchBlogPosts();
|
|
if (posts) {
|
|
cachePosts(posts);
|
|
renderPosts(posts);
|
|
} else if (!cachedPosts) {
|
|
showError();
|
|
}
|
|
}
|
|
|
|
// Start when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
})();
|