diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..8c280be Binary files /dev/null and b/logo.png differ diff --git a/themes/modern-card-ui/card-template.html b/themes/modern-card-ui/card-template.html new file mode 100644 index 0000000..2121975 --- /dev/null +++ b/themes/modern-card-ui/card-template.html @@ -0,0 +1,59 @@ + + diff --git a/themes/modern-card-ui/comment-template.html b/themes/modern-card-ui/comment-template.html new file mode 100644 index 0000000..9a900c2 --- /dev/null +++ b/themes/modern-card-ui/comment-template.html @@ -0,0 +1,41 @@ + + diff --git a/themes/modern-card-ui/detail-template.html b/themes/modern-card-ui/detail-template.html new file mode 100644 index 0000000..1799471 --- /dev/null +++ b/themes/modern-card-ui/detail-template.html @@ -0,0 +1,69 @@ + + diff --git a/themes/modern-card-ui/index.html b/themes/modern-card-ui/index.html new file mode 100644 index 0000000..f93e6cf --- /dev/null +++ b/themes/modern-card-ui/index.html @@ -0,0 +1,357 @@ + + + + + + BalanceBoard - Content Feed + {% for css_path in theme.css_dependencies %} + + {% endfor %} + + + + + + +
+ + + + +
+
+
+

{{ filterset_name|title or 'All Posts' }}

+

{{ posts|length }} posts

