Add Stars page for bookmarking with notes and navigation badges

- Create /stars page with collapsible groups, notes, and keyboard nav
- Add yellow star bookmark button in breadcrumb (☆/★)
- Add nav badges showing star count and reading plan % complete
- Change 's' shortcut from Stories to Stars
- Update toast to say "Added to Starred Pages"
- Persist group collapse state in localStorage
- Support notes with monospace font and edit/add functionality
- Update accessibility page and ? help modal

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-03 15:39:01 -05:00
parent c4d02d75ed
commit 4819ef36ec
6 changed files with 1387 additions and 220 deletions
+5 -5
View File
@@ -231,19 +231,19 @@ async def verse_of_the_day_page(request: Request):
)
@router.get("/bookmarks", response_class=HTMLResponse)
async def bookmarks_page(request: Request):
"""Bookmarks page - displays user's saved bookmarks from localStorage"""
@router.get("/stars", response_class=HTMLResponse)
async def stars_page(request: Request):
"""Stars page - displays user's saved starred pages from localStorage"""
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Bookmarks", "url": None}
{"text": "Stars", "url": None}
]
return templates.TemplateResponse(
request,
"bookmarks.html",
"stars.html",
{
"books": books,
"breadcrumbs": breadcrumbs
+142 -4
View File
@@ -50,6 +50,49 @@ function isBookmarked(url) {
return bookmarks.some(function(b) { return b.url === url; });
}
function getPageExcerpt() {
var article = document.querySelector('article');
if (!article) return '';
var clone = article.cloneNode(true);
clone.querySelectorAll('.breadcrumb, .breadcrumb-actions, .sidenote, .marginnote, .toc, script, style, nav, button, h1, h2, h3').forEach(function(el) {
el.remove();
});
var text = (clone.textContent || clone.innerText || '').trim();
// Get first meaningful chunk
text = text.replace(/\s+/g, ' ').substring(0, 300);
return text;
}
function getPageDescription() {
var meta = document.querySelector('meta[name="description"]');
return meta ? meta.getAttribute('content') : '';
}
function getPageBreadcrumbs() {
var breadcrumb = document.querySelector('.breadcrumb');
if (!breadcrumb) return [];
var crumbs = [];
breadcrumb.querySelectorAll('a, span:not(.breadcrumb-separator):not(.breadcrumb-actions)').forEach(function(el) {
if (el.classList.contains('breadcrumb-actions')) return;
if (el.closest('.breadcrumb-actions')) return;
var text = el.textContent.trim();
if (text && text !== '>' && text.length > 0) {
crumbs.push({
text: text,
url: el.tagName === 'A' ? el.getAttribute('href') : null
});
}
});
// Drop the last item (it's the current page title)
if (crumbs.length > 0) {
crumbs.pop();
}
return crumbs;
}
function toggleBookmark() {
var btn = document.getElementById('bookmark-btn');
var url = window.location.pathname;
@@ -62,29 +105,117 @@ function toggleBookmark() {
// Remove bookmark
bookmarks.splice(existingIndex, 1);
if (btn) btn.classList.remove('bookmarked');
showBookmarkToast(false);
} else {
// Add bookmark
// Add bookmark with description, breadcrumbs, and excerpt
bookmarks.unshift({
url: url,
title: title,
description: getPageDescription(),
breadcrumbs: getPageBreadcrumbs(),
excerpt: getPageExcerpt(),
date: new Date().toISOString()
});
if (btn) btn.classList.add('bookmarked');
showBookmarkToast(true);
}
saveBookmarks(bookmarks);
updateStarsBadge();
}
// Check bookmark state on page load
function showBookmarkToast(added) {
var toast = document.getElementById('bookmark-toast');
if (!toast) return;
if (added) {
toast.innerHTML = 'Added to <a href="/stars">Starred Pages</a>';
} else {
toast.innerHTML = 'Removed from Starred Pages';
}
toast.classList.add('show');
// Hide after 3 seconds
setTimeout(function() {
toast.classList.remove('show');
}, 3000);
}
// Check bookmark state on page load and update nav badges
(function() {
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('bookmark-btn');
if (btn && isBookmarked(window.location.pathname)) {
btn.classList.add('bookmarked');
}
// Update nav badges
updateStarsBadge();
updateReadingPlansBadge();
});
})();
function updateStarsBadge() {
var badge = document.getElementById('stars-badge');
if (!badge) return;
var bookmarks = getBookmarks();
badge.textContent = bookmarks.length > 0 ? bookmarks.length : '';
}
// Reading plans progress badge
function updateReadingPlansBadge() {
var badge = document.getElementById('reading-plans-badge');
if (!badge) return;
// Reading plan IDs and their total days
var planDays = {
'chronological': 365,
'one-year': 365,
'new-testament': 90,
'gospels-acts': 30,
'psalms-proverbs': 31,
'pentateuch': 40,
'prophets': 60,
'paul-epistles': 30,
'minor-prophets': 14,
'wisdom': 30,
'historical': 45,
'general-epistles': 14
};
var totalCompleted = 0;
var totalDays = 0;
var activePlans = 0;
// Check each plan
for (var planId in planDays) {
var storageKey = 'reading-plan-' + planId;
var saved = localStorage.getItem(storageKey);
if (saved) {
try {
var data = JSON.parse(saved);
if (data.completed && data.completed.length > 0) {
activePlans++;
totalCompleted += data.completed.length;
totalDays += planDays[planId];
}
} catch (e) {
// Invalid JSON, skip
}
}
}
if (activePlans === 0 || totalDays === 0) {
badge.textContent = '';
return;
}
var percent = Math.round((totalCompleted / totalDays) * 100);
badge.textContent = percent + '%';
}
// Page speech toggle (for breadcrumb button) - triggers spacebar speech
function togglePageSpeech() {
// Simulate spacebar press to use existing speech system
@@ -900,6 +1031,12 @@ document.addEventListener('keydown', function(e) {
toggleDarkMode();
}
// Cmd/Ctrl + S: Toggle star (overrides browser save)
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault();
toggleBookmark();
}
// Cmd/Ctrl + K or /: Focus search
if (((e.metaKey || e.ctrlKey) && e.key === 'k') || e.key === '/') {
e.preventDefault();
@@ -955,7 +1092,7 @@ document.addEventListener('keydown', function(e) {
break;
case 's':
e.preventDefault();
window.location.href = '/stories';
window.location.href = '/stars';
break;
case '/':
e.preventDefault();
@@ -1160,7 +1297,7 @@ function showKeyboardHelp() {
'<div class="shortcut"><kbd>8</kbd><span>Proverbs</span></div>' +
'<div class="shortcut"><kbd>9</kbd><span>Revelation</span></div>' +
'<div class="shortcut"><kbd>b</kbd><span>Books</span></div>' +
'<div class="shortcut"><kbd>s</kbd><span>Stories</span></div>' +
'<div class="shortcut"><kbd>s</kbd><span>Stars</span></div>' +
'<div class="shortcut"><kbd>r</kbd><span>Resources</span></div>' +
'<div class="shortcut"><kbd>t</kbd><span>Topics</span></div>' +
'<div class="shortcut"><kbd>p</kbd><span>PDF (when available)</span></div>' +
@@ -1184,6 +1321,7 @@ function showKeyboardHelp() {
'<div class="shortcut"><kbd>n</kbd><span>Navigate sidebar</span></div>' +
'<div class="shortcut"><kbd>⌘</kbd>+<kbd>D</kbd><span>Toggle dark mode</span></div>' +
'<div class="shortcut"><kbd>R</kbd><span>Toggle red letters</span></div>' +
'<div class="shortcut"><kbd>⌘</kbd>+<kbd>S</kbd><span>Toggle star</span></div>' +
'<div class="shortcut"><kbd>/</kbd><span>Search</span></div>' +
'<div class="shortcut"><kbd>?</kbd><span>Show this help</span></div>' +
'<div class="shortcut"><kbd>Esc</kbd><span>Close / Clear focus</span></div>' +
+5 -1
View File
@@ -62,6 +62,10 @@
<td><kbd>?</kbd></td>
<td>Show keyboard shortcuts help</td>
</tr>
<tr>
<td><kbd></kbd>+<kbd>S</kbd></td>
<td>Toggle star (bookmark current page)</td>
</tr>
</tbody>
</table>
@@ -120,7 +124,7 @@
</tr>
<tr>
<td><kbd>s</kbd></td>
<td>Stories</td>
<td>Stars (your bookmarks)</td>
</tr>
<tr>
<td><kbd>c</kbd></td>
+67 -11
View File
@@ -433,6 +433,44 @@
.breadcrumb-action-btn.bookmark-btn.bookmarked::before {
content: '★';
color: #eab308;
}
.bookmark-toast {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--text-color);
color: var(--bg-color);
padding: 0.75rem 1.25rem;
border-radius: 6px;
font-size: 0.9rem;
opacity: 0;
transition: all 0.3s ease;
z-index: 1000;
pointer-events: none;
}
.bookmark-toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
pointer-events: auto;
}
.bookmark-toast a {
color: var(--bg-color) !important;
margin-left: 0.5rem;
text-decoration: underline;
border-bottom: none !important;
}
.bookmark-toast a:hover {
opacity: 0.8;
}
[data-theme="dark"] .bookmark-toast a {
color: var(--bg-color) !important;
}
/* Large font size */
@@ -719,6 +757,23 @@
background: rgba(74, 124, 89, 0.08);
}
/* Nav badge for counts */
.nav-badge {
display: inline-block;
font-size: 0.7rem;
font-weight: 500;
background: var(--code-bg);
color: var(--text-secondary);
padding: 0.1rem 0.4rem;
border-radius: 8px;
margin-left: 0.35rem;
vertical-align: middle;
}
.nav-badge:empty {
display: none;
}
/* Sidebar search box */
.sidebar-search {
margin-bottom: 1rem;
@@ -1420,6 +1475,8 @@
</p>
{% endif %}
<div class="bookmark-toast" id="bookmark-toast"></div>
<article role="main" id="main-content">
{% if breadcrumbs %}
<nav class="breadcrumb" aria-label="Breadcrumb" id="breadcrumb">
@@ -1456,20 +1513,9 @@
<ul>
<li><a href="/" {% if request.url.path == "/" %}class="current"{% endif %}>Home</a></li>
<li><a href="/books" {% if request.url.path == "/books" or request.url.path.startswith("/book/") %}class="current"{% endif %}>Books</a></li>
<li><a href="/search">Search</a></li>
</ul>
<!-- Daily Reading -->
<details class="resource-group" open aria-label="Daily reading resources">
<summary aria-expanded="true">Daily Reading</summary>
<ul>
<li><a href="/verse-of-the-day" {% if request.url.path == "/verse-of-the-day" %}class="current"{% endif %}>Verse of the Day</a></li>
<li><a href="/random-verse" {% if request.url.path == "/random-verse" %}class="current"{% endif %}>Random Verse</a></li>
<li><a href="/reading-plans" {% if request.url.path.startswith("/reading-plans") %}class="current"{% endif %}>Reading Plans</a></li>
</ul>
</details>
<!-- Study Resources -->
<details class="resource-group" open aria-label="Study resources">
<summary aria-expanded="true">Study Resources</summary>
@@ -1478,6 +1524,7 @@
<li><a href="/stories" {% if request.url.path.startswith("/stories") %}class="current"{% endif %}>Bible Stories</a></li>
<li><a href="/topics" {% if request.url.path.startswith("/topics") %}class="current"{% endif %}>Topics</a></li>
<li><a href="/resources" {% if request.url.path == "/resources" or request.url.path.startswith("/biblical-") or request.url.path.startswith("/names-of-") or request.url.path.startswith("/parables") or request.url.path.startswith("/the-twelve-apostles") or request.url.path.startswith("/women-of-the-bible") or request.url.path.startswith("/fruits-of-the-spirit") or request.url.path.startswith("/miracles-of-jesus") or request.url.path.startswith("/prayers-of-the-bible") or request.url.path.startswith("/beatitudes") or request.url.path.startswith("/ten-commandments") or request.url.path.startswith("/armor-of-god") or request.url.path.startswith("/i-am-statements") or request.url.path.startswith("/trinity") or request.url.path.startswith("/christology") or request.url.path.startswith("/soteriology") or request.url.path.startswith("/pneumatology") or request.url.path.startswith("/eschatology") or request.url.path.startswith("/ecclesiology") or request.url.path.startswith("/types-and-shadows") or request.url.path.startswith("/messianic-prophecies") or request.url.path.startswith("/blood-in-scripture") or request.url.path.startswith("/kingdom-of-god") or request.url.path.startswith("/spirits-and-demons") or request.url.path.startswith("/personifications") or request.url.path.startswith("/bibliology") or request.url.path.startswith("/theology-proper") or request.url.path.startswith("/anthropology") or request.url.path.startswith("/hamartiology") or request.url.path.startswith("/providence") or request.url.path.startswith("/grace") or request.url.path.startswith("/justification") or request.url.path.startswith("/sanctification") or request.url.path.startswith("/law-and-gospel") or request.url.path.startswith("/worship") or request.url.path.startswith("/tetragrammaton") %}class="current"{% endif %}>Resources</a></li>
<li><a href="/random-verse" {% if request.url.path == "/random-verse" %}class="current"{% endif %}>Random Verse</a></li>
</ul>
</details>
@@ -1490,6 +1537,15 @@
</ul>
</details>
<!-- Your Library -->
<details class="resource-group" open aria-label="Your personal library">
<summary aria-expanded="true">Your Library</summary>
<ul>
<li><a href="/reading-plans" {% if request.url.path.startswith("/reading-plans") %}class="current"{% endif %}>Reading Plans <span class="nav-badge" id="reading-plans-badge"></span></a></li>
<li><a href="/stars" {% if request.url.path == "/stars" %}class="current"{% endif %}>Starred Pages <span class="nav-badge" id="stars-badge"></span></a></li>
</ul>
</details>
<!-- About -->
<div class="nav-footer">
<a href="/about" {% if request.url.path == "/about" %}class="current"{% endif %}>About</a>
-199
View File
@@ -1,199 +0,0 @@
{% extends "base.html" %}
{% block title %}Bookmarks - KJV Study{% endblock %}
{% block description %}Your saved bookmarks from KJV Study.{% endblock %}
{% block head %}
<style>
.bookmarks-container {
max-width: 700px;
}
.bookmarks-empty {
text-align: center;
padding: 3rem 1rem;
color: var(--text-secondary);
}
.bookmarks-empty p {
margin-bottom: 1rem;
}
.bookmark-list {
list-style: none;
padding: 0;
margin: 2rem 0;
}
.bookmark-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
margin-bottom: 0.75rem;
background: var(--bg-color);
transition: all 0.15s;
}
.bookmark-item:hover {
border-color: var(--link-color);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.bookmark-info {
flex: 1;
}
.bookmark-title {
font-size: 1.1rem;
font-weight: 500;
color: var(--text-color);
text-decoration: none;
border-bottom: none;
}
.bookmark-title:hover {
color: var(--link-color);
}
.bookmark-date {
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: 0.25rem;
}
.bookmark-remove {
background: none;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0.4rem 0.8rem;
cursor: pointer;
color: var(--text-secondary);
font-size: 0.85rem;
transition: all 0.15s;
}
.bookmark-remove:hover {
border-color: #c41e3a;
color: #c41e3a;
background: rgba(196, 30, 58, 0.05);
}
.clear-all-btn {
display: inline-block;
margin-top: 1rem;
padding: 0.5rem 1rem;
background: none;
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
color: var(--text-secondary);
font-size: 0.9rem;
transition: all 0.15s;
}
.clear-all-btn:hover {
border-color: #c41e3a;
color: #c41e3a;
}
[data-theme="dark"] .bookmark-item {
background: #1a1a1a;
border-color: #333;
}
[data-theme="dark"] .bookmark-item:hover {
border-color: var(--link-color);
}
</style>
{% endblock %}
{% block content %}
<div class="bookmarks-container">
<h1>Bookmarks</h1>
<div id="bookmarks-content">
<!-- Populated by JavaScript -->
</div>
</div>
<script>
(function() {
function getBookmarks() {
try {
return JSON.parse(localStorage.getItem('kjvBookmarks') || '[]');
} catch (e) {
return [];
}
}
function saveBookmarks(bookmarks) {
localStorage.setItem('kjvBookmarks', JSON.stringify(bookmarks));
}
function formatDate(dateStr) {
var date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
function removeBookmark(url) {
var bookmarks = getBookmarks();
bookmarks = bookmarks.filter(function(b) { return b.url !== url; });
saveBookmarks(bookmarks);
renderBookmarks();
}
function clearAllBookmarks() {
if (confirm('Remove all bookmarks?')) {
localStorage.removeItem('kjvBookmarks');
renderBookmarks();
}
}
function renderBookmarks() {
var container = document.getElementById('bookmarks-content');
var bookmarks = getBookmarks();
if (bookmarks.length === 0) {
container.innerHTML =
'<div class="bookmarks-empty">' +
'<p>No bookmarks yet.</p>' +
'<p>Click the <strong>☆</strong> button on any page to save it here.</p>' +
'</div>';
return;
}
var html = '<ul class="bookmark-list">';
bookmarks.forEach(function(bookmark) {
html += '<li class="bookmark-item">' +
'<div class="bookmark-info">' +
'<a href="' + bookmark.url + '" class="bookmark-title">' + bookmark.title + '</a>' +
'<div class="bookmark-date">Saved ' + formatDate(bookmark.date) + '</div>' +
'</div>' +
'<button class="bookmark-remove" onclick="removeBookmark(\'' + bookmark.url + '\')" title="Remove bookmark">Remove</button>' +
'</li>';
});
html += '</ul>';
if (bookmarks.length > 1) {
html += '<button class="clear-all-btn" onclick="clearAllBookmarks()">Clear all bookmarks</button>';
}
container.innerHTML = html;
}
// Make functions global for onclick handlers
window.removeBookmark = removeBookmark;
window.clearAllBookmarks = clearAllBookmarks;
// Initial render
renderBookmarks();
})();
</script>
{% endblock %}
File diff suppressed because it is too large Load Diff