- Flask-based web application with PostgreSQL - User authentication and session management - Content moderation and filtering - Docker deployment with docker-compose - Admin interface for content management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1264 lines
35 KiB
HTML
1264 lines
35 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Dashboard - 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">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main Content Area -->
|
|
<main class="main-content">
|
|
<!-- Sidebar -->
|
|
<aside class="sidebar">
|
|
<div class="sidebar-section">
|
|
<h3>Content Filters</h3>
|
|
<div class="filter-item active" data-filter="no_filter">
|
|
<span class="filter-icon">🌐</span>
|
|
<span>All Content</span>
|
|
</div>
|
|
<div class="filter-item" data-filter="safe_content">
|
|
<span class="filter-icon">✅</span>
|
|
<span>Safe Content</span>
|
|
</div>
|
|
<div class="filter-item" data-filter="custom">
|
|
<span class="filter-icon">🎯</span>
|
|
<span>Custom Filter</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<h3>Communities</h3>
|
|
<div id="community-list" class="community-list">
|
|
<!-- Communities will be loaded dynamically -->
|
|
<div class="loading-communities">Loading communities...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<h3>Quick Stats</h3>
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-number">156</div>
|
|
<div class="stat-label">Posts Today</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number">{{ user_settings.get('communities', [])|length or 'All' }}</div>
|
|
<div class="stat-label">Communities</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Content Feed -->
|
|
<section class="content-section">
|
|
<div class="content-header">
|
|
<h1>Your Feed</h1>
|
|
<div class="content-actions">
|
|
<button class="refresh-btn" onclick="refreshFeed()">🔄 Refresh</button>
|
|
<a href="{{ url_for('settings_filters') }}" class="filter-btn">🔧 Customize</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="feed-container">
|
|
<div id="loading-indicator" class="loading">
|
|
<div class="loading-spinner"></div>
|
|
<p>Loading your feed...</p>
|
|
</div>
|
|
<div id="posts-container" class="posts-grid">
|
|
<!-- Posts will be loaded here dynamically -->
|
|
</div>
|
|
|
|
<!-- Pagination Controls -->
|
|
<div class="pagination-controls">
|
|
<button id="prev-btn" class="pagination-btn" onclick="loadPreviousPage()" disabled>
|
|
← Previous
|
|
</button>
|
|
<span id="page-info" class="page-info">Page 1 of 1</span>
|
|
<button id="next-btn" class="pagination-btn" onclick="loadNextPage()" disabled>
|
|
Next →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<style>
|
|
/* 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;
|
|
}
|
|
|
|
/* Main Content Layout */
|
|
.main-content {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
display: grid;
|
|
grid-template-columns: 280px 1fr;
|
|
gap: 24px;
|
|
padding: 24px;
|
|
min-height: calc(100vh - 64px);
|
|
}
|
|
|
|
/* Sidebar */
|
|
.sidebar {
|
|
background: white;
|
|
border-radius: 16px;
|
|
padding: 24px;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
|
height: fit-content;
|
|
position: sticky;
|
|
top: 88px;
|
|
}
|
|
|
|
.sidebar-section {
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.sidebar-section h3 {
|
|
margin: 0 0 16px 0;
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.filter-item, .community-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px 16px;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.filter-item:hover, .community-item:hover {
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.filter-item.active {
|
|
background: #e0f7fa;
|
|
color: #4db6ac;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.community-item.active {
|
|
background: #e0f7fa;
|
|
color: #4db6ac;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.filter-icon, .community-icon {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.count {
|
|
margin-left: auto;
|
|
background: #e2e8f0;
|
|
color: #64748b;
|
|
font-size: 12px;
|
|
padding: 2px 8px;
|
|
border-radius: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.loading-communities {
|
|
text-align: center;
|
|
color: #64748b;
|
|
font-style: italic;
|
|
padding: 20px;
|
|
}
|
|
|
|
.no-communities {
|
|
text-align: center;
|
|
color: #64748b;
|
|
font-style: italic;
|
|
padding: 20px;
|
|
}
|
|
|
|
.community-name {
|
|
flex: 1;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: #f8fafc;
|
|
padding: 16px;
|
|
border-radius: 12px;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: #4db6ac;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Content Section */
|
|
.content-section {
|
|
background: white;
|
|
border-radius: 16px;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.content-header {
|
|
padding: 24px 32px;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
background: #fafbfc;
|
|
}
|
|
|
|
.content-header h1 {
|
|
margin: 0;
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.content-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
|
|
.refresh-btn, .filter-btn {
|
|
background: #4db6ac;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.refresh-btn:hover, .filter-btn:hover {
|
|
background: #26a69a;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.feed-container {
|
|
padding: 0;
|
|
max-height: calc(100vh - 200px);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.loading {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 60px 20px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 3px solid #e2e8f0;
|
|
border-top: 3px solid #4db6ac;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
.posts-grid {
|
|
padding: 24px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.post-card {
|
|
background: white;
|
|
border-radius: 12px;
|
|
border: 1px solid #e5e7eb;
|
|
padding: 20px;
|
|
transition: all 0.2s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.post-card:hover {
|
|
border-color: #4db6ac;
|
|
box-shadow: 0 4px 12px rgba(77, 182, 172, 0.1);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.post-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.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;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.platform-badge:hover {
|
|
transform: scale(1.1);
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.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;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
border-radius: 4px;
|
|
padding: 2px 4px;
|
|
}
|
|
|
|
.post-source:hover {
|
|
background: rgba(77, 182, 172, 0.1);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.post-separator {
|
|
color: #cbd5e1;
|
|
}
|
|
|
|
.post-time {
|
|
color: #64748b;
|
|
}
|
|
|
|
.external-link-indicator {
|
|
margin-left: 8px;
|
|
color: #4db6ac;
|
|
}
|
|
|
|
.post-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
color: #2c3e50;
|
|
margin: 12px 0 8px 0;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.post-preview {
|
|
color: #64748b;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.post-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-top: 12px;
|
|
padding-top: 12px;
|
|
border-top: 1px solid #f1f5f9;
|
|
}
|
|
|
|
.post-stats {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
font-size: 13px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.post-score {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.post-comments {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.post-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.post-action {
|
|
padding: 6px 12px;
|
|
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;
|
|
}
|
|
|
|
/* Pagination Controls */
|
|
.pagination-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 20px;
|
|
padding: 24px 32px;
|
|
border-top: 1px solid #e5e7eb;
|
|
background: #fafbfc;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.pagination-btn {
|
|
background: #4db6ac;
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.pagination-btn:hover:not(:disabled) {
|
|
background: #26a69a;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.pagination-btn:disabled {
|
|
background: #e2e8f0;
|
|
color: #64748b;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.page-info {
|
|
font-size: 14px;
|
|
color: #2c3e50;
|
|
font-weight: 500;
|
|
min-width: 120px;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 1024px) {
|
|
.main-content {
|
|
grid-template-columns: 1fr;
|
|
gap: 16px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.sidebar {
|
|
position: static;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.nav-content {
|
|
padding: 0 16px;
|
|
}
|
|
|
|
.search-bar {
|
|
min-width: 250px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.nav-center {
|
|
display: none;
|
|
}
|
|
|
|
.nav-content {
|
|
justify-content: space-between;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// Global state
|
|
let postsData = [];
|
|
let currentPage = 1;
|
|
let currentCommunity = '';
|
|
let currentPlatform = '';
|
|
let paginationData = {};
|
|
let platformConfig = {};
|
|
let communitiesData = [];
|
|
|
|
// User experience settings
|
|
let userSettings = {{ user_settings|tojson }};
|
|
|
|
// Load posts on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadPlatformConfig();
|
|
loadPosts();
|
|
setupFilterSwitching();
|
|
setupInfiniteScroll();
|
|
setupAutoRefresh();
|
|
});
|
|
|
|
// Load platform configuration and communities
|
|
async function loadPlatformConfig() {
|
|
try {
|
|
const response = await fetch('/api/platforms');
|
|
const data = await response.json();
|
|
platformConfig = data.platforms || {};
|
|
communitiesData = data.communities || [];
|
|
|
|
renderCommunities(communitiesData);
|
|
setupCommunityFiltering();
|
|
} catch (error) {
|
|
console.error('Error loading platform configuration:', error);
|
|
// Show fallback communities
|
|
const fallbackCommunities = [
|
|
{platform: 'reddit', id: 'programming', display_name: 'r/programming', icon: '💻', count: 0},
|
|
{platform: 'reddit', id: 'python', display_name: 'r/python', icon: '🐍', count: 0},
|
|
{platform: 'hackernews', id: 'hackernews', display_name: 'Hacker News', icon: '🧮', count: 0}
|
|
];
|
|
renderCommunities(fallbackCommunities);
|
|
setupCommunityFiltering();
|
|
}
|
|
}
|
|
|
|
// Render communities in sidebar
|
|
function renderCommunities(communities) {
|
|
const communityList = document.getElementById('community-list');
|
|
if (!communityList) return;
|
|
|
|
if (communities.length === 0) {
|
|
communityList.innerHTML = '<div class="no-communities">No communities available</div>';
|
|
return;
|
|
}
|
|
|
|
// Add "All Communities" option at the top
|
|
let communitiesHTML = `
|
|
<div class="community-item all-communities active" data-platform="" data-community="">
|
|
<span class="community-icon">🌐</span>
|
|
<span class="community-name">All Communities</span>
|
|
<span class="count">${communities.reduce((sum, c) => sum + c.count, 0)}</span>
|
|
</div>
|
|
`;
|
|
|
|
communitiesHTML += communities.map(community => {
|
|
return `
|
|
<div class="community-item" data-platform="${community.platform}" data-community="${community.id}">
|
|
<span class="community-icon">${community.icon}</span>
|
|
<span class="community-name">${community.display_name}</span>
|
|
<span class="count">${community.count}</span>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
communityList.innerHTML = communitiesHTML;
|
|
}
|
|
|
|
// Load posts from API
|
|
async function loadPosts(page = 1, community = '', platform = '', append = false) {
|
|
try {
|
|
// Build query parameters
|
|
const params = new URLSearchParams();
|
|
params.append('page', page);
|
|
params.append('per_page', 20);
|
|
if (community) params.append('community', community);
|
|
if (platform) params.append('platform', platform);
|
|
|
|
const response = await fetch(`/api/posts?${params}`);
|
|
const data = await response.json();
|
|
const newPosts = data.posts || [];
|
|
paginationData = data.pagination || {};
|
|
|
|
// Update current state
|
|
currentPage = page;
|
|
currentCommunity = community;
|
|
currentPlatform = platform;
|
|
|
|
if (append && userSettings?.experience?.infinite_scroll) {
|
|
// Append new posts for infinite scroll
|
|
postsData = [...postsData, ...newPosts];
|
|
renderPosts(newPosts, true);
|
|
} else {
|
|
// Replace posts for pagination
|
|
postsData = newPosts;
|
|
renderPosts(postsData, false);
|
|
}
|
|
|
|
updatePaginationControls();
|
|
} catch (error) {
|
|
console.error('Error loading posts:', error);
|
|
showError('Failed to load posts');
|
|
}
|
|
}
|
|
|
|
// Render posts to the feed
|
|
function renderPosts(posts, append = false) {
|
|
const loadingIndicator = document.getElementById('loading-indicator');
|
|
const postsContainer = document.getElementById('posts-container');
|
|
|
|
// Hide loading indicator
|
|
loadingIndicator.style.display = 'none';
|
|
|
|
if (!append && posts.length === 0) {
|
|
postsContainer.innerHTML = '<div class="no-posts">No posts available</div>';
|
|
return;
|
|
}
|
|
|
|
if (append && posts.length === 0) {
|
|
// No more posts to append
|
|
return;
|
|
}
|
|
|
|
const postsHTML = posts.map(post => createPostCard(post)).join('');
|
|
|
|
if (append) {
|
|
// Append new posts for infinite scroll
|
|
postsContainer.insertAdjacentHTML('beforeend', postsHTML);
|
|
} else {
|
|
// Replace all posts for pagination
|
|
postsContainer.innerHTML = postsHTML;
|
|
}
|
|
}
|
|
|
|
// Create individual post card HTML
|
|
function createPostCard(post) {
|
|
const timeAgo = formatTimeAgo(post.timestamp);
|
|
const platformClass = `platform-${post.platform}`;
|
|
const platformInitial = post.platform.charAt(0).toUpperCase();
|
|
const hasExternalLink = post.external_url && !post.external_url.includes(window.location.hostname);
|
|
|
|
return `
|
|
<article class="post-card" onclick="openPost('${post.id}')">
|
|
<div class="post-header">
|
|
<div class="platform-badge ${platformClass}" onclick="event.stopPropagation(); filterByPlatform('${post.platform}')" title="Filter by ${post.platform}">
|
|
${platformInitial}
|
|
</div>
|
|
<div class="post-meta">
|
|
<span class="post-author">${escapeHtml(post.author)}</span>
|
|
<span class="post-separator">•</span>
|
|
${post.source_display ? `<span class="post-source" onclick="event.stopPropagation(); filterByCommunity('${post.source}', '${post.platform}')" title="Filter by ${post.source_display}">${escapeHtml(post.source_display)}</span><span class="post-separator">•</span>` : ''}
|
|
<span class="post-time">${timeAgo}</span>
|
|
${hasExternalLink ? '<span class="external-link-indicator">🔗</span>' : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<h3 class="post-title">${escapeHtml(post.title)}</h3>
|
|
|
|
${post.content_preview ? `<div class="post-preview">${escapeHtml(post.content_preview)}</div>` : ''}
|
|
|
|
${post.tags && post.tags.length > 0 ? `
|
|
<div class="post-tags">
|
|
${post.tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join('')}
|
|
</div>
|
|
` : ''}
|
|
|
|
<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>${post.comments_count} comments</span>
|
|
</div>
|
|
</div>
|
|
<div class="post-actions">
|
|
${hasExternalLink ? `<button class="post-action" onclick="event.stopPropagation(); window.open('${escapeHtml(post.external_url)}', '_blank')">🔗 Source</button>` : ''}
|
|
<button class="post-action" onclick="event.stopPropagation(); sharePost('${post.id}')">Share</button>
|
|
<button class="post-action" onclick="event.stopPropagation(); savePost('${post.id}')">Save</button>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
`;
|
|
}
|
|
|
|
// Utility functions
|
|
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`;
|
|
return `${Math.floor(diff / 86400)}d ago`;
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function showError(message) {
|
|
const postsContainer = document.getElementById('posts-container');
|
|
postsContainer.innerHTML = `<div class="error-message">${message}</div>`;
|
|
}
|
|
|
|
// Platform and community filtering functions
|
|
function filterByPlatform(platform) {
|
|
// Remove active from all community items
|
|
document.querySelectorAll('.community-item').forEach(c => c.classList.remove('active'));
|
|
|
|
// Update header
|
|
const contentHeader = document.querySelector('.content-header h1');
|
|
contentHeader.textContent = `${platform.charAt(0).toUpperCase() + platform.slice(1)} Posts`;
|
|
|
|
// Show loading state
|
|
const postsContainer = document.getElementById('posts-container');
|
|
const loadingIndicator = document.getElementById('loading-indicator');
|
|
loadingIndicator.style.display = 'flex';
|
|
postsContainer.innerHTML = '';
|
|
|
|
// Load filtered posts by platform
|
|
loadPosts(1, '', platform);
|
|
}
|
|
|
|
function filterByCommunity(community, platform) {
|
|
// Remove active from all community items
|
|
document.querySelectorAll('.community-item').forEach(c => c.classList.remove('active'));
|
|
|
|
// Set active state on matching community if it exists
|
|
const matchingCommunity = document.querySelector(`.community-item[data-community="${community}"][data-platform="${platform}"]`);
|
|
if (matchingCommunity) {
|
|
matchingCommunity.classList.add('active');
|
|
}
|
|
|
|
// Update header
|
|
const contentHeader = document.querySelector('.content-header h1');
|
|
contentHeader.textContent = community ? `${community} Posts` : 'Your Feed';
|
|
|
|
// Show loading state
|
|
const postsContainer = document.getElementById('posts-container');
|
|
const loadingIndicator = document.getElementById('loading-indicator');
|
|
loadingIndicator.style.display = 'flex';
|
|
postsContainer.innerHTML = '';
|
|
|
|
// Load filtered posts
|
|
loadPosts(1, community, platform);
|
|
}
|
|
|
|
// Post actions
|
|
function openPost(postId) {
|
|
window.open(`/post/${postId}`, '_blank');
|
|
}
|
|
|
|
function sharePost(postId) {
|
|
const url = `${window.location.origin}/post/${postId}`;
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
alert('Link copied to clipboard!');
|
|
});
|
|
}
|
|
|
|
function savePost(postId) {
|
|
alert('Save functionality coming soon!');
|
|
}
|
|
|
|
// Filter switching functionality
|
|
function setupFilterSwitching() {
|
|
const filterItems = document.querySelectorAll('.filter-item');
|
|
|
|
filterItems.forEach(item => {
|
|
item.addEventListener('click', function() {
|
|
// Remove active class from all items
|
|
filterItems.forEach(f => f.classList.remove('active'));
|
|
|
|
// Add active class to clicked item
|
|
this.classList.add('active');
|
|
|
|
// Get filter type
|
|
const filterType = this.dataset.filter;
|
|
|
|
// Apply filter (for now just reload)
|
|
if (filterType && filterType !== 'custom') {
|
|
loadPosts(); // In future, pass filter parameter
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Refresh feed function
|
|
function refreshFeed() {
|
|
const refreshBtn = document.querySelector('.refresh-btn');
|
|
const loadingIndicator = document.getElementById('loading-indicator');
|
|
|
|
// Add loading state
|
|
refreshBtn.innerHTML = '⏳ Refreshing...';
|
|
refreshBtn.disabled = true;
|
|
|
|
// Show loading indicator
|
|
loadingIndicator.style.display = 'flex';
|
|
|
|
// Reload posts with current filters
|
|
loadPosts(currentPage, currentCommunity, currentPlatform).finally(() => {
|
|
// Reset button
|
|
refreshBtn.innerHTML = '🔄 Refresh';
|
|
refreshBtn.disabled = false;
|
|
});
|
|
}
|
|
|
|
// Search functionality
|
|
document.querySelector('.search-input').addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
const query = this.value.trim();
|
|
if (query) {
|
|
alert(`Search functionality coming soon! You searched for: "${query}"`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Setup infinite scroll functionality
|
|
function setupInfiniteScroll() {
|
|
if (!userSettings?.experience?.infinite_scroll) {
|
|
return;
|
|
}
|
|
|
|
const feedContainer = document.querySelector('.feed-container');
|
|
let loadingMore = false;
|
|
|
|
// Hide pagination controls for infinite scroll
|
|
const paginationControls = document.querySelector('.pagination-controls');
|
|
if (paginationControls) {
|
|
paginationControls.style.display = 'none';
|
|
}
|
|
|
|
// Add scroll event listener
|
|
feedContainer.addEventListener('scroll', async function() {
|
|
if (loadingMore) return;
|
|
|
|
const scrollTop = feedContainer.scrollTop;
|
|
const scrollHeight = feedContainer.scrollHeight;
|
|
const clientHeight = feedContainer.clientHeight;
|
|
|
|
// Load more when user scrolls near bottom (80% through content)
|
|
const scrollThreshold = 0.8;
|
|
|
|
if (scrollTop + clientHeight >= scrollHeight * scrollThreshold) {
|
|
if (paginationData.has_next) {
|
|
loadingMore = true;
|
|
|
|
// Show loading indicator at bottom
|
|
const loadingDiv = document.createElement('div');
|
|
loadingDiv.className = 'loading-more';
|
|
loadingDiv.innerHTML = `
|
|
<div class="loading-spinner" style="width: 20px; height: 20px; border-width: 2px;"></div>
|
|
<p style="font-size: 0.9rem; margin: 8px 0 0 0;">Loading more posts...</p>
|
|
`;
|
|
document.getElementById('posts-container').appendChild(loadingDiv);
|
|
|
|
try {
|
|
await loadPosts(currentPage + 1, currentCommunity, currentPlatform, true);
|
|
} finally {
|
|
loadingDiv.remove();
|
|
loadingMore = false;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Auto-refresh functionality
|
|
let autoRefreshInterval = null;
|
|
|
|
function setupAutoRefresh() {
|
|
// Clear any existing interval
|
|
if (autoRefreshInterval) {
|
|
clearInterval(autoRefreshInterval);
|
|
autoRefreshInterval = null;
|
|
}
|
|
|
|
// Only set up auto-refresh if enabled in user settings
|
|
if (!userSettings?.experience?.auto_refresh) {
|
|
return;
|
|
}
|
|
|
|
// Refresh once per day (86400000 ms = 24 hours)
|
|
// TODO: Make this configurable in admin settings - allow admins to set auto-refresh interval
|
|
const refreshInterval = 86400000;
|
|
|
|
autoRefreshInterval = setInterval(async () => {
|
|
// Only auto-refresh if user is viewing first page and no specific filters
|
|
if (currentPage === 1 && !currentCommunity && !currentPlatform) {
|
|
try {
|
|
// Check if new content is available by checking timestamp
|
|
const response = await fetch('/api/content-timestamp');
|
|
const data = await response.json();
|
|
const lastContentUpdate = data.timestamp;
|
|
|
|
// Compare with our last known update (stored in localStorage)
|
|
const lastKnownUpdate = localStorage.getItem('lastContentUpdate');
|
|
|
|
if (!lastKnownUpdate || lastContentUpdate > lastKnownUpdate) {
|
|
// Show subtle refresh indicator
|
|
const refreshBtn = document.querySelector('.refresh-btn');
|
|
const originalText = refreshBtn.innerHTML;
|
|
refreshBtn.innerHTML = '⏳ New content available...';
|
|
refreshBtn.style.opacity = '0.7';
|
|
|
|
// Only reload if there's actually new content
|
|
await loadPosts(1, currentCommunity, currentPlatform);
|
|
|
|
// Update our timestamp
|
|
localStorage.setItem('lastContentUpdate', lastContentUpdate);
|
|
|
|
// Show brief success feedback
|
|
refreshBtn.innerHTML = '✅ Updated';
|
|
setTimeout(() => {
|
|
refreshBtn.innerHTML = originalText;
|
|
refreshBtn.style.opacity = '1';
|
|
}, 2000);
|
|
} else {
|
|
// No new content, just show a subtle indicator
|
|
console.log('Auto-refresh: No new content available');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Auto-refresh failed:', error);
|
|
// Don't show error for auto-refresh failures, just log them
|
|
}
|
|
}
|
|
}, refreshInterval);
|
|
|
|
// Show auto-refresh status in console for debugging
|
|
console.log('Auto-refresh enabled: once per day (24 hours)');
|
|
}
|
|
|
|
// Function to toggle auto-refresh (for settings page)
|
|
function toggleAutoRefresh(enabled) {
|
|
userSettings.experience = userSettings.experience || {};
|
|
userSettings.experience.auto_refresh = enabled;
|
|
|
|
if (enabled) {
|
|
setupAutoRefresh();
|
|
} else if (autoRefreshInterval) {
|
|
clearInterval(autoRefreshInterval);
|
|
autoRefreshInterval = null;
|
|
console.log('Auto-refresh disabled');
|
|
}
|
|
}
|
|
|
|
// Pagination functions
|
|
function updatePaginationControls() {
|
|
const prevBtn = document.getElementById('prev-btn');
|
|
const nextBtn = document.getElementById('next-btn');
|
|
const pageInfo = document.getElementById('page-info');
|
|
|
|
// Hide pagination controls if infinite scroll is enabled
|
|
const paginationControls = document.querySelector('.pagination-controls');
|
|
if (userSettings?.experience?.infinite_scroll) {
|
|
if (paginationControls) paginationControls.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
if (paginationData) {
|
|
prevBtn.disabled = !paginationData.has_prev;
|
|
nextBtn.disabled = !paginationData.has_next;
|
|
pageInfo.textContent = `Page ${paginationData.current_page} of ${paginationData.total_pages}`;
|
|
}
|
|
}
|
|
|
|
function loadNextPage() {
|
|
if (paginationData.has_next) {
|
|
loadPosts(currentPage + 1, currentCommunity, currentPlatform);
|
|
}
|
|
}
|
|
|
|
function loadPreviousPage() {
|
|
if (paginationData.has_prev) {
|
|
loadPosts(currentPage - 1, currentCommunity, currentPlatform);
|
|
}
|
|
}
|
|
|
|
// Community filtering setup
|
|
function setupCommunityFiltering() {
|
|
document.querySelectorAll('.community-item').forEach(item => {
|
|
item.addEventListener('click', function() {
|
|
// Remove active class from all community items
|
|
document.querySelectorAll('.community-item').forEach(c => c.classList.remove('active'));
|
|
|
|
// Add active class to clicked item
|
|
this.classList.add('active');
|
|
|
|
// Get platform and community from data attributes
|
|
const platform = this.dataset.platform || '';
|
|
const community = this.dataset.community || '';
|
|
|
|
// Update the header to show what's being filtered
|
|
const contentHeader = document.querySelector('.content-header h1');
|
|
if (community && platform) {
|
|
contentHeader.textContent = `${this.querySelector('.community-name').textContent}`;
|
|
} else {
|
|
contentHeader.textContent = 'Your Feed';
|
|
}
|
|
|
|
// Show loading state
|
|
const postsContainer = document.getElementById('posts-container');
|
|
const loadingIndicator = document.getElementById('loading-indicator');
|
|
loadingIndicator.style.display = 'flex';
|
|
postsContainer.innerHTML = '';
|
|
|
|
// Load filtered posts
|
|
loadPosts(1, community, platform);
|
|
});
|
|
});
|
|
|
|
// Add "All Content" filter option
|
|
const allContentFilter = document.querySelector('.filter-item[data-filter="no_filter"]');
|
|
if (allContentFilter) {
|
|
allContentFilter.addEventListener('click', function() {
|
|
// Remove active from community items
|
|
document.querySelectorAll('.community-item').forEach(c => c.classList.remove('active'));
|
|
|
|
// Set "All Communities" as active
|
|
const allCommunitiesItem = document.querySelector('.community-item.all-communities');
|
|
if (allCommunitiesItem) {
|
|
allCommunitiesItem.classList.add('active');
|
|
}
|
|
|
|
// Reset header
|
|
document.querySelector('.content-header h1').textContent = 'Your Feed';
|
|
|
|
// Show loading and load all posts
|
|
const postsContainer = document.getElementById('posts-container');
|
|
const loadingIndicator = document.getElementById('loading-indicator');
|
|
loadingIndicator.style.display = 'flex';
|
|
postsContainer.innerHTML = '';
|
|
|
|
loadPosts(1, '', '');
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %} |