mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 23:00:16 +00:00
3336863a4d
- Add KJVNav.initGridNav for standardized 2D grid navigation - Migrate books.html, topics.html, resources.html to use initGridNav - Add sidebarActive check to all templates with custom keyboard handlers - Add [ and ] shortcuts for prev/next chapter on chapter pages - Add [ and ] shortcuts for prev/next book on book pages - Update accessibility page with comprehensive keyboard shortcut docs - Add honest note about keyboard navigation complexity - Fix sidebar nav conflicting with main content selection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
484 lines
16 KiB
HTML
484 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{% if query %}Search Results for "{{ query }}"{% else %}Search the KJV Bible{% endif %} - KJV Study{% endblock %}
|
|
{% block description %}{% if query %}Search results for "{{ query }}" in the King James Bible.{% else %}Search the complete King James Bible.{% endif %}{% endblock %}
|
|
|
|
{% block head %}
|
|
<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%;
|
|
padding: 0.75rem;
|
|
font-size: 1.1rem;
|
|
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: var(--text-color, #111);
|
|
}
|
|
|
|
.search-button {
|
|
margin-top: 1rem;
|
|
padding: 0.75rem 1.5rem;
|
|
background: var(--text-color, #111);
|
|
color: var(--bg-color, #fff);
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.search-button:hover {
|
|
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 {
|
|
margin: 2rem 0;
|
|
color: #666;
|
|
}
|
|
|
|
.search-result {
|
|
margin: 2rem 0;
|
|
padding: 1rem;
|
|
border-left: 3px solid #111;
|
|
}
|
|
|
|
.result-reference {
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.result-reference a {
|
|
text-decoration: none;
|
|
color: #111;
|
|
}
|
|
|
|
.result-reference a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.result-text {
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.result-text mark {
|
|
background: #fffacd;
|
|
padding: 0.1rem 0.2rem;
|
|
}
|
|
|
|
.search-tips {
|
|
margin: 2rem 0;
|
|
padding: 1.5rem;
|
|
background: #f9f9f9;
|
|
border-left: 3px solid #111;
|
|
}
|
|
|
|
.search-tips h3 {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.search-tips ul {
|
|
line-height: 1.8;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<h1>Search the KJV Bible</h1>
|
|
<p class="subtitle">Search across all verses in the Authorized King James Version</p>
|
|
|
|
<section role="search" aria-label="Bible search">
|
|
<form class="search-form" method="get" action="/search" role="search">
|
|
<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"
|
|
autocorrect="off"
|
|
autocapitalize="off"
|
|
spellcheck="false"
|
|
{% if not query %}autofocus{% endif %}
|
|
aria-label="Search for Bible verses, topics, and resources"
|
|
>
|
|
<div id="search-dropdown" class="search-dropdown" role="listbox" aria-live="polite"></div>
|
|
</div>
|
|
<button type="submit" class="search-button" aria-label="Submit search query">Search</button>
|
|
</form>
|
|
|
|
{% if query %}
|
|
{% if total_results > 0 %}
|
|
<div class="search-stats" role="status" aria-live="polite">
|
|
Found <strong>{{ total_results }}</strong> result{{ 's' if total_results != 1 else '' }} for "<strong>{{ query }}</strong>"
|
|
</div>
|
|
|
|
{% if family_tree_results %}
|
|
<h2 style="margin-top: 2rem;" role="heading" aria-level="2">People in Family Tree</h2>
|
|
{% for result in family_tree_results %}
|
|
<article class="search-result">
|
|
<div class="result-reference">
|
|
<a href="{{ result.url }}">{{ result.name }}</a>
|
|
</div>
|
|
<div class="result-text" style="color: #666;">
|
|
{{ result.description }}
|
|
{% if result.birth_year != "Unknown" or result.death_year != "Unknown" %}
|
|
•
|
|
{% if result.birth_year != "Unknown" %}Born {{ result.birth_year }}{% endif %}
|
|
{% if result.death_year != "Unknown" %}{% if result.birth_year != "Unknown" %}, {% endif %}Died {{ result.death_year }}{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
</article>
|
|
{% endfor %}
|
|
{% endif %}
|
|
|
|
{% if results %}
|
|
<h2 style="margin-top: 2rem;">Bible Verses</h2>
|
|
{% for result in results %}
|
|
<article class="search-result">
|
|
<div class="result-reference">
|
|
<a href="{{ result.url }}">{{ result.reference }}</a>
|
|
</div>
|
|
<div class="result-text">{{ result.highlighted_text | link_names | safe }}</div>
|
|
</article>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% elif total_results == 0 %}
|
|
<div class="search-stats">
|
|
<p><strong>No results found</strong> for "{{ query }}". Try different words or check your spelling.</p>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% if not query or total_results == 0 %}
|
|
<div class="search-tips">
|
|
<h3>Search Tips</h3>
|
|
<ul>
|
|
<li>Search for words or phrases that appear in Bible verses</li>
|
|
<li>Enter specific verse references like "John 3:16" or "Genesis 1:1"</li>
|
|
<li>Use Roman numerals ("I John 4:8") or numbers ("1 John 4:8") for numbered books</li>
|
|
<li>Use multiple words to find verses containing all terms</li>
|
|
<li>Use Old English spellings for better KJV results (e.g., "loveth" instead of "loves")</li>
|
|
</ul>
|
|
</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>';
|
|
});
|
|
}
|
|
|
|
// 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>';
|
|
});
|
|
}
|
|
|
|
// Resources
|
|
if (results.resources && results.resources.length > 0) {
|
|
html += '<div class="search-result-category">Resources</div>';
|
|
results.resources.forEach(function(resource) {
|
|
currentResults.push(resource.url);
|
|
html += '<a href="' + resource.url + '" class="search-result-item">';
|
|
html += '<div class="search-result-title">' + resource.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>';
|
|
});
|
|
}
|
|
|
|
// Verses (at the end)
|
|
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>';
|
|
});
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Keyboard navigation for search results
|
|
var searchResults = Array.from(document.querySelectorAll('.search-result'));
|
|
if (searchResults.length > 0) {
|
|
var resultIndex = -1;
|
|
|
|
function selectResult(index) {
|
|
if (resultIndex >= 0 && resultIndex < searchResults.length) {
|
|
searchResults[resultIndex].style.outline = '';
|
|
searchResults[resultIndex].style.outlineOffset = '';
|
|
}
|
|
resultIndex = Math.max(0, Math.min(index, searchResults.length - 1));
|
|
searchResults[resultIndex].style.outline = '2px solid #4a7c59';
|
|
searchResults[resultIndex].style.outlineOffset = '4px';
|
|
searchResults[resultIndex].scrollIntoView({ behavior: 'auto', block: 'center' });
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
// Don't handle when typing in search input or dropdown is open
|
|
if (e.target === searchInput || dropdown.classList.contains('show')) return;
|
|
// Don't handle if sidebar navigation is active
|
|
if (KJVNav.sidebarActive) return;
|
|
|
|
if (e.key === 'ArrowDown' || e.key === 'j') {
|
|
e.preventDefault();
|
|
selectResult(resultIndex < 0 ? 0 : resultIndex + 1);
|
|
} else if (e.key === 'ArrowUp' || e.key === 'k') {
|
|
e.preventDefault();
|
|
if (resultIndex <= 0) selectResult(0);
|
|
else selectResult(resultIndex - 1);
|
|
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
|
|
e.preventDefault();
|
|
history.back();
|
|
} else if (e.key === 'Enter' && resultIndex >= 0) {
|
|
e.preventDefault();
|
|
var link = searchResults[resultIndex].querySelector('.result-reference a');
|
|
if (link) window.location.href = link.href;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|