Files
balanceboard/templates/dashboard.html
chelsea 07df6d8f0a Fix 500 error: Change register route to signup
Fixed BuildError caused by incorrect endpoint name in anonymous mode.
The route is called 'signup' not 'register' in app.py line 878.

Error was:
werkzeug.routing.exceptions.BuildError: Could not build url for endpoint 'register'

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-11 22:26:35 -05:00

1328 lines
37 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">
{% if anonymous %}
<div class="anonymous-actions">
<a href="{{ url_for('login') }}" class="login-btn">🔑 Login</a>
<a href="{{ url_for('signup') }}" class="register-btn">📝 Sign Up</a>
</div>
{% else %}
{# This block only executes for authenticated users (per app.py line 278) #}
<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>
{% endif %}
</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>{% if anonymous %}Public Feed{% else %}Your Feed{% endif %}</h1>
<div class="content-actions">
<button class="refresh-btn" onclick="refreshFeed()">🔄 Refresh</button>
{% if not anonymous %}
<a href="{{ url_for('settings_filters') }}" class="filter-btn">🔧 Customize</a>
{% endif %}
</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);
}
.clear-search-btn {
background: #f1f5f9;
color: #64748b;
border: 1px solid #e2e8f0;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.clear-search-btn:hover {
background: #e2e8f0;
color: #2c3e50;
transform: translateY(-1px);
}
.feed-container {
padding: 0;
}
.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);
if (currentSearchQuery) params.append('q', currentSearchQuery);
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
let currentSearchQuery = '';
document.querySelector('.search-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const query = this.value.trim();
performSearch(query);
}
});
document.querySelector('.search-btn').addEventListener('click', function() {
const query = document.querySelector('.search-input').value.trim();
performSearch(query);
});
function performSearch(query) {
currentSearchQuery = query;
currentPage = 1;
if (query) {
document.querySelector('.content-header h1').textContent = `Search results for "${query}"`;
// Show clear search button
if (!document.querySelector('.clear-search-btn')) {
const clearBtn = document.createElement('button');
clearBtn.className = 'clear-search-btn';
clearBtn.textContent = '✕ Clear search';
clearBtn.onclick = clearSearch;
document.querySelector('.content-actions').prepend(clearBtn);
}
}
loadPosts();
}
function clearSearch() {
currentSearchQuery = '';
document.querySelector('.search-input').value = '';
// Restore original feed title based on user state
const isAnonymous = {{ 'true' if anonymous else 'false' }};
document.querySelector('.content-header h1').textContent = isAnonymous ? 'Public Feed' : 'Your Feed';
const clearBtn = document.querySelector('.clear-search-btn');
if (clearBtn) {
clearBtn.remove();
}
loadPosts();
}
// 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 %}