Files
balanceboard/templates/dashboard.html
chelsea 3ab3b04643 Clean up redundant authentication checks in dashboard template
Removed redundant current_user.is_authenticated checks in the else block
of the navigation menu. The else block only executes for authenticated
users per app.py logic (line 278 vs 293), so the nested checks were
dead code that created confusion.

Changes:
- Removed defensive checks for unauthenticated users in authenticated block
- Added clarifying comment about when else block executes
- Simplified template logic for better maintainability
- Removed dead code paths (Anonymous User label, ? avatar)

Addresses concerns raised in Issue #2 about confusing template logic.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-11 22:08:42 -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('register') }}" 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 %}