Files
balanceboard/templates/bookmarks.html
chelsea cdc415b0c1 Implement comprehensive bookmark/save system
Added full bookmark functionality for users to save posts:

Backend Features:
- New Bookmark model with user_id, post_uuid, and cached metadata
- API endpoints:
  - POST /api/bookmark - Toggle bookmark status
  - GET /api/bookmarks - Get user's bookmarks with pagination
  - GET /api/bookmark-status/<uuid> - Check if post is bookmarked
- Database migration script for bookmarks table

Frontend Features:
- New /bookmarks page with post list and management
- Bookmark buttons on post cards with save/unsave toggle
- Real-time bookmark status loading and updates
- Navigation menu integration
- Responsive design with archived post handling

UI Components:
- Modern bookmark button with hover states
- Pagination for bookmark listings
- Empty state for users with no bookmarks
- Error handling and loading states
- Remove bookmark functionality

The system handles:
- Unique bookmarks per user/post combination
- Cached post metadata for performance
- Graceful handling of deleted/archived posts
- Authentication requirements for all bookmark features

Users can now save posts for later reading and manage their bookmarks
through a dedicated bookmarks page.

Fixes #20

~claude

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

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

272 lines
7.9 KiB
HTML

{% extends "base.html" %}
{% block title %}Bookmarks - {{ APP_NAME }}{% endblock %}
{% block content %}
{% include '_nav.html' %}
<div style="max-width: 1200px; margin: 0 auto; padding: 24px;">
<div style="margin-bottom: 32px;">
<h1 style="color: var(--text-primary); margin-bottom: 8px;">📚 Your Bookmarks</h1>
<p style="color: var(--text-secondary); font-size: 1.1rem;">Posts you've saved for later reading</p>
</div>
<div id="bookmarks-container">
<div id="loading" style="text-align: center; padding: 40px; color: var(--text-secondary);">
<div style="font-size: 1.2rem;">Loading your bookmarks...</div>
</div>
</div>
<!-- Pagination -->
<div id="pagination" style="display: none; text-align: center; margin-top: 32px;">
<button id="prev-btn" style="padding: 8px 16px; margin: 0 8px; background: var(--surface-elevation-1); border: 1px solid var(--border-color); border-radius: 6px; color: var(--text-primary); cursor: pointer;">← Previous</button>
<span id="page-info" style="margin: 0 16px; color: var(--text-secondary);"></span>
<button id="next-btn" style="padding: 8px 16px; margin: 0 8px; background: var(--surface-elevation-1); border: 1px solid var(--border-color); border-radius: 6px; color: var(--text-primary); cursor: pointer;">Next →</button>
</div>
</div>
<style>
.bookmark-item {
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
transition: all 0.2s ease;
}
.bookmark-item:hover {
border-color: var(--primary-color);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.bookmark-item.archived {
opacity: 0.6;
border-style: dashed;
}
.bookmark-header {
display: flex;
justify-content: between;
align-items: flex-start;
margin-bottom: 12px;
}
.bookmark-title {
color: var(--text-primary);
font-size: 1.2rem;
font-weight: 600;
text-decoration: none;
flex: 1;
margin-right: 12px;
}
.bookmark-title:hover {
color: var(--primary-color);
}
.bookmark-remove {
background: var(--error-color);
color: white;
border: none;
border-radius: 6px;
padding: 6px 12px;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
}
.bookmark-remove:hover {
background: var(--error-hover);
}
.bookmark-meta {
display: flex;
gap: 16px;
margin-bottom: 8px;
font-size: 0.9rem;
color: var(--text-secondary);
}
.bookmark-meta span {
display: flex;
align-items: center;
gap: 4px;
}
.bookmark-preview {
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 12px;
}
.bookmark-date {
font-size: 0.85rem;
color: var(--text-tertiary);
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
}
.empty-state h3 {
font-size: 1.3rem;
margin-bottom: 8px;
color: var(--text-primary);
}
.error-state {
text-align: center;
padding: 40px 20px;
color: var(--error-color);
background: var(--error-bg);
border-radius: 8px;
margin: 20px 0;
}
</style>
<script>
let currentPage = 1;
let pagination = null;
async function loadBookmarks(page = 1) {
try {
document.getElementById('loading').style.display = 'block';
const response = await fetch(`/api/bookmarks?page=${page}&per_page=20`);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to load bookmarks');
}
renderBookmarks(data.posts);
updatePagination(data.pagination);
currentPage = page;
} catch (error) {
console.error('Error loading bookmarks:', error);
document.getElementById('bookmarks-container').innerHTML = `
<div class="error-state">
<h3>Error loading bookmarks</h3>
<p>${error.message}</p>
<button onclick="loadBookmarks()" style="margin-top: 12px; padding: 8px 16px; background: var(--primary-color); color: white; border: none; border-radius: 6px; cursor: pointer;">Retry</button>
</div>
`;
}
}
function renderBookmarks(posts) {
document.getElementById('loading').style.display = 'none';
const container = document.getElementById('bookmarks-container');
if (posts.length === 0) {
container.innerHTML = `
<div class="empty-state">
<h3>📚 No bookmarks yet</h3>
<p>Start exploring and bookmark posts you want to read later!</p>
<a href="/" style="display: inline-block; margin-top: 16px; padding: 12px 24px; background: var(--primary-color); color: white; text-decoration: none; border-radius: 8px;">Browse Posts</a>
</div>
`;
return;
}
container.innerHTML = posts.map(post => `
<div class="bookmark-item ${post.archived ? 'archived' : ''}">
<div class="bookmark-header">
<a href="${post.url}" class="bookmark-title">${post.title}</a>
<button class="bookmark-remove" onclick="removeBookmark('${post.id}', this)">
🗑️ Remove
</button>
</div>
<div class="bookmark-meta">
<span>👤 ${post.author}</span>
<span>📍 ${post.source}</span>
<span>⭐ ${post.score}</span>
<span>💬 ${post.comments_count}</span>
${post.archived ? '<span style="color: var(--warning-color);">📦 Archived</span>' : ''}
</div>
<div class="bookmark-preview">${post.content_preview}</div>
<div class="bookmark-date">
Bookmarked on ${new Date(post.bookmarked_at).toLocaleDateString()}
</div>
</div>
`).join('');
}
function updatePagination(paginationData) {
pagination = paginationData;
const paginationEl = document.getElementById('pagination');
if (paginationData.total_pages <= 1) {
paginationEl.style.display = 'none';
return;
}
paginationEl.style.display = 'block';
document.getElementById('prev-btn').disabled = !paginationData.has_prev;
document.getElementById('next-btn').disabled = !paginationData.has_next;
document.getElementById('page-info').textContent = `Page ${paginationData.current_page} of ${paginationData.total_pages}`;
}
async function removeBookmark(postId, button) {
if (!confirm('Are you sure you want to remove this bookmark?')) {
return;
}
try {
button.disabled = true;
button.textContent = 'Removing...';
const response = await fetch('/api/bookmark', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ post_uuid: postId })
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to remove bookmark');
}
// Reload bookmarks to reflect changes
loadBookmarks(currentPage);
} catch (error) {
console.error('Error removing bookmark:', error);
alert('Error removing bookmark: ' + error.message);
button.disabled = false;
button.textContent = '🗑️ Remove';
}
}
// Pagination event listeners
document.getElementById('prev-btn').addEventListener('click', () => {
if (pagination && pagination.has_prev) {
loadBookmarks(currentPage - 1);
}
});
document.getElementById('next-btn').addEventListener('click', () => {
if (pagination && pagination.has_next) {
loadBookmarks(currentPage + 1);
}
});
// Load bookmarks on page load
document.addEventListener('DOMContentLoaded', () => {
loadBookmarks();
});
</script>
{% endblock %}