+
+
+ {% for post in posts %} + {{ post|safe }} + {% endfor %} +
+
+
+
+ + {% for js_path in theme.js_dependencies %} + + {% endfor %} + + + + diff --git a/themes/modern-card-ui/interactions.js b/themes/modern-card-ui/interactions.js new file mode 100644 index 0000000..da64523 --- /dev/null +++ b/themes/modern-card-ui/interactions.js @@ -0,0 +1,121 @@ +// Modern Card UI Theme Interactions + +(function() { + 'use strict'; + + // Enhanced hover effects + function initializeCardHoverEffects() { + const cards = document.querySelectorAll('.card-surface, .list-card, .comment-surface'); + + cards.forEach(card => { + card.addEventListener('mouseenter', function() { + // Subtle scale effect on hover + this.style.transform = 'translateY(-2px)'; + this.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.15)'; + }); + + card.addEventListener('mouseleave', function() { + // Reset transform + this.style.transform = ''; + this.style.boxShadow = ''; + }); + }); + } + + // Lazy loading for performance + function initializeLazyLoading() { + if ('IntersectionObserver' in window) { + const options = { + root: null, + rootMargin: '50px', + threshold: 0.1 + }; + + const observer = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + // Add visible class for animations + entry.target.classList.add('visible'); + + // Unobserve after animation + observer.unobserve(entry.target); + } + }); + }, options); + + // Observe all cards and comments + document.querySelectorAll('.post-card, .comment-card').forEach(card => { + observer.observe(card); + }); + } + } + + // Improved comment thread visibility + function initializeCommentThreading() { + const toggleButtons = document.querySelectorAll('.comment-toggle'); + + toggleButtons.forEach(button => { + button.addEventListener('click', function() { + const comment = this.closest('.comment-card'); + const replies = comment.querySelector('.comment-replies'); + + if (replies) { + replies.classList.toggle('collapsed'); + this.textContent = replies.classList.contains('collapsed') ? '+' : '-'; + } + }); + }); + } + + // Add CSS classes for JavaScript-enhanced features + function initializeThemeFeatures() { + document.documentElement.classList.add('js-enabled'); + + // Add platform-specific classes to body + const platformElements = document.querySelectorAll('[data-platform]'); + const platforms = new Set(); + + platformElements.forEach(el => { + platforms.add(el.dataset.platform); + }); + + platforms.forEach(platform => { + document.body.classList.add(`has-${platform}`); + }); + } + + // Keyboard navigation for accessibility + function initializeKeyboardNavigation() { + const cards = document.querySelectorAll('.post-card, .comment-card'); + + cards.forEach(card => { + card.setAttribute('tabindex', '0'); + + card.addEventListener('keydown', function(e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + const link = this.querySelector('a'); + if (link) { + link.click(); + } + } + }); + }); + } + + // Initialize all features when DOM is ready + function initializeTheme() { + initializeThemeFeatures(); + initializeCardHoverEffects(); + initializeLazyLoading(); + initializeCommentThreading(); + initializeKeyboardNavigation(); + } + + // Run initialization after DOM load + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeTheme); + } else { + initializeTheme(); + } +})(); diff --git a/themes/modern-card-ui/list-template.html b/themes/modern-card-ui/list-template.html new file mode 100644 index 0000000..964caf2 --- /dev/null +++ b/themes/modern-card-ui/list-template.html @@ -0,0 +1,42 @@ + + diff --git a/themes/modern-card-ui/styles.css b/themes/modern-card-ui/styles.css new file mode 100644 index 0000000..5c77688 --- /dev/null +++ b/themes/modern-card-ui/styles.css @@ -0,0 +1,936 @@ +/* BalanceBoard Theme Styles */ +:root { + /* BalanceBoard Color Palette */ + --primary-color: #4DB6AC; + --primary-hover: #26A69A; + --primary-dark: #1B3A52; + --accent-color: #4DB6AC; + --surface-color: #FFFFFF; + --background-color: #F5F5F5; + --surface-elevation-1: rgba(0, 0, 0, 0.05); + --surface-elevation-2: rgba(0, 0, 0, 0.10); + --surface-elevation-3: rgba(0, 0, 0, 0.15); + --text-primary: #1B3A52; + --text-secondary: #757575; + --text-accent: #4DB6AC; + --border-color: rgba(0, 0, 0, 0.12); + --divider-color: rgba(0, 0, 0, 0.08); + --hover-overlay: rgba(77, 182, 172, 0.08); + --active-overlay: rgba(77, 182, 172, 0.16); +} + +/* Global Styles */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background-color: var(--background-color); + color: var(--text-primary); + line-height: 1.6; + margin: 0; + padding: 0; +} + +/* BalanceBoard Navigation */ +.balanceboard-nav { + background: linear-gradient(135deg, var(--primary-dark) 0%, #2a5068 100%); + box-shadow: 0 2px 8px var(--surface-elevation-2); + position: sticky; + top: 0; + z-index: 1000; + border-bottom: 3px solid var(--primary-color); +} + +.nav-container { + max-width: 1200px; + margin: 0 auto; + padding: 16px 24px; + display: flex; + align-items: center; + gap: 24px; +} + +.nav-brand { + display: flex; + align-items: center; + gap: 12px; + text-decoration: none; + color: white; +} + +.nav-logo { + width: 48px; + height: 48px; + border-radius: 50%; + background: white; + padding: 4px; + transition: transform 0.2s ease; +} + +.nav-brand:hover .nav-logo { + transform: scale(1.05); +} + +.nav-brand-text { + font-size: 1.5rem; + font-weight: 300; + letter-spacing: 0.5px; +} + +.nav-brand-text .brand-balance { + color: var(--primary-color); + font-weight: 400; +} + +.nav-brand-text .brand-board { + color: white; + font-weight: 600; +} + +.nav-subtitle { + color: rgba(255, 255, 255, 0.7); + font-size: 0.85rem; + margin-top: -4px; +} + +/* Main Layout */ +.app-layout { + display: flex; + max-width: 1400px; + margin: 0 auto; + gap: 24px; + padding: 24px; + min-height: calc(100vh - 80px); +} + +/* Sidebar */ +.sidebar { + width: 280px; + flex-shrink: 0; + position: sticky; + top: 88px; + height: fit-content; + max-height: calc(100vh - 104px); + overflow-y: auto; +} + +.sidebar-section { + background: var(--surface-color); + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 2px 4px var(--surface-elevation-1); + border: 1px solid var(--border-color); +} + +.sidebar-section h3 { + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-secondary); + margin-bottom: 12px; + font-weight: 600; +} + +/* User Card */ +.user-card { + background: linear-gradient(135deg, var(--primary-dark) 0%, #2a5068 100%); + border-radius: 12px; + padding: 20px; + color: white; + text-align: center; + border: 2px solid var(--primary-color); +} + +.user-avatar { + width: 64px; + height: 64px; + border-radius: 50%; + background: var(--primary-color); + margin: 0 auto 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.75rem; + font-weight: 600; + color: white; +} + +.user-name { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 4px; +} + +.user-karma { + font-size: 0.85rem; + opacity: 0.8; + display: flex; + justify-content: center; + gap: 16px; + margin-top: 12px; +} + +.karma-item { + display: flex; + align-items: center; + gap: 4px; +} + +.login-prompt { + text-align: center; +} + +.login-prompt p { + margin-bottom: 16px; + font-size: 0.95rem; + opacity: 0.9; +} + +.btn-login { + display: block; + width: 100%; + padding: 10px 16px; + background: var(--primary-color); + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + text-align: center; +} + +.btn-login:hover { + background: var(--primary-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(77, 182, 172, 0.3); +} + +.btn-signup { + display: block; + width: 100%; + padding: 10px 16px; + background: transparent; + color: white; + border: 2px solid var(--primary-color); + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + text-align: center; + margin-top: 8px; +} + +.btn-signup:hover { + background: rgba(77, 182, 172, 0.1); + border-color: var(--primary-hover); +} + +/* Navigation Menu */ +.nav-menu { + list-style: none; + padding: 0; + margin: 0; +} + +.nav-menu li { + margin-bottom: 4px; +} + +.nav-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-radius: 8px; + color: var(--text-primary); + text-decoration: none; + transition: all 0.2s ease; + font-size: 0.95rem; +} + +.nav-item:hover { + background: var(--hover-overlay); + color: var(--primary-color); +} + +.nav-item.active { + background: var(--primary-color); + color: white; + font-weight: 600; +} + +.nav-icon { + font-size: 1.25rem; + width: 20px; + text-align: center; +} + +/* Filter Tags */ +.filter-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.filter-tag { + padding: 6px 12px; + background: var(--background-color); + border: 1px solid var(--border-color); + border-radius: 16px; + font-size: 0.85rem; + color: var(--text-primary); + text-decoration: none; + transition: all 0.2s ease; + cursor: pointer; +} + +.filter-tag:hover { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); +} + +.filter-tag.active { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); +} + +/* Main Content Area */ +.main-content { + flex: 1; + min-width: 0; +} + +.container { + max-width: 100%; +} + +/* Platform Colors */ +.platform-reddit { background: linear-gradient(135deg, #FF4500, #FF6B35); color: white; } +.platform-hackernews { background: linear-gradient(135deg, #FF6600, #FF8533); color: white; } +.platform-lobsters { background: linear-gradient(135deg, #8B5A3C, #A0695A); color: white; } + +/* Page Header */ +.container > header { + background: white; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 2px 4px var(--surface-elevation-1); + border-left: 4px solid var(--primary-color); +} + +header h1 { + font-size: 2rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 8px; +} + +header .post-count { + color: var(--text-secondary); + font-size: 1.1rem; + display: flex; + align-items: center; + gap: 8px; +} + +header .post-count::before { + content: "•"; + color: var(--primary-color); + font-size: 1.5rem; + line-height: 1; +} + +/* Post Cards */ +.post-card { + margin-bottom: 16px; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.card-surface { + background: var(--surface-color); + border-radius: 12px; + box-shadow: 0 2px 4px var(--surface-elevation-1); + padding: 24px; + border: 1px solid var(--border-color); + transition: all 0.2s ease; +} + +.card-surface:hover { + box-shadow: 0 4px 12px var(--surface-elevation-2); + transform: translateY(-2px); + background: var(--hover-overlay); +} + +/* Card Header */ +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.header-meta { + display: flex; + gap: 12px; + align-items: center; +} + +.platform-badge { + padding: 4px 8px; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.025em; +} + +.vote-indicator { + display: flex; + align-items: center; + gap: 4px; +} + +.vote-score { + font-size: 1.25rem; + font-weight: 700; + color: var(--text-accent); +} + +/* Card Title */ +.card-title { + font-size: 1.25rem; + font-weight: 600; + line-height: 1.4; + margin-bottom: 12px; +} + +.title-link { + color: var(--text-primary); + text-decoration: none; + transition: color 0.2s ease; +} + +.title-link:hover { + color: var(--primary-color); +} + +/* Content Preview */ +.card-content-preview { + margin-bottom: 16px; +} + +.content-text { + color: var(--text-secondary); + font-size: 0.95rem; + line-height: 1.5; +} + +/* Card Footer */ +.card-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.author-info { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.author-name { + font-weight: 500; + color: var(--text-primary); +} + +.engagement-info { + font-size: 0.9rem; + color: var(--text-secondary); +} + +/* Tags */ +.card-tags { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.tag-chip { + background: var(--primary-color); + color: white; + padding: 4px 8px; + border-radius: 16px; + font-size: 0.8rem; + font-weight: 500; +} + +/* List View */ +.post-list-item { + margin-bottom: 8px; +} + +.list-card { + display: flex; + align-items: center; + background: var(--surface-color); + border-radius: 8px; + padding: 12px 16px; + border: 1px solid var(--border-color); + transition: all 0.2s ease; +} + +.list-card:hover { + background: var(--hover-overlay); + box-shadow: 0 2px 8px var(--surface-elevation-1); +} + +.list-content { + display: flex; + align-items: center; + flex: 1; + gap: 16px; +} + +.list-vote-section { + min-width: 60px; + text-align: center; +} + +.vote-number { + font-size: 1.1rem; + font-weight: 700; + color: var(--text-accent); +} + +.list-meta { + flex: 1; +} + +.list-title { + font-size: 1rem; + font-weight: 500; + margin-bottom: 4px; +} + +.list-details { + display: flex; + justify-content: space-between; + align-items: center; +} + +.list-attribution { + font-size: 0.85rem; + color: var(--text-secondary); + display: flex; + gap: 6px; + align-items: center; +} + +.separator { + color: var(--divider-color); +} + +.list-engagement { + font-size: 0.8rem; + color: var(--text-secondary); +} + +/* Detailed View */ +.post-detail { + background: var(--surface-color); + border-radius: 16px; + box-shadow: 0 4px 12px var(--surface-elevation-1); + margin-bottom: 24px; +} + +.detail-container { + padding: 32px; +} + +.detail-header { + margin-bottom: 32px; +} + +.header-meta-card { + display: flex; + flex-direction: column; + gap: 16px; +} + +.meta-row { + display: flex; + gap: 12px; + align-items: center; +} + +.detail-source { + font-size: 1rem; + color: var(--text-secondary); +} + +.headline-section { + margin-bottom: 24px; +} + +.detail-title { + font-size: 2.5rem; + font-weight: 700; + line-height: 1.2; + color: var(--text-primary); + margin-bottom: 16px; +} + +.byline { + display: flex; + gap: 16px; + font-size: 1rem; + color: var(--text-secondary); +} + +.author-link { + font-weight: 500; + color: var(--primary-color); +} + +.stats-row { + display: flex; + gap: 32px; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; +} + +.stat-number { + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); +} + +.stat-label { + font-size: 0.9rem; + color: var(--text-secondary); + text-transform: lowercase; +} + +.detail-tags { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.tag-pill { + background: var(--primary-color); + color: white; + padding: 6px 12px; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 500; +} + +/* Article Body */ +.article-body { + margin-bottom: 32px; + padding: 24px 0; + border-top: 1px solid var(--divider-color); + border-bottom: 1px solid var(--divider-color); +} + +.article-content { + color: var(--text-secondary); + font-size: 1.1rem; + line-height: 1.8; +} + +.article-content p { + margin-bottom: 16px; +} + +.article-content strong { + font-weight: 600; + color: var(--text-primary); +} + +.article-content img { + max-width: 100%; + height: auto; + border-radius: 8px; + box-shadow: 0 2px 8px var(--surface-elevation-2); + margin: 16px 0; + display: block; +} + +.article-content a { + color: var(--primary-color); + text-decoration: none; + border-bottom: 1px solid transparent; + transition: border-color 0.2s ease; +} + +.article-content a:hover { + border-bottom-color: var(--primary-color); +} + +.article-content em { + font-style: italic; + color: var(--text-secondary); +} + +/* Action Buttons */ +.article-actions { + margin-bottom: 32px; + display: flex; + gap: 16px; +} + +.action-button { + display: inline-flex; + align-items: center; + padding: 12px 24px; + border-radius: 8px; + text-decoration: none; + font-weight: 600; + font-size: 0.95rem; + transition: all 0.2s ease; +} + +.action-button.primary { + background: var(--primary-color); + color: white; + border: none; +} + +.action-button.primary:hover { + background: var(--primary-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(25, 118, 210, 0.3); +} + +/* Comments Section */ +.comments-section { + border-top: 1px solid var(--divider-color); + padding-top: 24px; +} + +.comments-header { + font-size: 1.5rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 24px; +} + +/* Comments */ +.comment-card { + margin-bottom: 16px; + transition: margin-left 0.2s ease; +} + +.comment-surface { + background: var(--surface-color); + border-radius: 8px; + border: 1px solid var(--border-color); + padding: 16px; + box-shadow: 0 1px 3px var(--surface-elevation-1); +} + +.comment-header { + margin-bottom: 8px; +} + +.comment-meta { + display: flex; + gap: 12px; + align-items: center; + font-size: 0.9rem; +} + +.comment-author { + font-weight: 500; + color: var(--text-primary); +} + +.comment-time { + color: var(--text-secondary); +} + +.comment-score { + display: flex; + align-items: center; + gap: 4px; +} + +.score-number { + font-weight: 600; + color: var(--text-secondary); +} + +.comment-body { + font-size: 0.95rem; + line-height: 1.6; + margin-bottom: 8px; +} + +.comment-text { + margin-bottom: 12px; +} + +.comment-text p { + margin-bottom: 8px; +} + +.comment-text img { + max-width: 100%; + height: auto; + border-radius: 6px; + box-shadow: 0 2px 6px var(--surface-elevation-1); + margin: 12px 0; + display: block; +} + +.comment-text a { + color: var(--primary-color); + text-decoration: none; + border-bottom: 1px solid transparent; + transition: border-color 0.2s ease; +} + +.comment-text a:hover { + border-bottom-color: var(--primary-color); +} + +.comment-text strong { + font-weight: 600; + color: var(--text-primary); +} + +.comment-text em { + font-style: italic; +} + +.comment-replies { + border-left: 3px solid var(--divider-color); + margin-left: 16px; + padding-left: 16px; +} + +.comment-footer { + font-size: 0.8rem; +} + +.depth-indicator { + color: var(--text-secondary); +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .app-layout { + flex-direction: column; + padding: 16px; + } + + .sidebar { + width: 100%; + position: static; + max-height: none; + order: -1; + } + + .sidebar-section { + margin-bottom: 12px; + } + + .nav-container { + padding: 12px 16px; + } + + .nav-logo { + width: 40px; + height: 40px; + } + + .nav-brand-text { + font-size: 1.25rem; + } +} + +@media (max-width: 768px) { + .app-layout { + padding: 12px; + gap: 16px; + } + + .container { + padding: 0; + } + + .card-surface { + padding: 16px; + } + + .detail-container { + padding: 16px; + } + + .detail-title { + font-size: 2rem; + } + + .stats-row { + flex-direction: row; + gap: 24px; + } + + .list-card { + padding: 8px 12px; + } + + .list-content { + gap: 8px; + } + + .nav-subtitle { + display: none; + } +} + +/* Sidebar Responsive */ +@media (max-width: 1024px) { + .app-layout { + flex-direction: column; + } + + .sidebar { + width: 100%; + position: static; + max-height: none; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 16px; + } + + .sidebar-section { + margin-bottom: 0; + } +} + +@media (max-width: 640px) { + .sidebar { + grid-template-columns: 1fr; + } +} diff --git a/themes/modern-card-ui/theme.json b/themes/modern-card-ui/theme.json new file mode 100644 index 0000000..ff7f29e --- /dev/null +++ b/themes/modern-card-ui/theme.json @@ -0,0 +1,67 @@ +{ + "template_id": "modern-card-ui-theme", + "template_path": "./themes/modern-card-ui", + "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/modern-card-ui/styles.css" + ], + "js_dependencies": [ + "./themes/modern-card-ui/interactions.js" + ], + "templates": { + "card": "./themes/modern-card-ui/card-template.html", + "list": "./themes/modern-card-ui/list-template.html", + "detail": "./themes/modern-card-ui/detail-template.html", + "comment": "./themes/modern-card-ui/comment-template.html" + }, + "render_options": { + "container_selector": "#posts-container", + "batch_size": 20, + "lazy_load": true, + "animate": true, + "hover_effects": true, + "card_elevation": 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" + ] + }, + "color_scheme": { + "primary": "#1976D2", + "secondary": "#FFFFFF", + "accent": "#FF5722", + "background": "#FAFAFA", + "surface": "#FFFFFF", + "text_primary": "#212121", + "text_secondary": "#757575" + } +} diff --git a/themes/template_prompt.txt b/themes/template_prompt.txt new file mode 100644 index 0000000..12a097d --- /dev/null +++ b/themes/template_prompt.txt @@ -0,0 +1,120 @@ +# Template Creation Prompt for AI + +This document describes the data structures, helper functions, and conventions an AI needs to create or modify HTML templates for this social media archive system. + +## Data Structures Available + +### Post Data (when rendering posts) +- **Available in all post templates (card, list, detail):** + - platform: string (e.g., "reddit", "hackernews") + - id: string (unique post identifier) + - title: string + - author: string + - timestamp: integer (unix timestamp) + - score: integer (up/down vote score) + - replies: integer (number of comments) + - url: string (original post URL) + - content: string (optional post body text) + - source: string (optional subreddit/community) + - tags: array of strings (optional tags/flair) + - meta: object (optional platform-specific metadata) + - comments: array (optional nested comment tree - only in detail templates) + - post_url: string (generated: "{uuid}.html" - for local linking to detail pages) + +### Comment Data (when rendering comments) +- **Available in comment templates:** + - uuid: string (unique comment identifier) + - id: string (platform-specific identifier) + - author: string (comment author username) + - content: string (comment text) + - timestamp: integer (unix timestamp) + - score: integer (comment score) + - platform: string + - depth: integer (nesting level) + - children: array (nested replies) + - children_section: string (pre-rendered HTML of nested children) + +## Template Engine: Jinja2 + +Templates use Jinja2 syntax (`{{ }}` for variables, `{% %}` for control flow). + +### Important Filters: +- `|safe`: Mark content as safe HTML (for already-escaped content) +- Example: `{{ renderMarkdown(content)|safe }}` + +### Available Control Structures: +- `{% if variable %}...{% endif %}` +- `{% for item in array %}...{% endfor %}` +- `{% set variable = value %}` (create local variables) + +## Helper Functions Available + +Call these in templates using `{{ function(arg) }}`: + +### Time/Date Formatting: +- `formatTime(timestamp)` -> "HH:MM" +- `formatTimeAgo(timestamp)` -> "2 hours ago" +- `formatDateTime(timestamp)` -> "January 15, 2024 at 14:30" + +### Text Processing: +- `truncate(text, max_length)` -> truncated string with "..." +- `escapeHtml(text)` -> HTML-escaped version + +### Content Rendering: +- `renderMarkdown(text)` -> Basic HTML from markdown (returns already-escaped HTML) + +## Template Types + +### Card Template (for index/listing pages) +- Used for summary view of posts +- Links should use `post_url` to point to local detail pages +- Keep concise - truncated content, basic info + +### List Template (compact listing) +- Even more compact than cards +- Vote scores, basic metadata, title link + +### Detail Template (full post view) +- Full content, meta information +- Source link uses `url` (external) +- Must include `{{comments_section|safe}}` for rendered comments + +### Comment Template (nested comments) +- Recursive rendering with depth styling +- Children rendered as flattened HTML in `children_section` + +## Convenience Data Added by System + +In `generate_html.py`, `post_url` is added to each post before rendering: `{post['uuid']}.html` + +This allows templates to link to local detail pages instead of external Reddit. + +## CSS Classes Convention + +Templates use semantic CSS classes: +- Post cards: `.post-card`, `.post-header`, `.post-meta`, etc. +- Comments: `.comment`, `.comment-header`, `.comment-body`, etc. +- Platform: `.platform-{platform}` for platform-specific styling + +## Examples + +### Conditional Rendering: +``` +{% if content %} +

