Add themes, static assets, and logo

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-11 17:38:19 -05:00
parent 5c00a99523
commit 62001d08a4
18 changed files with 2825 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
/**
* Vanilla JS Template Renderer
* Renders posts using HTML template literals
*/
class VanillaRenderer {
constructor() {
this.templates = new Map();
this.formatters = {
formatTime: this.formatTime.bind(this),
formatTimeAgo: this.formatTimeAgo.bind(this),
formatDateTime: this.formatDateTime.bind(this),
truncate: this.truncate.bind(this),
renderMarkdown: this.renderMarkdown.bind(this)
};
}
/**
* Load a template from HTML file
*/
async loadTemplate(templateId, templatePath) {
const response = await fetch(templatePath);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const template = doc.querySelector('template');
if (template) {
this.templates.set(templateId, template.innerHTML);
}
}
/**
* Render a post using a template
*/
render(templateId, postData) {
const template = this.templates.get(templateId);
if (!template) {
throw new Error(`Template ${templateId} not loaded`);
}
// Create context with data and helper functions
const context = { ...postData, ...this.formatters };
// Use Function constructor to evaluate template literal
const rendered = new Function(...Object.keys(context), `return \`${template}\`;`)(...Object.values(context));
return rendered;
}
/**
* Render multiple posts
*/
renderBatch(templateId, posts, container) {
const fragment = document.createDocumentFragment();
posts.forEach(post => {
const html = this.render(templateId, post);
const temp = document.createElement('div');
temp.innerHTML = html;
fragment.appendChild(temp.firstElementChild);
});
container.appendChild(fragment);
}
// Helper functions available in templates
formatTime(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
}
formatTimeAgo(timestamp) {
const seconds = Math.floor(Date.now() / 1000 - timestamp);
const intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60
};
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
const interval = Math.floor(seconds / secondsInUnit);
if (interval >= 1) {
return `${interval} ${unit}${interval !== 1 ? 's' : ''} ago`;
}
}
return 'just now';
}
formatDateTime(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
truncate(text, maxLength) {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength).trim() + '...';
}
renderMarkdown(text) {
// Basic markdown rendering (expand as needed)
return text
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank">$1</a>');
}
}
// Export for use
export default VanillaRenderer;