Compare commits

...

3 Commits

Author SHA1 Message Date
chelsea
fc440eafa3 Fix post detail page navigation issues
- Change back to feed button from onclick to proper link to ensure it always works
- Make platform badge clickable to external source when available (upper left clickable link)
- This addresses the issue where posts had no upper left clickable link and back button didn't work
2025-10-12 13:28:41 -05:00
chelsea
343d6b51ea Temporarily disable community filter to fix urgent issue: logged in users not seeing feed
This disables the community-based filtering in /api/posts to allow logged in users to see posts in their feed. The community selection may need further debugging as it appears users have selected communities that match no posts.
2025-10-12 13:27:41 -05:00
chelsea
02d4e5a7e4 Fix community settings and admin panel layout issues
## Problems Fixed:
1. **Community settings grid layout** - Cards were too spread out horizontally
2. **Missing navigation elements** - No back link or breadcrumbs on community settings
3. **Admin panel grid layouts** - Stats and system info cards too wide on large screens
4. **Responsive layout issues** - Poor layout on different screen sizes

## Root Cause:
The CSS grid layouts were using `auto-fill` and `auto-fit` with minimum sizes that
caused elements to spread too wide on large screens, creating an awkward UI.

## Solutions Implemented:

### Community Settings Page:
- **Improved grid layout** - Better responsive grid with max 3 columns
- **Added back navigation** - "← Back to Settings" link for better UX
- **Responsive breakpoints** - Single column on mobile, max 3 on desktop
- **Better max-width** - Increased container width for better balance

### Admin Panel Base Template:
- **Fixed admin stats grid** - Limited to 4 columns max, responsive breakpoints
- **Fixed system info grid** - Limited to 3 columns max with proper responsive design
- **Added mobile support** - Single column layout on mobile devices
- **Improved spacing** - Better gap management and max-width constraints

### CSS Improvements:
- Mobile-first responsive design
- Proper breakpoints at 768px and 1200px
- Maximum column limits to prevent over-stretching
- Better visual balance on all screen sizes

The admin and community settings interfaces now have proper, responsive layouts\!

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 04:08:09 -05:00
4 changed files with 208 additions and 124 deletions

92
app.py
View File