{{ renderMarkdown(content)|safe }}

+{% endif %} +``` + +### Looping Tags: +``` +{% for tag in tags if tag %} + {{ tag }} +{% endfor %} +``` + +### Styling by Depth (comments): +``` +
+``` + +When creating new templates, follow these patterns and use the available data and helper functions appropriately. diff --git a/themes/vanilla-js/card-template.html b/themes/vanilla-js/card-template.html new file mode 100644 index 0000000..47bb425 --- /dev/null +++ b/themes/vanilla-js/card-template.html @@ -0,0 +1,42 @@ + + diff --git a/themes/vanilla-js/comment-template.html b/themes/vanilla-js/comment-template.html new file mode 100644 index 0000000..4f1a3cf --- /dev/null +++ b/themes/vanilla-js/comment-template.html @@ -0,0 +1,21 @@ + + diff --git a/themes/vanilla-js/detail-template.html b/themes/vanilla-js/detail-template.html new file mode 100644 index 0000000..5eacd1a --- /dev/null +++ b/themes/vanilla-js/detail-template.html @@ -0,0 +1,52 @@ + + diff --git a/themes/vanilla-js/index.html b/themes/vanilla-js/index.html new file mode 100644 index 0000000..7b43383 --- /dev/null +++ b/themes/vanilla-js/index.html @@ -0,0 +1,357 @@ + + + + + + BalanceBoard - Content Feed + {% for css_path in theme.css_dependencies %} + + {% endfor %} + + + + + + +
+ + + + +
+
+
+

{{ filterset_name|title or 'All Posts' }}

+

{{ posts|length }} posts

+
+
+ {% for post in posts %} + {{ post|safe }} + {% endfor %} +
+
+
+
+ + {% for js_path in theme.js_dependencies %} + + {% endfor %} + + + + diff --git a/themes/vanilla-js/list-template.html b/themes/vanilla-js/list-template.html new file mode 100644 index 0000000..28eeb03 --- /dev/null +++ b/themes/vanilla-js/list-template.html @@ -0,0 +1,22 @@ + + diff --git a/themes/vanilla-js/renderer.js b/themes/vanilla-js/renderer.js new file mode 100644 index 0000000..13afdc8 --- /dev/null +++ b/themes/vanilla-js/renderer.js @@ -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, '

') + .replace(/\n/g, '
') + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/\[(.*?)\]\((.*?)\)/g, '$1'); + } +} + +// Export for use +export default VanillaRenderer; diff --git a/themes/vanilla-js/styles.css b/themes/vanilla-js/styles.css new file mode 100644 index 0000000..2235ac4 --- /dev/null +++ b/themes/vanilla-js/styles.css @@ -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); +} diff --git a/themes/vanilla-js/theme.json b/themes/vanilla-js/theme.json new file mode 100644 index 0000000..f1f5ddc --- /dev/null +++ b/themes/vanilla-js/theme.json @@ -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" + ] + } +}