BalanceBoard - Clean release
- Docker deployment ready
- Content aggregation and filtering
- User authentication
- Polling service for updates
🤖 Generated with Claude Code
This commit is contained in:
42
themes/vanilla-js/card-template.html
Normal file
42
themes/vanilla-js/card-template.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!-- Card Template - Jinja2 template -->
|
||||
<template id="post-card-template">
|
||||
<article class="post-card" data-post-id="{{id}}" data-platform="{{platform}}">
|
||||
<header class="post-header">
|
||||
<div class="post-meta">
|
||||
<span class="platform-badge platform-{{platform}}">{{platform}}</span>
|
||||
{% if source %}<span class="post-source">{{source}}</span>{% endif %}
|
||||
</div>
|
||||
<h2 class="post-title">
|
||||
<a href="{{post_url}}" target="_blank" rel="noopener">{{title}}</a>
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div class="post-info">
|
||||
<span class="post-author">by {{author}}</span>
|
||||
<time class="post-time" datetime="{{timestamp}}">{{formatTime(timestamp)}}</time>
|
||||
</div>
|
||||
|
||||
<div class="post-content">
|
||||
{% if content %}<p class="post-excerpt">{{ renderMarkdown(content)|safe }}</p>{% endif %}
|
||||
</div>
|
||||
|
||||
<footer class="post-footer">
|
||||
<div class="post-stats">
|
||||
<span class="stat-score" title="Score">
|
||||
<i class="icon-score">▲</i> {{score}}
|
||||
</span>
|
||||
<span class="stat-replies" title="Replies">
|
||||
<i class="icon-replies">💬</i> {{replies}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if tags %}
|
||||
<div class="post-tags">
|
||||
{% for tag in tags if tag %}
|
||||
<span class="tag">{{tag}}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</article>
|
||||
</template>
|
||||
21
themes/vanilla-js/comment-template.html
Normal file
21
themes/vanilla-js/comment-template.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!-- Comment Template - Nested comment rendering with unlimited depth -->
|
||||
<template id="comment-template">
|
||||
<div class="comment" data-comment-uuid="{{uuid}}" data-depth="{{depth}}" style="margin-left: {{depth * 20}}px">
|
||||
<div class="comment-header">
|
||||
<span class="comment-author">{{author}}</span>
|
||||
<time class="comment-time" datetime="{{timestamp}}">{{formatTimeAgo(timestamp)}}</time>
|
||||
<span class="comment-score" title="Score">↑ {{score}}</span>
|
||||
</div>
|
||||
|
||||
<div class="comment-body">
|
||||
<p class="comment-content">{{renderMarkdown(content)|safe}}</p>
|
||||
</div>
|
||||
|
||||
<div class="comment-footer">
|
||||
<span class="comment-depth-indicator">Depth: {{depth}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Placeholder for nested children -->
|
||||
{{children_section|safe}}
|
||||
</div>
|
||||
</template>
|
||||
52
themes/vanilla-js/detail-template.html
Normal file
52
themes/vanilla-js/detail-template.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!-- Detail Template - Full post view -->
|
||||
<template id="post-detail-template">
|
||||
<article class="post-detail" data-post-id="{{id}}" data-platform="{{platform}}">
|
||||
<header class="detail-header">
|
||||
<div class="breadcrumb">
|
||||
<span class="platform-badge platform-{{platform}}">{{platform}}</span>
|
||||
<span class="separator">/</span>
|
||||
{% if source %}<span class="source-link">{{source}}</span>{% endif %}
|
||||
</div>
|
||||
|
||||
<h1 class="detail-title">{{title}}</h1>
|
||||
|
||||
<div class="detail-meta">
|
||||
<div class="author-info">
|
||||
<span class="author-name">{{author}}</span>
|
||||
<time class="post-time" datetime="{{timestamp}}">{{formatDateTime(timestamp)}}</time>
|
||||
</div>
|
||||
|
||||
<div class="post-stats">
|
||||
<span class="stat-item">
|
||||
<i class="icon-score">▲</i> {{score}} points
|
||||
</span>
|
||||
<span class="stat-item">
|
||||
<i class="icon-replies">💬</i> {{replies}} comments
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if content %}
|
||||
<div class="detail-content">
|
||||
{{ renderMarkdown(content)|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if tags %}
|
||||
<div class="detail-tags">
|
||||
{% for tag in tags if tag %}
|
||||
<span class="tag">{{tag}}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{comments_section|safe}}
|
||||
|
||||
<footer class="detail-footer">
|
||||
<a href="{{url}}" target="_blank" rel="noopener" class="source-link-btn">
|
||||
View on {{platform}}
|
||||
</a>
|
||||
</footer>
|
||||
</article>
|
||||
</template>
|
||||
357
themes/vanilla-js/index.html
Normal file
357
themes/vanilla-js/index.html
Normal file
@@ -0,0 +1,357 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BalanceBoard - Content Feed</title>
|
||||
{% for css_path in theme.css_dependencies %}
|
||||
<link rel="stylesheet" href="{{ css_path }}">
|
||||
{% endfor %}
|
||||
<style>
|
||||
/* Enhanced Navigation Styles */
|
||||
.nav-top {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.5rem 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-top-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.nav-brand-text {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.brand-balance {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.brand-board {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-user-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.nav-user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.nav-username {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.hamburger-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hamburger-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.hamburger-toggle:hover {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.hamburger-line {
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background: var(--text-primary);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.hamburger-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
min-width: 200px;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hamburger-dropdown.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
padding: 0.75rem 1rem;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.dropdown-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.nav-login-prompt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-nav-login, .btn-nav-signup {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-nav-login {
|
||||
background: var(--surface);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-nav-login:hover {
|
||||
background: var(--surface-hover);
|
||||
}
|
||||
|
||||
.btn-nav-signup {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-nav-signup:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.nav-username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Enhanced Top Navigation -->
|
||||
<nav class="nav-top">
|
||||
<div class="nav-top-container">
|
||||
<a href="/" class="nav-brand">
|
||||
<img src="/logo.png" alt="BalanceBoard Logo" class="nav-logo">
|
||||
<div class="nav-brand-text">
|
||||
<span class="brand-balance">balance</span><span class="brand-board">Board</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="nav-user-section">
|
||||
<!-- Logged in user state -->
|
||||
<div class="nav-user-info" style="display: none;">
|
||||
<div class="nav-avatar">JD</div>
|
||||
<span class="nav-username">johndoe</span>
|
||||
<div class="hamburger-menu">
|
||||
<button class="hamburger-toggle" onclick="toggleDropdown()">
|
||||
<div class="hamburger-line"></div>
|
||||
<div class="hamburger-line"></div>
|
||||
<div class="hamburger-line"></div>
|
||||
</button>
|
||||
<div class="hamburger-dropdown" id="userDropdown">
|
||||
<a href="/settings" class="dropdown-item">
|
||||
⚙️ Settings
|
||||
</a>
|
||||
<a href="/settings/profile" class="dropdown-item">
|
||||
👤 Profile
|
||||
</a>
|
||||
<a href="/settings/communities" class="dropdown-item">
|
||||
🌐 Communities
|
||||
</a>
|
||||
<a href="/settings/filters" class="dropdown-item">
|
||||
🎛️ Filters
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="/admin" class="dropdown-item" style="display: none;">
|
||||
🛠️ Admin
|
||||
</a>
|
||||
<div class="dropdown-divider" style="display: none;"></div>
|
||||
<a href="/logout" class="dropdown-item">
|
||||
🚪 Sign Out
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logged out state -->
|
||||
<div class="nav-login-prompt">
|
||||
<a href="/login" class="btn-nav-login">Log In</a>
|
||||
<a href="/signup" class="btn-nav-signup">Sign Up</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="app-layout">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar">
|
||||
<!-- User Card -->
|
||||
<div class="sidebar-section user-card">
|
||||
<div class="login-prompt">
|
||||
<div class="user-avatar">?</div>
|
||||
<p>Join BalanceBoard to customize your feed</p>
|
||||
<a href="/login" class="btn-login">Log In</a>
|
||||
<a href="/signup" class="btn-signup">Sign Up</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="sidebar-section">
|
||||
<h3>Navigation</h3>
|
||||
<ul class="nav-menu">
|
||||
<li><a href="/" class="nav-item active">
|
||||
<span class="nav-icon">🏠</span>
|
||||
<span>Home</span>
|
||||
</a></li>
|
||||
<li><a href="#" class="nav-item">
|
||||
<span class="nav-icon">🔥</span>
|
||||
<span>Popular</span>
|
||||
</a></li>
|
||||
<li><a href="#" class="nav-item">
|
||||
<span class="nav-icon">⭐</span>
|
||||
<span>Saved</span>
|
||||
</a></li>
|
||||
<li><a href="#" class="nav-item">
|
||||
<span class="nav-icon">📊</span>
|
||||
<span>Analytics</span>
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="sidebar-section">
|
||||
<h3>Filter by Platform</h3>
|
||||
<div class="filter-tags">
|
||||
<a href="#" class="filter-tag active">All</a>
|
||||
<a href="#" class="filter-tag">Reddit</a>
|
||||
<a href="#" class="filter-tag">HackerNews</a>
|
||||
<a href="#" class="filter-tag">Lobsters</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- About -->
|
||||
<div class="sidebar-section">
|
||||
<h3>About</h3>
|
||||
<p style="font-size: 0.85rem; color: var(--text-secondary); line-height: 1.5;">
|
||||
BalanceBoard filters and curates content from multiple platforms to help you stay informed.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>{{ filterset_name|title or 'All Posts' }}</h1>
|
||||
<p class="post-count">{{ posts|length }} posts</p>
|
||||
</header>
|
||||
<div id="posts-container">
|
||||
{% for post in posts %}
|
||||
{{ post|safe }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{% for js_path in theme.js_dependencies %}
|
||||
<script src="{{ js_path }}"></script>
|
||||
{% endfor %}
|
||||
|
||||
<script>
|
||||
// Dropdown functionality
|
||||
function toggleDropdown() {
|
||||
const dropdown = document.getElementById('userDropdown');
|
||||
dropdown.classList.toggle('show');
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
const dropdown = document.getElementById('userDropdown');
|
||||
const toggle = document.querySelector('.hamburger-toggle');
|
||||
|
||||
if (!toggle.contains(event.target) && !dropdown.contains(event.target)) {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Check user authentication state (this would be dynamic in a real app)
|
||||
function checkAuthState() {
|
||||
// This would normally check with the server
|
||||
// For now, we'll show the logged out state
|
||||
document.querySelector('.nav-user-info').style.display = 'none';
|
||||
document.querySelector('.nav-login-prompt').style.display = 'flex';
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', checkAuthState);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
22
themes/vanilla-js/list-template.html
Normal file
22
themes/vanilla-js/list-template.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!-- List Template - Compact list view -->
|
||||
<template id="post-list-template">
|
||||
<div class="post-list-item" data-post-id="{{id}}" data-platform="{{platform}}">
|
||||
<div class="post-vote">
|
||||
<span class="vote-score">{{score}}</span>
|
||||
</div>
|
||||
|
||||
<div class="post-main">
|
||||
<h3 class="post-title">
|
||||
<a href="{{post_url}}" target="_blank" rel="noopener">{{title}}</a>
|
||||
</h3>
|
||||
|
||||
<div class="post-metadata">
|
||||
<span class="platform-badge platform-{{platform}}">{{platform}}</span>
|
||||
{% if source %}<span class="post-source">{{source}}</span>{% endif %}
|
||||
<span class="post-author">u/{{author}}</span>
|
||||
<time class="post-time" datetime="{{timestamp}}">{{formatTimeAgo(timestamp)}}</time>
|
||||
<span class="post-replies">{{replies}} comments</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
127
themes/vanilla-js/renderer.js
Normal file
127
themes/vanilla-js/renderer.js
Normal 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;
|
||||
336
themes/vanilla-js/styles.css
Normal file
336
themes/vanilla-js/styles.css
Normal file
@@ -0,0 +1,336 @@
|
||||
/* Vanilla JS Theme Styles */
|
||||
|
||||
:root {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f6f7f8;
|
||||
--bg-hover: #f0f1f2;
|
||||
--text-primary: #1c1c1c;
|
||||
--text-secondary: #7c7c7c;
|
||||
--border-color: #e0e0e0;
|
||||
--accent-reddit: #ff4500;
|
||||
--accent-hn: #ff6600;
|
||||
--accent-lobsters: #990000;
|
||||
--accent-se: #0077cc;
|
||||
}
|
||||
|
||||
/* Card Template Styles */
|
||||
.post-card {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
transition: box-shadow 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.post-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.post-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.platform-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.platform-reddit { background: var(--accent-reddit); color: white; }
|
||||
.platform-hackernews { background: var(--accent-hn); color: white; }
|
||||
.platform-lobsters { background: var(--accent-lobsters); color: white; }
|
||||
.platform-stackexchange { background: var(--accent-se); color: white; }
|
||||
|
||||
.post-source {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.post-title a {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-title a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.post-info {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.post-content {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.post-excerpt {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.post-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.post-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stat-score,
|
||||
.stat-replies {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.post-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: var(--bg-secondary);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* List Template Styles */
|
||||
.post-list-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.post-list-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.post-vote {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.vote-score {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.post-main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.post-list-item .post-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.post-metadata {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Detail Template Styles */
|
||||
.post-detail {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 32px;
|
||||
line-height: 1.3;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.detail-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
line-height: 1.8;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.detail-content p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.detail-footer {
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.source-link-btn {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background: var(--accent-reddit);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.source-link-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Comment Styles */
|
||||
.comments-section {
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.comment {
|
||||
background: var(--bg-primary);
|
||||
border-left: 2px solid var(--border-color);
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.comment:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.comment-author {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.comment-score {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.comment-body {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.comment-content p {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.comment-footer {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.comment-depth-indicator {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.comment-children {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Depth-based styling */
|
||||
.comment[data-depth="0"] {
|
||||
border-left-color: var(--accent-reddit);
|
||||
}
|
||||
|
||||
.comment[data-depth="1"] {
|
||||
border-left-color: var(--accent-hn);
|
||||
}
|
||||
|
||||
.comment[data-depth="2"] {
|
||||
border-left-color: var(--accent-lobsters);
|
||||
}
|
||||
|
||||
.comment[data-depth="3"] {
|
||||
border-left-color: var(--accent-se);
|
||||
}
|
||||
56
themes/vanilla-js/theme.json
Normal file
56
themes/vanilla-js/theme.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"template_id": "vanilla-js-theme",
|
||||
"template_path": "./themes/vanilla-js",
|
||||
"template_type": "card",
|
||||
"data_schema": "../../schemas/post_schema.json",
|
||||
"required_fields": [
|
||||
"platform",
|
||||
"id",
|
||||
"title",
|
||||
"author",
|
||||
"timestamp",
|
||||
"score",
|
||||
"replies",
|
||||
"url"
|
||||
],
|
||||
"optional_fields": [
|
||||
"content",
|
||||
"source",
|
||||
"tags",
|
||||
"meta"
|
||||
],
|
||||
"css_dependencies": [
|
||||
"./themes/vanilla-js/styles.css"
|
||||
],
|
||||
"js_dependencies": [
|
||||
"./themes/vanilla-js/renderer.js"
|
||||
],
|
||||
"templates": {
|
||||
"card": "./themes/vanilla-js/card-template.html",
|
||||
"list": "./themes/vanilla-js/list-template.html",
|
||||
"detail": "./themes/vanilla-js/detail-template.html",
|
||||
"comment": "./themes/vanilla-js/comment-template.html"
|
||||
},
|
||||
"render_options": {
|
||||
"container_selector": "#posts-container",
|
||||
"batch_size": 50,
|
||||
"lazy_load": true,
|
||||
"animate": true
|
||||
},
|
||||
"filters": {
|
||||
"platform": true,
|
||||
"date_range": true,
|
||||
"score_threshold": true,
|
||||
"source": true
|
||||
},
|
||||
"sorting": {
|
||||
"default_field": "timestamp",
|
||||
"default_order": "desc",
|
||||
"available_fields": [
|
||||
"timestamp",
|
||||
"score",
|
||||
"replies",
|
||||
"title"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user