@@ -449,23 +449,24 @@ def api_posts():
continue continue
# Apply user's community preferences (before filterset) # Apply user's community preferences (before filterset)
if user_communities: # Temporarily disabled to fix urgent feed issue for logged in users
post_source = post_data.get('source', '').lower() # if user_communities:
post_platform = post_data.get('platform', '').lower() # post_source = post_data.get('source', '').lower()
# post_platform = post_data.get('platform', '').lower()
# Check if this post matches any of the user's selected communities #
matches_community = False # # Check if this post matches any of the user's selected communities
for selected_community in user_communities: # matches_community = False
selected_community = selected_community.lower() # for selected_community in user_communities:
# Match by exact source name or platform name # selected_community = selected_community.lower()
if (post_source == selected_community or # # Match by exact source name or platform name
post_platform == selected_community or # if (post_source == selected_community or
selected_community in post_source): # post_platform == selected_community or
matches_community = True # selected_community in post_source):
break # matches_community = True
# break
if not matches_community: #
continue # if not matches_community:
# continue
# Apply search filter (before filterset) # Apply search filter (before filterset)
if search_query: if search_query:
@@ -1527,33 +1528,59 @@ def settings_experience():
@login_required @login_required
def upload_avatar(): def upload_avatar():
"""Upload profile picture""" """Upload profile picture"""
try:
# Debug logging
logger.info(f"Avatar upload attempt by user {current_user.id} ({current_user.username})")
logger.debug(f"Request files: {list(request.files.keys())}")
logger.debug(f"Request form: {dict(request.form)}")
# Check if user is properly authenticated and has required attributes
if not hasattr(current_user, 'id') or not current_user.id:
logger.error("User missing ID attribute")
flash('Authentication error. Please log in again.', 'error')
return redirect(url_for('login'))
if not hasattr(current_user, 'username') or not current_user.username:
logger.error("User missing username attribute")
flash('User profile incomplete. Please update your profile.', 'error')
return redirect(url_for('settings_profile'))
# Check for file in request
if 'avatar' not in request.files: if 'avatar' not in request.files:
logger.warning("No avatar file in request")
flash('No file selected', 'error') flash('No file selected', 'error')
return redirect(url_for('settings_profile')) return redirect(url_for('settings_profile'))
file = request.files['avatar'] file = request.files['avatar']
if file.filename == '': if file.filename == '':
logger.warning("Empty filename provided")
flash('No file selected', 'error') flash('No file selected', 'error')
return redirect(url_for('settings_profile')) return redirect(url_for('settings_profile'))
logger.info(f"Processing file: {file.filename}")
# Validate file type and size # Validate file type and size
if not _is_allowed_file(file.filename): if not _is_allowed_file(file.filename):
logger.warning(f"Invalid file type: {file.filename}")
flash('Invalid file type. Please upload PNG, JPG, or GIF', 'error') flash('Invalid file type. Please upload PNG, JPG, or GIF', 'error')
return redirect(url_for('settings_profile')) return redirect(url_for('settings_profile'))
# Check file size (Flask's MAX_CONTENT_LENGTH handles this too, but double-check) # Check file size (Flask's MAX_CONTENT_LENGTH handles this too, but double-check)
if hasattr(file, 'content_length') and file.content_length > app.config['MAX_CONTENT_LENGTH']: if hasattr(file, 'content_length') and file.content_length > app.config.get('MAX_CONTENT_LENGTH', 16*1024*1024):
logger.warning(f"File too large: {file.content_length}")
flash('File too large. Maximum size is 16MB', 'error') flash('File too large. Maximum size is 16MB', 'error')
return redirect(url_for('settings_profile')) return redirect(url_for('settings_profile'))
# Validate and secure filename # Validate and secure filename
filename = secure_filename(file.filename) filename = secure_filename(file.filename)
if not filename or len(filename) > MAX_FILENAME_LENGTH: if not filename or len(filename) > MAX_FILENAME_LENGTH:
logger.warning(f"Invalid filename after sanitization: {filename}")
flash('Invalid filename', 'error') flash('Invalid filename', 'error')
return redirect(url_for('settings_profile')) return redirect(url_for('settings_profile'))
# Add user ID to make filename unique and prevent conflicts # Add user ID to make filename unique and prevent conflicts
unique_filename = f"{current_user.id}_{filename}" unique_filename = f"{current_user.id}_{filename}"
logger.info(f"Generated unique filename: {unique_filename}")
# Ensure upload directory exists and is secure # Ensure upload directory exists and is secure
upload_dir = os.path.abspath(UPLOAD_FOLDER) upload_dir = os.path.abspath(UPLOAD_FOLDER)
@@ -1567,21 +1594,36 @@ def upload_avatar():
flash('Invalid file path', 'error') flash('Invalid file path', 'error')
return redirect(url_for('settings_profile')) return redirect(url_for('settings_profile'))
try: # Save the file
file.save(upload_path) file.save(upload_path)
logger.info(f"File uploaded successfully: {unique_filename} by user {current_user.id}") logger.info(f"File saved successfully: {upload_path}")
except Exception as e:
logger.error(f"Error saving uploaded file: {e}")
flash('Error saving file', 'error')
return redirect(url_for('settings_profile'))
# Update user profile # Update user profile with database error handling
old_avatar_url = current_user.profile_picture_url
current_user.profile_picture_url = f"/static/avatars/{unique_filename}" current_user.profile_picture_url = f"/static/avatars/{unique_filename}"
db.session.commit() db.session.commit()
logger.info(f"User profile updated successfully for {current_user.username}")
# Clean up old avatar file if it exists and was uploaded by user
if old_avatar_url and old_avatar_url.startswith('/static/avatars/') and current_user.id in old_avatar_url:
try:
old_file_path = os.path.join(upload_dir, os.path.basename(old_avatar_url))
if os.path.exists(old_file_path):
os.remove(old_file_path)
logger.info(f"Cleaned up old avatar: {old_file_path}")
except Exception as e:
logger.warning(f"Could not clean up old avatar: {e}")
flash('Profile picture updated successfully', 'success') flash('Profile picture updated successfully', 'success')
return redirect(url_for('settings_profile')) return redirect(url_for('settings_profile'))
except Exception as e:
logger.error(f"Unexpected error in avatar upload: {e}")
db.session.rollback()
flash('An unexpected error occurred. Please try again.', 'error')
return redirect(url_for('settings_profile'))
@app.route('/profile') @app.route('/profile')
@login_required @login_required

View File

@@ -245,9 +245,22 @@
/* ===== STATS & CARDS ===== */ /* ===== STATS & CARDS ===== */
.admin-stats { .admin-stats {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px; gap: 16px;
margin-bottom: 24px; margin-bottom: 24px;
max-width: 100%;
}
@media (max-width: 768px) {
.admin-stats {
grid-template-columns: 1fr;
}
}
@media (min-width: 1200px) {
.admin-stats {
grid-template-columns: repeat(4, 1fr);
}
} }
.stat-card { .stat-card {
@@ -291,6 +304,19 @@
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px; gap: 16px;
max-width: 100%;
}
@media (max-width: 768px) {
.system-info {
grid-template-columns: 1fr;
}
}
@media (min-width: 1200px) {
.system-info {
grid-template-columns: repeat(3, 1fr);
}
} }
/* ===== FORMS ===== */ /* ===== FORMS ===== */

View File

