- Add build_comment_tree() to organize comments hierarchically - Create recursive Jinja macro to render nested comments - Add visual styling with left border and indentation - Comments now display as threaded tree structure Fixes #10
709 lines
16 KiB
HTML
709 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ post.title }} - BalanceBoard{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Modern Top Navigation -->
|
|
<nav class="top-nav">
|
|
<div class="nav-content">
|
|
<div class="nav-left">
|
|
<div class="logo-section">
|
|
<img src="{{ url_for('serve_logo') }}" alt="BalanceBoard" class="nav-logo">
|
|
<span class="brand-text"><span class="brand-balance">balance</span><span class="brand-board">Board</span></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nav-center">
|
|
<div class="search-bar">
|
|
<input type="text" placeholder="Search content..." class="search-input">
|
|
<button class="search-btn">🔍</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nav-right">
|
|
{% if current_user.is_authenticated %}
|
|
<div class="user-menu">
|
|
<div class="user-info">
|
|
<div class="user-avatar">
|
|
{% if current_user.profile_picture_url %}
|
|
<img src="{{ current_user.profile_picture_url }}" alt="Avatar">
|
|
{% else %}
|
|
<div class="avatar-placeholder">{{ current_user.username[:2].upper() }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<span class="username">{{ current_user.username }}</span>
|
|
</div>
|
|
<div class="user-dropdown">
|
|
<a href="{{ url_for('settings') }}" class="dropdown-item">⚙️ Settings</a>
|
|
{% if current_user.is_admin %}
|
|
<a href="{{ url_for('admin_panel') }}" class="dropdown-item">👨💼 Admin Panel</a>
|
|
{% endif %}
|
|
<a href="{{ url_for('logout') }}" class="dropdown-item">🚪 Logout</a>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="auth-buttons">
|
|
<a href="{{ url_for('login') }}" class="auth-btn">Login</a>
|
|
<a href="{{ url_for('signup') }}" class="auth-btn primary">Sign Up</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main Content Area -->
|
|
<main class="main-content single-post">
|
|
<!-- Back Button -->
|
|
<div class="back-section">
|
|
<button onclick="goBackToFeed()" class="back-btn">← Back to Feed</button>
|
|
</div>
|
|
|
|
<!-- Post Content -->
|
|
<article class="post-detail">
|
|
<div class="post-header">
|
|
<div class="platform-badge platform-{{ post.platform }}">
|
|
{{ post.platform.title()[:1] }}
|
|
</div>
|
|
<div class="post-meta">
|
|
<span class="post-author">{{ post.author }}</span>
|
|
<span class="post-separator">•</span>
|
|
{% if post.source %}
|
|
<span class="post-source">{{ post.source_display if post.source_display else ('r/' + post.source if post.platform == 'reddit' else post.source) }}</span>
|
|
<span class="post-separator">•</span>
|
|
{% endif %}
|
|
<span class="post-time">{{ moment(post.timestamp).fromNow() if moment else 'Recently' }}</span>
|
|
{% if post.url and not post.url.startswith('/') %}
|
|
<span class="external-link-indicator">🔗</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if post.url and not post.url.startswith('/') %}
|
|
<h1 class="post-title">
|
|
<a href="{{ post.url }}" target="_blank" class="post-title-link">{{ post.title }}</a>
|
|
</h1>
|
|
{% else %}
|
|
<h1 class="post-title">{{ post.title }}</h1>
|
|
{% endif %}
|
|
|
|
{% if post.content %}
|
|
<div class="post-content">
|
|
{{ post.content | safe | nl2br }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if post.url and not post.url.startswith('/') %}
|
|
<div class="external-link">
|
|
<a href="{{ post.url }}" target="_blank" class="external-btn">
|
|
{% if post.platform == 'reddit' %}
|
|
🔺 View on Reddit
|
|
{% elif post.platform == 'hackernews' %}
|
|
🧮 View on Hacker News
|
|
{% elif post.platform == 'lobsters' %}
|
|
🦞 View on Lobsters
|
|
{% elif post.platform == 'github' %}
|
|
🐙 View on GitHub
|
|
{% elif post.platform == 'devto' %}
|
|
📝 View on Dev.to
|
|
{% elif post.platform == 'stackoverflow' %}
|
|
📚 View on Stack Overflow
|
|
{% else %}
|
|
🔗 View Original Source
|
|
{% endif %}
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="post-footer">
|
|
<div class="post-stats">
|
|
<div class="post-score">
|
|
<span>▲</span>
|
|
<span>{{ post.score }}</span>
|
|
</div>
|
|
<div class="post-comments">
|
|
<span>💬</span>
|
|
<span>{{ comments|length }} comments</span>
|
|
</div>
|
|
</div>
|
|
<div class="post-actions">
|
|
<button class="post-action" onclick="sharePost()">Share</button>
|
|
<button class="post-action" onclick="savePost()">Save</button>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<!-- Comments Section -->
|
|
<section class="comments-section">
|
|
<h2>Comments ({{ comments|length }})</h2>
|
|
|
|
{% macro render_comment(comment, depth=0) %}
|
|
<div class="comment" style="margin-left: {{ depth * 24 }}px;">
|
|
<div class="comment-header">
|
|
<span class="comment-author">{{ comment.author }}</span>
|
|
<span class="comment-separator">•</span>
|
|
<span class="comment-time">{{ moment(comment.timestamp).fromNow() if moment else 'Recently' }}</span>
|
|
</div>
|
|
<div class="comment-content">
|
|
{{ comment.content | safe | nl2br }}
|
|
</div>
|
|
<div class="comment-footer">
|
|
<div class="comment-score">
|
|
<span>▲ {{ comment.score or 0 }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{% if comment.replies %}
|
|
<div class="comment-replies">
|
|
{% for reply in comment.replies %}
|
|
{{ render_comment(reply, depth + 1) }}
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% if comments %}
|
|
<div class="comments-list">
|
|
{% for comment in comments %}
|
|
{{ render_comment(comment) }}
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="no-comments">
|
|
<p>No comments yet. Be the first to share your thoughts!</p>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
</main>
|
|
|
|
<style>
|
|
/* Inherit styles from dashboard */
|
|
/* Top Navigation */
|
|
.top-nav {
|
|
background: white;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1000;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.nav-content {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 24px;
|
|
height: 64px;
|
|
}
|
|
|
|
.nav-left .logo-section {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.nav-logo {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.brand-text {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.brand-balance {
|
|
color: #4db6ac;
|
|
}
|
|
|
|
.brand-board {
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.search-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
background: #f8fafc;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: 12px;
|
|
padding: 8px 16px;
|
|
min-width: 400px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.search-bar:focus-within {
|
|
border-color: #4db6ac;
|
|
box-shadow: 0 0 0 3px rgba(77, 182, 172, 0.1);
|
|
}
|
|
|
|
.search-input {
|
|
flex: 1;
|
|
border: none;
|
|
background: none;
|
|
outline: none;
|
|
font-size: 14px;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.search-btn {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.user-menu {
|
|
position: relative;
|
|
}
|
|
|
|
.user-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
cursor: pointer;
|
|
padding: 8px 16px;
|
|
border-radius: 12px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.user-info:hover {
|
|
background: #f1f5f9;
|
|
}
|
|
|
|
.user-avatar img, .avatar-placeholder {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.avatar-placeholder {
|
|
background: linear-gradient(135deg, #4db6ac, #26a69a);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.username {
|
|
font-weight: 500;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.user-dropdown {
|
|
position: absolute;
|
|
top: 100%;
|
|
right: 0;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
|
padding: 8px 0;
|
|
min-width: 180px;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transform: translateY(-10px);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.user-menu:hover .user-dropdown {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.dropdown-item {
|
|
display: block;
|
|
padding: 12px 20px;
|
|
color: #2c3e50;
|
|
text-decoration: none;
|
|
font-size: 14px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.dropdown-item:hover {
|
|
background: #f8fafc;
|
|
color: #4db6ac;
|
|
}
|
|
|
|
.auth-buttons {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
|
|
.auth-btn {
|
|
padding: 8px 16px;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.auth-btn:not(.primary) {
|
|
color: #2c3e50;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.auth-btn.primary {
|
|
background: #4db6ac;
|
|
color: white;
|
|
}
|
|
|
|
.auth-btn:hover {
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
/* Main Content */
|
|
.main-content.single-post {
|
|
max-width: 1200px;
|
|
width: 100%;
|
|
margin: 0 auto;
|
|
padding: 24px;
|
|
min-height: calc(100vh - 64px);
|
|
}
|
|
|
|
.back-section {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.back-btn {
|
|
background: #f8fafc;
|
|
border: 1px solid #e2e8f0;
|
|
padding: 8px 16px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
color: #2c3e50;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.back-btn:hover {
|
|
background: #e2e8f0;
|
|
}
|
|
|
|
/* Post Detail */
|
|
.post-detail {
|
|
background: white;
|
|
border-radius: 16px;
|
|
padding: 32px;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.post-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.platform-badge {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
color: white;
|
|
}
|
|
|
|
.platform-reddit {
|
|
background: #ff4500;
|
|
}
|
|
|
|
.platform-hackernews {
|
|
background: #ff6600;
|
|
}
|
|
|
|
.platform-unknown {
|
|
background: #64748b;
|
|
}
|
|
|
|
.post-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: #64748b;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.post-author {
|
|
font-weight: 500;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.post-source {
|
|
color: #4db6ac;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.post-separator {
|
|
color: #cbd5e1;
|
|
}
|
|
|
|
.post-time {
|
|
color: #64748b;
|
|
}
|
|
|
|
.external-link-indicator {
|
|
color: #4db6ac;
|
|
}
|
|
|
|
.post-title {
|
|
font-size: 1.8rem;
|
|
font-weight: 700;
|
|
color: #2c3e50;
|
|
margin: 0 0 24px 0;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.post-title-link {
|
|
color: #2c3e50;
|
|
text-decoration: none;
|
|
transition: color 0.2s ease;
|
|
}
|
|
|
|
.post-title-link:hover {
|
|
color: #4db6ac;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.post-content {
|
|
color: #374151;
|
|
font-size: 16px;
|
|
line-height: 1.6;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.external-link {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.external-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
background: #4db6ac;
|
|
color: white;
|
|
padding: 12px 20px;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.external-btn:hover {
|
|
background: #26a69a;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.post-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #f1f5f9;
|
|
}
|
|
|
|
.post-stats {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
font-size: 14px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.post-score, .post-comments {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.post-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
|
|
.post-action {
|
|
padding: 8px 16px;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 6px;
|
|
background: none;
|
|
color: #64748b;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.post-action:hover {
|
|
border-color: #4db6ac;
|
|
color: #4db6ac;
|
|
}
|
|
|
|
/* Comments Section */
|
|
.comments-section {
|
|
background: white;
|
|
border-radius: 16px;
|
|
padding: 32px;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.comments-section h2 {
|
|
margin: 0 0 24px 0;
|
|
font-size: 1.3rem;
|
|
font-weight: 600;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.comment {
|
|
padding: 20px 0;
|
|
border-bottom: 1px solid #f1f5f9;
|
|
position: relative;
|
|
}
|
|
|
|
.comment:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
/* Threaded comment styling */
|
|
.comment[style*="margin-left"] {
|
|
padding-left: 16px;
|
|
border-left: 2px solid #e2e8f0;
|
|
border-bottom: none;
|
|
}
|
|
|
|
.comment-replies {
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.comment-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.comment-author {
|
|
font-weight: 500;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.comment-separator {
|
|
color: #cbd5e1;
|
|
}
|
|
|
|
.comment-time {
|
|
color: #64748b;
|
|
}
|
|
|
|
.comment-content {
|
|
color: #374151;
|
|
line-height: 1.5;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.comment-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.comment-score {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.no-comments {
|
|
text-align: center;
|
|
color: #64748b;
|
|
font-style: italic;
|
|
padding: 40px 0;
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 768px) {
|
|
.nav-center {
|
|
display: none;
|
|
}
|
|
|
|
.main-content.single-post {
|
|
padding: 16px;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.post-detail, .comments-section {
|
|
padding: 20px;
|
|
}
|
|
|
|
.search-bar {
|
|
min-width: 250px;
|
|
}
|
|
|
|
.post-title {
|
|
font-size: 1.4rem;
|
|
}
|
|
|
|
.external-btn {
|
|
font-size: 14px;
|
|
padding: 10px 16px;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
function goBackToFeed() {
|
|
// Try to go back to the dashboard if possible
|
|
if (document.referrer && document.referrer.includes(window.location.origin)) {
|
|
window.history.back();
|
|
} else {
|
|
// Fallback to dashboard
|
|
window.location.href = '/';
|
|
}
|
|
}
|
|
|
|
function sharePost() {
|
|
const url = window.location.href;
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
alert('Link copied to clipboard!');
|
|
});
|
|
}
|
|
|
|
function savePost() {
|
|
// TODO: Implement save post functionality
|
|
// User can save posts to their profile for later viewing
|
|
// This needs database backend integration with user_saved_posts table
|
|
// Same implementation needed as dashboard.html savePost function
|
|
alert('Save functionality coming soon!');
|
|
}
|
|
|
|
// Moment.js replacement for timestamp formatting
|
|
function formatTimeAgo(timestamp) {
|
|
const now = Date.now() / 1000;
|
|
const diff = now - timestamp;
|
|
|
|
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
|
|
return new Date(timestamp * 1000).toLocaleDateString();
|
|
}
|
|
|
|
// Update timestamps on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.querySelectorAll('.post-time, .comment-time').forEach(el => {
|
|
const timestamp = parseInt(el.dataset.timestamp);
|
|
if (timestamp) {
|
|
el.textContent = formatTimeAgo(timestamp);
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %} |