Files
balanceboard/templates/post_detail.html
chelsea b47155cc36 Fix Stack Overflow crawling platform name mismatch
The issue was that Stack Overflow was configured with platform name
'stackoverflow' but the data collection code expected 'stackexchange'.
Fixed by:

1. Renamed platform from 'stackoverflow' to 'stackexchange' in platform_config.json
2. Added Stack Overflow collection target to enable crawling
3. Updated templates and app.py to use the correct platform name
4. Added default 'stackoverflow' community alongside existing featured/newest

This resolves the platform name mismatch that prevented Stack Overflow
from being crawlable.

Fixes #23

~claude

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 03:07:41 -05:00

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 == 'stackexchange' %}
📚 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 %}