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:
59
themes/modern-card-ui/card-template.html
Normal file
59
themes/modern-card-ui/card-template.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<!-- Modern Card UI - Post Card Template -->
|
||||||
|
<template id="modern-card-template">
|
||||||
|
<article class="post-card" data-post-id="{{id}}" data-platform="{{platform}}">
|
||||||
|
<div class="card-surface">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="card-header">
|
||||||
|
<div class="header-meta">
|
||||||
|
<span class="platform-badge platform-{{platform}}">{{platform|upper}}</span>
|
||||||
|
{% if source %}
|
||||||
|
<span class="card-source">{{source}}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vote-indicator">
|
||||||
|
<span class="vote-score">{{score}}</span>
|
||||||
|
<span class="vote-label">pts</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="card-title-section">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<a href="{{post_url}}" class="title-link">{{title}}</a>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content Preview -->
|
||||||
|
{% if content %}
|
||||||
|
<div class="card-content-preview">
|
||||||
|
<p class="content-text">{{ truncate(content, 150) }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="card-footer">
|
||||||
|
<div class="author-info">
|
||||||
|
<span class="author-name">{{author}}</span>
|
||||||
|
<span class="post-time">{{formatTimeAgo(timestamp)}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="engagement-info">
|
||||||
|
<span class="reply-count">{{replies}} replies</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Tags -->
|
||||||
|
{% if tags %}
|
||||||
|
<div class="card-tags">
|
||||||
|
{% for tag in tags[:3] if tag %}
|
||||||
|
<span class="tag-chip">{{tag}}</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% if tags|length > 3 %}
|
||||||
|
<span class="tag-more">+{{tags|length - 3}} more</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
41
themes/modern-card-ui/comment-template.html
Normal file
41
themes/modern-card-ui/comment-template.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<!-- Modern Card UI - Comment Template -->
|
||||||
|
<template id="modern-comment-template">
|
||||||
|
<div class="comment-card" data-comment-id="{{uuid}}" style="margin-left: {{depth * 24}}px">
|
||||||
|
<div class="comment-surface">
|
||||||
|
<!-- Comment Header -->
|
||||||
|
<header class="comment-header">
|
||||||
|
<div class="comment-meta">
|
||||||
|
<span class="comment-author">{{author}}</span>
|
||||||
|
<span class="comment-time">{{formatTimeAgo(timestamp)}}</span>
|
||||||
|
{% if score != 0 %}
|
||||||
|
<div class="comment-score">
|
||||||
|
<span class="score-number">{{score}}</span>
|
||||||
|
<span class="score-label">pts</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Comment Content -->
|
||||||
|
<div class="comment-body">
|
||||||
|
<div class="comment-text">
|
||||||
|
{{ renderMarkdown(content)|safe }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if children_section %}
|
||||||
|
<!-- Nested replies section -->
|
||||||
|
<div class="comment-replies">
|
||||||
|
{{children_section|safe}}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Comment Footer (for actions) -->
|
||||||
|
<footer class="comment-footer">
|
||||||
|
<div class="depth-indicator" data-depth="{{depth}}">
|
||||||
|
<span class="depth-label">Level {{depth + 1}}</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
69
themes/modern-card-ui/detail-template.html
Normal file
69
themes/modern-card-ui/detail-template.html
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<!-- Modern Card UI - Post Detail Template -->
|
||||||
|
<template id="modern-detail-template">
|
||||||
|
<article class="post-detail" data-post-id="{{id}}" data-platform="{{platform}}">
|
||||||
|
<div class="detail-container">
|
||||||
|
<!-- Header Card -->
|
||||||
|
<header class="detail-header">
|
||||||
|
<div class="header-meta-card">
|
||||||
|
<div class="meta-row">
|
||||||
|
<span class="platform-badge platform-{{platform}}">{{platform|upper}}</span>
|
||||||
|
{% if source %}
|
||||||
|
<span class="detail-source">in {{source}}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="headline-section">
|
||||||
|
<h1 class="detail-title">{{title}}</h1>
|
||||||
|
<div class="byline">
|
||||||
|
<span class="author-link">by {{author}}</span>
|
||||||
|
<span class="publication-time">{{formatDateTime(timestamp)}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-row">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-number">{{score}}</span>
|
||||||
|
<span class="stat-label">points</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-number">{{replies}}</span>
|
||||||
|
<span class="stat-label">comments</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if tags %}
|
||||||
|
<div class="detail-tags">
|
||||||
|
{% for tag in tags if tag %}
|
||||||
|
<span class="tag-pill">{{tag}}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Article Body -->
|
||||||
|
{% if content %}
|
||||||
|
<section class="article-body">
|
||||||
|
<div class="article-content">
|
||||||
|
{{ renderMarkdown(content)|safe }}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Action Row -->
|
||||||
|
<div class="article-actions">
|
||||||
|
<a href="{{url}}" target="_blank" class="action-button primary">
|
||||||
|
View Original
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Comments Section -->
|
||||||
|
{% if comments_section %}
|
||||||
|
<section class="comments-section">
|
||||||
|
<h2 class="comments-header">Comments ({{replies}})</h2>
|
||||||
|
{{comments_section|safe}}
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
357
themes/modern-card-ui/index.html
Normal file
357
themes/modern-card-ui/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-color);
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
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(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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(--primary-color);
|
||||||
|
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(--hover-overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-color);
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
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(--divider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background: var(--hover-overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--divider-color);
|
||||||
|
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);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-nav-login:hover {
|
||||||
|
background: var(--hover-overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-nav-signup {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-nav-signup:hover {
|
||||||
|
background: var(--primary-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>
|
||||||
121
themes/modern-card-ui/interactions.js
Normal file
121
themes/modern-card-ui/interactions.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
42
themes/modern-card-ui/list-template.html
Normal file
42
themes/modern-card-ui/list-template.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<!-- Modern Card UI - Post List Template -->
|
||||||
|
<template id="modern-list-template">
|
||||||
|
<div class="post-list-item" data-post-id="{{id}}" data-platform="{{platform}}">
|
||||||
|
<div class="list-card">
|
||||||
|
<!-- Platform indicator -->
|
||||||
|
<div class="list-platform">
|
||||||
|
<span class="platform-badge medium platform-{{platform}}">{{platform[:1]|upper}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<div class="list-content">
|
||||||
|
<div class="list-vote-section">
|
||||||
|
<div class="vote-display">
|
||||||
|
<span class="vote-number">{{score}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-meta">
|
||||||
|
<h3 class="list-title">
|
||||||
|
<a href="{{post_url}}" class="title-link">{{title}}</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="list-details">
|
||||||
|
<div class="list-attribution">
|
||||||
|
{% if source %}
|
||||||
|
<span class="list-source">{{source}}</span>
|
||||||
|
<span class="separator">•</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="list-author">{{author}}</span>
|
||||||
|
<span class="separator">•</span>
|
||||||
|
<span class="list-time">{{formatTimeAgo(timestamp)}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-engagement">
|
||||||
|
<span class="replies-indicator">{{replies}} replies</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
936
themes/modern-card-ui/styles.css
Normal file
936
themes/modern-card-ui/styles.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
themes/modern-card-ui/theme.json
Normal file
67
themes/modern-card-ui/theme.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
120
themes/template_prompt.txt
Normal file
120
themes/template_prompt.txt
Normal file
@@ -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 %}
|
||||||
|
<p class="content">{{ renderMarkdown(content)|safe }}</p>
|
||||||
|
{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Looping Tags:
|
||||||
|
```
|
||||||
|
{% for tag in tags if tag %}
|
||||||
|
<span class="tag">{{ tag }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Styling by Depth (comments):
|
||||||
|
```
|
||||||
|
<div class="comment" style="margin-left: {{depth * 20}}px">
|
||||||
|
```
|
||||||
|
|
||||||
|
When creating new templates, follow these patterns and use the available data and helper functions appropriately.
|
||||||
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