@@ -3,67 +3,27 @@
{% block title %}{{ post.title }} - {{ APP_NAME }}{% endblock %} {% block title %}{{ post.title }} - {{ APP_NAME }}{% endblock %}
{% block content %} {% block content %}
<!-- Modern Top Navigation --> {% include '_nav.html' %}
<nav class="top-nav">
<div class="nav-content">
<div class="nav-left">
<div class="logo-section">
<img src="{{ url_for('serve_logo') }}" alt="{{ APP_NAME }}" class="nav-logo">
<span class="brand-text">{{ APP_NAME }}</span>
</div>
</div>
<div class="nav-center">
<div class="search-bar">
<input type="text" placeholder="Search content..." class="search-input">
<button class="search-btn">🔍</button>
</div>
</div>
<div class="nav-right">
{% if current_user.is_authenticated %}
<div class="user-menu">
<div class="user-info">
<div class="user-avatar">
{% if current_user.profile_picture_url %}
<img src="{{ current_user.profile_picture_url }}" alt="Avatar">
{% else %}
<div class="avatar-placeholder">{{ current_user.username[:2].upper() }}</div>
{% endif %}
</div>
<span class="username">{{ current_user.username }}</span>
</div>
<div class="user-dropdown">
<a href="{{ url_for('settings') }}" class="dropdown-item">⚙️ Settings</a>
{% if current_user.is_admin %}
<a href="{{ url_for('admin_panel') }}" class="dropdown-item">👨‍💼 Admin Panel</a>
{% endif %}
<a href="{{ url_for('logout') }}" class="dropdown-item">🚪 Logout</a>
</div>
</div>
{% else %}
<div class="auth-buttons">
<a href="{{ url_for('login') }}" class="auth-btn">Login</a>
<a href="{{ url_for('signup') }}" class="auth-btn primary">Sign Up</a>
</div>
{% endif %}
</div>
</div>
</nav>
<!-- Main Content Area --> <!-- Main Content Area -->
<main class="main-content single-post"> <main class="main-content single-post">
<!-- Back Button --> <!-- Back Button -->
<div class="back-section"> <div class="back-section">
<button onclick="goBackToFeed()" class="back-btn">← Back to Feed</button> <a href="{{ url_for('index') }}" class="back-btn">← Back to Feed</a>
</div> </div>
<!-- Post Content --> <!-- Post Content -->
<article class="post-detail"> <article class="post-detail">
<div class="post-header"> <div class="post-header">
{% if post.url and not post.url.startswith('/') %}
<a href="{{ post.url }}" target="_blank" class="platform-badge platform-{{ post.platform }}" title="View on {{ post.platform.title() }}">
{{ post.platform.title()[:1] }}
</a>
{% else %}
<div class="platform-badge platform-{{ post.platform }}"> <div class="platform-badge platform-{{ post.platform }}">
{{ post.platform.title()[:1] }} {{ post.platform.title()[:1] }}
</div> </div>
{% endif %}
<div class="post-meta"> <div class="post-meta">
<span class="post-author">{{ post.author }}</span> <span class="post-author">{{ post.author }}</span>
<span class="post-separator"></span> <span class="post-separator"></span>
@@ -202,6 +162,14 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
text-decoration: none;
color: inherit;
transition: all 0.2s ease;
}
.nav-left .logo-section:hover {
transform: scale(1.02);
text-decoration: none;
} }
.nav-logo { .nav-logo {
@@ -359,6 +327,35 @@
transform: translateY(-1px); transform: translateY(-1px);
} }
.anonymous-actions {
display: flex;
gap: 12px;
}
.login-btn, .register-btn {
padding: 8px 16px;
border-radius: 8px;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.login-btn {
color: #2c3e50;
border: 1px solid #e2e8f0;
}
.register-btn {
background: #4db6ac;
color: white;
}
.login-btn:hover, .register-btn:hover {
transform: translateY(-1px);
text-decoration: none;
}
/* Main Content */ /* Main Content */
.main-content.single-post { .main-content.single-post {
max-width: 1200px; max-width: 1200px;
@@ -661,12 +658,12 @@
<script> <script>
function goBackToFeed() { function goBackToFeed() {
// Try to go back to the dashboard if possible // Try to go back in browser history first
if (document.referrer && document.referrer.includes(window.location.origin)) { if (window.history.length > 1 && document.referrer && document.referrer.includes(window.location.origin)) {
window.history.back(); window.history.back();
} else { } else {
// Fallback to dashboard // Fallback to dashboard - use the proper URL
window.location.href = '/'; window.location.href = {{ url_for('index')|tojson }};
} }
} }

View File

@@ -5,7 +5,7 @@
{% block extra_css %} {% block extra_css %}
<style> <style>
.settings-container { .settings-container {
max-width: 1000px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 24px; padding: 24px;
} }
@@ -83,8 +83,21 @@
.community-grid { .community-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px; gap: 16px;
max-width: 100%;
}
@media (max-width: 768px) {
.community-grid {
grid-template-columns: 1fr;
}
}
@media (min-width: 1400px) {
.community-grid {
grid-template-columns: repeat(3, 1fr);
}
} }
.community-item { .community-item {
@@ -237,6 +250,12 @@
{% block content %} {% block content %}
{% include '_nav.html' %} {% include '_nav.html' %}
<div class="settings-container"> <div class="settings-container">
<nav style="margin-bottom: 24px;">
<a href="{{ url_for('settings') }}" style="color: var(--primary-color); text-decoration: none; font-weight: 500;">
← Back to Settings
</a>
</nav>
<div class="settings-header"> <div class="settings-header">
<h1>Community Settings</h1> <h1>Community Settings</h1>
<p>Select which communities, subreddits, and sources to include in your feed</p> <p>Select which communities, subreddits, and sources to include in your feed</p>