Enhance search dropdowns across site

- Limit stories page dropdown to only show stories (remove verse refs, topics)
- Add universal search dropdown to /search page
- Add stories-only dropdown to kids stories page
- Disable verse tooltips on all search dropdown links
- Fix cursor style on dropdown links (no help cursor)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-26 11:07:39 -05:00
parent 0c355850ce
commit bbda34e223
4 changed files with 485 additions and 121 deletions
+16
View File
@@ -96,6 +96,18 @@
cursor: help;
}
/* But not in search dropdowns */
.search-results-dropdown a,
.search-results-dropdown a[href*="/book/"],
.search-dropdown a,
.search-dropdown a[href*="/book/"],
.lookup-results-dropdown a,
.lookup-results-dropdown a[href*="/book/"],
.story-search-dropdown a,
.story-search-dropdown a[href*="/book/"] {
cursor: pointer !important;
}
.verse-tooltip {
position: absolute;
background: var(--bg-color);
@@ -1777,6 +1789,10 @@
// Skip if this is a verse number link (the number at the start of each verse)
if (target.classList.contains('verse-number-link')) return;
// Skip links in search dropdowns
if (target.closest('.search-results-dropdown') || target.closest('.search-dropdown') ||
target.closest('.lookup-results-dropdown') || target.closest('.story-search-dropdown')) return;
var verseInfo = parseVerseUrl(target.pathname + target.hash);
if (!verseInfo) return;
+276 -14
View File
@@ -7,27 +7,36 @@
<style>
.search-form {
margin: 2rem 0;
position: relative;
}
.search-input-wrapper {
position: relative;
display: inline-block;
width: 100%;
max-width: 55%;
}
.search-input {
width: 100%;
max-width: 55%;
padding: 0.75rem;
font-size: 1.1rem;
border: 1px solid #ccc;
border: 1px solid var(--border-color, #ccc);
border-radius: 4px;
background: var(--bg-color);
color: var(--text-color);
}
.search-input:focus {
outline: none;
border-color: #111;
border-color: var(--text-color, #111);
}
.search-button {
margin-top: 1rem;
padding: 0.75rem 1.5rem;
background: #111;
color: #fff;
background: var(--text-color, #111);
color: var(--bg-color, #fff);
border: none;
border-radius: 4px;
cursor: pointer;
@@ -35,7 +44,68 @@
}
.search-button:hover {
background: #333;
opacity: 0.85;
}
.search-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-color);
border: 1px solid var(--border-color-darker, #999);
border-top: none;
border-radius: 0 0 4px 4px;
max-height: 400px;
overflow-y: auto;
z-index: 1000;
display: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.search-dropdown.show {
display: block;
}
.search-result-category {
padding: 0.5rem 0.75rem 0.25rem;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-tertiary);
background: var(--code-bg);
border-bottom: 1px solid var(--border-color);
}
.search-result-item {
display: block;
padding: 0.5rem 0.75rem;
text-decoration: none;
color: var(--text-color);
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: background 0.15s;
}
.search-result-item:hover,
.search-result-item.selected {
background: var(--code-bg);
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-title {
font-weight: 500;
font-size: 0.95rem;
}
.search-result-subtitle {
font-size: 0.8rem;
color: var(--text-tertiary);
margin-top: 0.1rem;
}
.search-stats {
@@ -95,14 +165,19 @@
<section>
<form class="search-form" method="get" action="/search">
<input
type="text"
name="q"
value="{{ query }}"
placeholder="Enter words or phrases to search..."
class="search-input"
autofocus
>
<div class="search-input-wrapper">
<input
type="text"
name="q"
id="search-input"
value="{{ query }}"
placeholder="Enter words or phrases to search..."
class="search-input"
autocomplete="off"
autofocus
>
<div id="search-dropdown" class="search-dropdown"></div>
</div>
<button type="submit" class="search-button">Search</button>
</form>
@@ -162,4 +237,191 @@
</div>
{% endif %}
</section>
<script>
document.addEventListener('DOMContentLoaded', function() {
var searchInput = document.getElementById('search-input');
var dropdown = document.getElementById('search-dropdown');
var currentResults = [];
var selectedIndex = -1;
var searchTimeout = null;
var bookMap = {
'gen': 'Genesis', 'ex': 'Exodus', 'lev': 'Leviticus', 'num': 'Numbers', 'deut': 'Deuteronomy',
'josh': 'Joshua', 'judg': 'Judges', 'ruth': 'Ruth', 'ps': 'Psalms', 'prov': 'Proverbs',
'isa': 'Isaiah', 'jer': 'Jeremiah', 'dan': 'Daniel', 'matt': 'Matthew', 'mk': 'Mark',
'lk': 'Luke', 'jn': 'John', 'acts': 'Acts', 'rom': 'Romans', 'rev': 'Revelation',
'genesis': 'Genesis', 'exodus': 'Exodus', 'matthew': 'Matthew', 'mark': 'Mark',
'luke': 'Luke', 'john': 'John', 'romans': 'Romans', 'revelation': 'Revelation'
};
function capitalizeBook(name) {
return bookMap[name.toLowerCase()] || name;
}
function parseVerseReference(input) {
var match = input.match(/^(.+)\s+(\d+):(\d+)$/i);
if (match) {
var book = capitalizeBook(match[1].trim());
return { url: '/book/' + encodeURIComponent(book) + '/chapter/' + match[2] + '/verse/' + match[3], display: book + ' ' + match[2] + ':' + match[3] };
}
match = input.match(/^(.+)\s+(\d+)$/i);
if (match) {
var book = capitalizeBook(match[1].trim());
return { url: '/book/' + encodeURIComponent(book) + '/chapter/' + match[2], display: book + ' ' + match[2] };
}
return null;
}
function showDropdown(html) {
dropdown.innerHTML = html;
dropdown.classList.add('show');
}
function hideDropdown() {
dropdown.classList.remove('show');
currentResults = [];
selectedIndex = -1;
}
function updateSelection() {
var items = dropdown.querySelectorAll('.search-result-item');
items.forEach(function(item, i) {
if (i === selectedIndex) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
});
}
function renderDropdown(query, apiResults) {
var html = '';
currentResults = [];
// Check for verse reference
var verseRef = parseVerseReference(query);
if (verseRef) {
html += '<div class="search-result-category">Go to Verse</div>';
currentResults.push(verseRef.url);
html += '<a href="' + verseRef.url + '" class="search-result-item selected">';
html += '<div class="search-result-title">' + verseRef.display + '</div>';
html += '</a>';
}
// API results
if (apiResults && apiResults.results) {
var results = apiResults.results;
// Books
if (results.books && results.books.length > 0) {
html += '<div class="search-result-category">Books</div>';
results.books.forEach(function(book) {
currentResults.push(book.url);
html += '<a href="' + book.url + '" class="search-result-item">';
html += '<div class="search-result-title">' + book.name + '</div>';
html += '</a>';
});
}
// Verses
if (results.verses && results.verses.length > 0) {
html += '<div class="search-result-category">Verses</div>';
results.verses.forEach(function(verse) {
currentResults.push(verse.url);
html += '<a href="' + verse.url + '" class="search-result-item">';
html += '<div class="search-result-title">' + verse.reference + '</div>';
html += '<div class="search-result-subtitle">' + verse.text + '</div>';
html += '</a>';
});
}
// Topics
if (results.topics && results.topics.length > 0) {
html += '<div class="search-result-category">Topics</div>';
results.topics.forEach(function(topic) {
currentResults.push(topic.url);
html += '<a href="' + topic.url + '" class="search-result-item">';
html += '<div class="search-result-title">' + topic.name + '</div>';
html += '</a>';
});
}
// Stories
if (results.stories && results.stories.length > 0) {
html += '<div class="search-result-category">Stories</div>';
results.stories.forEach(function(story) {
currentResults.push(story.url);
html += '<a href="' + story.url + '" class="search-result-item">';
html += '<div class="search-result-title">' + story.title + '</div>';
html += '</a>';
});
}
}
if (html) {
selectedIndex = currentResults.length > 0 ? 0 : -1;
showDropdown(html);
} else {
hideDropdown();
}
}
if (searchInput) {
searchInput.addEventListener('input', function() {
var query = this.value.trim();
clearTimeout(searchTimeout);
if (query.length < 2) {
hideDropdown();
return;
}
// Show verse reference immediately if detected
var verseRef = parseVerseReference(query);
if (verseRef) {
renderDropdown(query, null);
}
// Fetch API results with debounce
searchTimeout = setTimeout(function() {
fetch('/api/universal-search?q=' + encodeURIComponent(query) + '&limit=5')
.then(function(r) { return r.json(); })
.then(function(data) {
renderDropdown(query, data);
})
.catch(function(err) {
console.error('Search error:', err);
});
}, 200);
});
searchInput.addEventListener('keydown', function(e) {
if (!dropdown.classList.contains('show')) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, currentResults.length - 1);
updateSelection();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, -1);
updateSelection();
} else if (e.key === 'Enter' && selectedIndex >= 0 && currentResults[selectedIndex]) {
e.preventDefault();
window.location.href = currentResults[selectedIndex];
} else if (e.key === 'Escape') {
hideDropdown();
}
});
// Click outside to close dropdown
document.addEventListener('click', function(e) {
if (!dropdown.contains(e.target) && e.target !== searchInput) {
hideDropdown();
}
});
}
});
</script>
{% endblock %}
+5 -85
View File
@@ -280,35 +280,6 @@ document.addEventListener('DOMContentLoaded', function() {
var noResults = document.getElementById('no-results');
var currentResults = [];
var selectedIndex = -1;
var searchTimeout = null;
// Book name mapping for verse references
var bookMap = {
'gen': 'Genesis', 'ex': 'Exodus', 'lev': 'Leviticus', 'num': 'Numbers', 'deut': 'Deuteronomy',
'josh': 'Joshua', 'judg': 'Judges', 'ruth': 'Ruth', 'ps': 'Psalms', 'prov': 'Proverbs',
'isa': 'Isaiah', 'jer': 'Jeremiah', 'dan': 'Daniel', 'matt': 'Matthew', 'mk': 'Mark',
'lk': 'Luke', 'jn': 'John', 'acts': 'Acts', 'rom': 'Romans', 'rev': 'Revelation',
'genesis': 'Genesis', 'exodus': 'Exodus', 'matthew': 'Matthew', 'mark': 'Mark',
'luke': 'Luke', 'john': 'John', 'romans': 'Romans', 'revelation': 'Revelation'
};
function capitalizeBook(name) {
return bookMap[name.toLowerCase()] || name;
}
function parseVerseReference(input) {
var match = input.match(/^(.+)\s+(\d+):(\d+)$/i);
if (match) {
var book = capitalizeBook(match[1].trim());
return { url: '/book/' + encodeURIComponent(book) + '/chapter/' + match[2] + '/verse/' + match[3], display: book + ' ' + match[2] + ':' + match[3] };
}
match = input.match(/^(.+)\s+(\d+)$/i);
if (match) {
var book = capitalizeBook(match[1].trim());
return { url: '/book/' + encodeURIComponent(book) + '/chapter/' + match[2], display: book + ' ' + match[2] };
}
return null;
}
function filterStories(query) {
if (!query) {
@@ -399,27 +370,17 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
function renderDropdown(query, apiResults) {
function renderDropdown(query) {
var html = '';
currentResults = [];
// Check for verse reference
var verseRef = parseVerseReference(query);
if (verseRef) {
html += '<div class="search-result-category">Go to Verse</div>';
currentResults.push(verseRef.url);
html += '<a href="' + verseRef.url + '" class="search-result-item' + (currentResults.length === 1 ? ' selected' : '') + '">';
html += '<div class="search-result-title">' + verseRef.display + '</div>';
html += '</a>';
}
// Matching stories on this page
var matchingStories = getMatchingStories(query.toLowerCase());
if (matchingStories.length > 0) {
html += '<div class="search-result-category">Stories on this page</div>';
html += '<div class="search-result-category">Stories</div>';
matchingStories.forEach(function(story) {
currentResults.push(story.url);
var isSelected = currentResults.length === 1 && !verseRef;
var isSelected = currentResults.length === 1;
html += '<a href="' + story.url + '" class="search-result-item' + (isSelected ? ' selected' : '') + '">';
html += '<div class="search-result-title">' + story.title + '</div>';
if (story.description) {
@@ -429,34 +390,6 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// API results for other content
if (apiResults && apiResults.results) {
var results = apiResults.results;
// Topics
if (results.topics && results.topics.length > 0) {
html += '<div class="search-result-category">Topics</div>';
results.topics.slice(0, 3).forEach(function(topic) {
currentResults.push(topic.url);
html += '<a href="' + topic.url + '" class="search-result-item">';
html += '<div class="search-result-title">' + topic.name + '</div>';
html += '</a>';
});
}
// Verses
if (results.verses && results.verses.length > 0) {
html += '<div class="search-result-category">Verses</div>';
results.verses.slice(0, 3).forEach(function(verse) {
currentResults.push(verse.url);
html += '<a href="' + verse.url + '" class="search-result-item">';
html += '<div class="search-result-title">' + verse.reference + '</div>';
html += '<div class="search-result-subtitle">' + verse.text + '</div>';
html += '</a>';
});
}
}
if (html) {
selectedIndex = currentResults.length > 0 ? 0 : -1;
showDropdown(html);
@@ -467,7 +400,6 @@ document.addEventListener('DOMContentLoaded', function() {
searchInput.addEventListener('input', function() {
var query = this.value.trim();
clearTimeout(searchTimeout);
// Filter stories on page
filterStories(query.toLowerCase());
@@ -477,20 +409,8 @@ document.addEventListener('DOMContentLoaded', function() {
return;
}
// Show immediate results (verse ref + matching stories)
renderDropdown(query, null);
// Fetch API results with debounce
searchTimeout = setTimeout(function() {
fetch('/api/universal-search?q=' + encodeURIComponent(query) + '&limit=3')
.then(function(r) { return r.json(); })
.then(function(data) {
renderDropdown(query, data);
})
.catch(function(err) {
console.error('Search error:', err);
});
}, 200);
// Show matching stories in dropdown
renderDropdown(query);
});
searchInput.addEventListener('keydown', function(e) {
+188 -22
View File
@@ -74,6 +74,7 @@
.story-search {
max-width: 400px;
margin: 1.5rem auto;
position: relative;
}
.story-search input {
width: 100%;
@@ -90,6 +91,60 @@
.story-search input::placeholder {
color: #9ca3af;
}
.story-search-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #fff;
border: 2px solid #e5e7eb;
border-top: none;
border-radius: 0 0 16px 16px;
max-height: 350px;
overflow-y: auto;
z-index: 1000;
display: none;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.15);
}
.story-search-dropdown.show {
display: block;
}
.search-result-category {
padding: 0.5rem 1rem 0.25rem;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #9ca3af;
background: linear-gradient(135deg, #f0f9ff 0%, #faf5ff 100%);
border-bottom: 1px solid #e5e7eb;
}
.search-result-item {
display: block;
padding: 0.6rem 1rem;
text-decoration: none;
color: #4338ca;
border-bottom: 1px solid #e5e7eb;
cursor: pointer;
transition: background 0.15s;
}
.search-result-item:hover,
.search-result-item.selected {
background: linear-gradient(135deg, #f0f9ff 0%, #faf5ff 100%);
}
.search-result-item:last-child {
border-bottom: none;
border-radius: 0 0 14px 14px;
}
.search-result-title {
font-weight: 500;
font-size: 0.95rem;
}
.search-result-subtitle {
font-size: 0.8rem;
color: #9ca3af;
margin-top: 0.1rem;
}
.stories-grid {
display: grid;
@@ -230,6 +285,7 @@
<div class="story-search">
<input type="text" id="story-search" placeholder="Search for a story..." autocomplete="off">
<div id="story-search-dropdown" class="story-search-dropdown"></div>
</div>
<section>
@@ -288,33 +344,33 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('story-search');
const stories = document.querySelectorAll('.kids-story-card');
const categories = document.querySelectorAll('.category-section');
const noResults = document.getElementById('no-results');
searchInput.addEventListener('input', function() {
const query = this.value.toLowerCase().trim();
var searchInput = document.getElementById('story-search');
var dropdown = document.getElementById('story-search-dropdown');
var stories = document.querySelectorAll('.kids-story-card');
var categorySections = document.querySelectorAll('.category-section');
var noResults = document.getElementById('no-results');
var currentResults = [];
var selectedIndex = -1;
function filterStories(query) {
if (!query) {
stories.forEach(s => s.classList.remove('hidden'));
categories.forEach(c => c.classList.remove('hidden'));
stories.forEach(function(s) { s.classList.remove('hidden'); });
categorySections.forEach(function(c) { c.classList.remove('hidden'); });
noResults.classList.remove('visible');
return;
}
let matchCount = 0;
var matchCount = 0;
stories.forEach(function(story) {
var title = story.dataset.title || '';
var description = story.dataset.description || '';
var characters = story.dataset.characters || '';
var themes = story.dataset.themes || '';
stories.forEach(story => {
const title = story.dataset.title || '';
const description = story.dataset.description || '';
const characters = story.dataset.characters || '';
const themes = story.dataset.themes || '';
const matches = title.includes(query) ||
description.includes(query) ||
characters.includes(query) ||
themes.includes(query);
var matches = title.includes(query) ||
description.includes(query) ||
characters.includes(query) ||
themes.includes(query);
if (matches) {
story.classList.remove('hidden');
@@ -324,8 +380,8 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
categories.forEach(category => {
const visibleStories = category.querySelectorAll('.kids-story-card:not(.hidden)');
categorySections.forEach(function(category) {
var visibleStories = category.querySelectorAll('.kids-story-card:not(.hidden)');
if (visibleStories.length === 0) {
category.classList.add('hidden');
} else {
@@ -338,6 +394,116 @@ document.addEventListener('DOMContentLoaded', function() {
} else {
noResults.classList.remove('visible');
}
}
function getMatchingStories(query) {
var matches = [];
stories.forEach(function(story) {
var title = story.dataset.title || '';
var description = story.dataset.description || '';
var characters = story.dataset.characters || '';
var themes = story.dataset.themes || '';
if (title.includes(query) || description.includes(query) ||
characters.includes(query) || themes.includes(query)) {
var link = story.querySelector('h3 a');
if (link) {
matches.push({
title: link.textContent,
url: link.getAttribute('href'),
description: story.querySelector('.description')?.textContent?.substring(0, 50) + '...'
});
}
}
});
return matches.slice(0, 5);
}
function showDropdown(html) {
dropdown.innerHTML = html;
dropdown.classList.add('show');
}
function hideDropdown() {
dropdown.classList.remove('show');
currentResults = [];
selectedIndex = -1;
}
function updateSelection() {
var items = dropdown.querySelectorAll('.search-result-item');
items.forEach(function(item, i) {
if (i === selectedIndex) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
});
}
function renderDropdown(query) {
var html = '';
currentResults = [];
var matchingStories = getMatchingStories(query.toLowerCase());
if (matchingStories.length > 0) {
html += '<div class="search-result-category">Stories</div>';
matchingStories.forEach(function(story) {
currentResults.push(story.url);
var isSelected = currentResults.length === 1;
html += '<a href="' + story.url + '" class="search-result-item' + (isSelected ? ' selected' : '') + '">';
html += '<div class="search-result-title">' + story.title + '</div>';
if (story.description) {
html += '<div class="search-result-subtitle">' + story.description + '</div>';
}
html += '</a>';
});
}
if (html) {
selectedIndex = currentResults.length > 0 ? 0 : -1;
showDropdown(html);
} else {
hideDropdown();
}
}
searchInput.addEventListener('input', function() {
var query = this.value.trim();
filterStories(query.toLowerCase());
if (query.length < 2) {
hideDropdown();
return;
}
renderDropdown(query);
});
searchInput.addEventListener('keydown', function(e) {
if (!dropdown.classList.contains('show')) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, currentResults.length - 1);
updateSelection();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, -1);
updateSelection();
} else if (e.key === 'Enter' && selectedIndex >= 0 && currentResults[selectedIndex]) {
e.preventDefault();
window.location.href = currentResults[selectedIndex];
} else if (e.key === 'Escape') {
hideDropdown();
}
});
document.addEventListener('click', function(e) {
if (!dropdown.contains(e.target) && e.target !== searchInput) {
hideDropdown();
}
});
});
</script>