mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 23:00:16 +00:00
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:
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user