Add copy and bookmark buttons to sticky breadcrumb

Copy button (⎘):
- Copies page content to clipboard
- Shows ✓ checkmark briefly when copied

Bookmark button (☆/★):
- Toggle bookmark for current page
- Stored in localStorage as kjvBookmarks
- Star fills in (★) when page is bookmarked

Bookmarks page (/bookmarks):
- View all saved bookmarks
- Shows title and date saved
- Remove individual bookmarks or clear all
- Reads from localStorage on client side

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-03 14:44:19 -05:00
parent 2ab98c08a3
commit de12fbbeb2
4 changed files with 313 additions and 0 deletions
+20
View File
@@ -229,3 +229,23 @@ async def verse_of_the_day_page(request: Request):
"breadcrumbs": breadcrumbs
}
)
@router.get("/bookmarks", response_class=HTMLResponse)
async def bookmarks_page(request: Request):
"""Bookmarks page - displays user's saved bookmarks from localStorage"""
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "Bookmarks", "url": None}
]
return templates.TemplateResponse(
request,
"bookmarks.html",
{
"books": books,
"breadcrumbs": breadcrumbs
}
)
+76
View File
@@ -32,6 +32,82 @@ function toggleFontSize() {
}
}
// Copy page text to clipboard
function copyPageText() {
var btn = document.getElementById('copy-btn');
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').forEach(function(el) {
el.remove();
});
var text = (clone.textContent || clone.innerText || '').trim();
navigator.clipboard.writeText(text).then(function() {
if (btn) {
btn.classList.add('copied');
setTimeout(function() {
btn.classList.remove('copied');
}, 1500);
}
});
}
// Bookmark functionality
function getBookmarks() {
try {
return JSON.parse(localStorage.getItem('kjvBookmarks') || '[]');
} catch (e) {
return [];
}
}
function saveBookmarks(bookmarks) {
localStorage.setItem('kjvBookmarks', JSON.stringify(bookmarks));
}
function isBookmarked(url) {
var bookmarks = getBookmarks();
return bookmarks.some(function(b) { return b.url === url; });
}
function toggleBookmark() {
var btn = document.getElementById('bookmark-btn');
var url = window.location.pathname;
var title = document.title.replace(' - KJV Study', '').replace(' - KJV Bible', '');
var bookmarks = getBookmarks();
var existingIndex = bookmarks.findIndex(function(b) { return b.url === url; });
if (existingIndex >= 0) {
// Remove bookmark
bookmarks.splice(existingIndex, 1);
if (btn) btn.classList.remove('bookmarked');
} else {
// Add bookmark
bookmarks.unshift({
url: url,
title: title,
date: new Date().toISOString()
});
if (btn) btn.classList.add('bookmarked');
}
saveBookmarks(bookmarks);
}
// Check bookmark state on page load
(function() {
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('bookmark-btn');
if (btn && isBookmarked(window.location.pathname)) {
btn.classList.add('bookmarked');
}
});
})();
// Page speech toggle (for breadcrumb button) - triggers spacebar speech
function togglePageSpeech() {
// Simulate spacebar press to use existing speech system
+18
View File
@@ -427,6 +427,22 @@
content: '■';
}
.breadcrumb-action-btn.copy-btn::before {
content: '⎘';
}
.breadcrumb-action-btn.copy-btn.copied::before {
content: '✓';
}
.breadcrumb-action-btn.bookmark-btn::before {
content: '☆';
}
.breadcrumb-action-btn.bookmark-btn.bookmarked::before {
content: '★';
}
/* Large font size */
[data-font-size="large"] article {
font-size: 1.1rem;
@@ -1418,6 +1434,8 @@
<span class="breadcrumb-actions">
<button class="breadcrumb-action-btn font-toggle" title="Toggle large text" onclick="toggleFontSize()" aria-label="Toggle large text">A</button>
<button class="breadcrumb-action-btn speech-toggle" id="speech-toggle-btn" title="Read aloud" onclick="togglePageSpeech()" aria-label="Read page aloud"></button>
<button class="breadcrumb-action-btn copy-btn" id="copy-btn" title="Copy page text" onclick="copyPageText()" aria-label="Copy page text"></button>
<button class="breadcrumb-action-btn bookmark-btn" id="bookmark-btn" title="Bookmark this page" onclick="toggleBookmark()" aria-label="Bookmark this page"></button>
<button class="breadcrumb-action-btn dark-toggle" title="Toggle dark mode" onclick="toggleDarkMode()" aria-label="Toggle dark mode"></button>
</span>
{% for crumb in breadcrumbs %}
+199
View File
@@ -0,0 +1,199 @@
{% 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 %}