Add Bible Stories section to stats page and fix story search autocomplete

- Add dedicated Bible Stories stats section showing categories, story count,
  kids versions, word counts (85k adult / 58k kids), characters, and themes
- Fix story search autocomplete on both /stories and /stories/kids pages
  (was looking for h3>a but cards are structured as a.story-card>h3)
- Increase autocomplete results from 5 to 8

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-02 18:56:50 -05:00
parent b37c7c5868
commit 0969e2a904
4 changed files with 354 additions and 25 deletions
+35
View File
@@ -83,6 +83,32 @@ async def stats(request: Request):
resource_files = len(list((data_dir / 'resources').glob('*.json')))
story_files = len(list((data_dir / 'stories').glob('*.json')))
# Bible Stories statistics
total_stories = 0
stories_with_kids = 0
total_story_characters = set()
total_story_themes = set()
total_story_words = 0
total_kids_story_words = 0
for file in (data_dir / 'stories').glob('*.json'):
try:
story_data = json.load(open(file))
stories = story_data.get('stories', [])
total_stories += len(stories)
for story in stories:
# Count words in narrative
narrative = story.get('narrative', '')
total_story_words += len(narrative.split())
if story.get('kids_narrative'):
stories_with_kids += 1
total_kids_story_words += len(story.get('kids_narrative', '').split())
for char in story.get('characters', []):
total_story_characters.add(char)
for theme in story.get('themes', []):
total_story_themes.add(theme)
except (json.JSONDecodeError, IOError):
continue
# Interlinear data size
interlinear_file = data_dir / 'interlinear.json.gz'
interlinear_size_mb = interlinear_file.stat().st_size / 1024 / 1024 if interlinear_file.exists() else 0
@@ -151,6 +177,15 @@ async def stats(request: Request):
'biographies': total_biographies,
'reading_plans': reading_plan_files,
},
'bible_stories': {
'categories': story_files,
'total_stories': total_stories,
'stories_with_kids': stories_with_kids,
'unique_characters': len(total_story_characters),
'unique_themes': len(total_story_themes),
'total_words': total_story_words,
'kids_words': total_kids_story_words,
},
'language_tools': {
'hebrew_entries': total_hebrew_entries,
'greek_entries': total_greek_entries,
+38
View File
@@ -111,6 +111,43 @@
</table>
</section>
<section>
<h2>Bible Stories</h2>
<p>Narrative retellings of Scripture for all ages
<br><a href="/stories">→ Browse Bible Stories</a>
</p>
<table>
<tr>
<td><strong>Story Categories</strong></td>
<td>{{ stats.bible_stories.categories }}</td>
</tr>
<tr>
<td><strong>Total Stories</strong></td>
<td>{{ stats.bible_stories.total_stories }}</td>
</tr>
<tr>
<td><strong>Kids Versions</strong></td>
<td>{{ stats.bible_stories.stories_with_kids }}</td>
</tr>
<tr>
<td><strong>Total Words (Adult)</strong></td>
<td>{{ "{:,}".format(stats.bible_stories.total_words) }}</td>
</tr>
<tr>
<td><strong>Total Words (Kids)</strong></td>
<td>{{ "{:,}".format(stats.bible_stories.kids_words) }}</td>
</tr>
<tr>
<td><strong>Biblical Characters</strong></td>
<td>{{ stats.bible_stories.unique_characters }}</td>
</tr>
<tr>
<td><strong>Unique Themes</strong></td>
<td>{{ stats.bible_stories.unique_themes }}</td>
</tr>
</table>
</section>
<section>
<h2>Language Tools</h2>
<p>Hebrew, Greek, and interlinear resources
@@ -195,6 +232,7 @@
<li>{{ "{:,}".format(stats.commentary.total_words) }} words of theological analysis</li>
<li>{{ "{:,}".format(stats.cross_references.total_references) }} cross-references linking related passages</li>
<li>{{ "{:,}".format(stats.language_tools.total_strongs) }} Strong's Concordance entries for word studies</li>
<li>{{ stats.bible_stories.total_stories }} Bible stories across {{ stats.bible_stories.categories }} categories</li>
<li>{{ stats.study_resources.biographies }} biblical biographies</li>
<li>{{ stats.study_resources.study_guides }} topical study guides</li>
<li>{{ stats.study_resources.reading_plans }} reading plans</li>
+64 -17
View File
@@ -136,12 +136,20 @@
border: 1px solid var(--border-color, #ddd);
border-radius: 6px;
padding: 1.25rem;
background: #fff;
background: var(--bg-color, #fff);
text-decoration: none;
color: inherit;
position: relative;
transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
[data-theme="dark"] .story-card {
background: #252525;
}
@media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) .story-card {
background: #252525;
}
}
.story-card:hover,
.story-card:focus-visible {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
@@ -158,7 +166,7 @@
}
.story-card .description {
font-size: 0.9rem;
color: #555;
color: var(--text-secondary, #555);
margin-bottom: 0.75rem;
line-height: 1.5;
}
@@ -179,7 +187,7 @@
background: var(--code-bg, #f5f5f5);
border-radius: 3px;
font-size: 0.75rem;
color: #666;
color: var(--text-secondary, #666);
}
.story-card .tag.character {
background: #e8f4f8;
@@ -189,6 +197,24 @@
background: #f0f4e8;
color: #5a7a2a;
}
[data-theme="dark"] .story-card .tag.character {
background: #1e3a4a;
color: #7fc4e0;
}
[data-theme="dark"] .story-card .tag.theme {
background: #2a3a1e;
color: #a8c87a;
}
@media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) .story-card .tag.character {
background: #1e3a4a;
color: #7fc4e0;
}
html:not([data-theme="light"]) .story-card .tag.theme {
background: #2a3a1e;
color: #a8c87a;
}
}
.story-card .links {
display: flex;
gap: 1rem;
@@ -207,12 +233,15 @@
.category-section.hidden {
display: none;
}
.story-toc li.hidden {
display: none;
}
.category-section {
margin-bottom: 3rem;
}
.category-description {
font-size: 1rem;
color: #555;
color: var(--text-secondary, #555);
margin-bottom: 1rem;
max-width: 65ch;
}
@@ -239,11 +268,16 @@
<p style="margin-top: 1rem;"><a href="/stories/kids" style="color: #8b5cf6; font-weight: 500;">View Kids Version</a> — Stories written for younger readers</p>
<div class="story-search">
<input type="text" id="story-search" placeholder="Search stories by name, character, or theme..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
<div id="story-search-dropdown" class="story-search-dropdown"></div>
</div>
<h3>Categories</h3>
<nav class="story-toc" aria-label="Story categories">
<ul>
{% for category in categories %}
<li>
<li data-category="{{ category.slug }}">
<a class="story-toc-item" href="#{{ category.slug }}">
<strong>{{ category.category }}</strong>
<span class="story-toc-count">{{ category.stories|length }} stories</span>
@@ -252,11 +286,6 @@
{% endfor %}
</ul>
</nav>
<div class="story-search">
<input type="text" id="story-search" placeholder="Search stories by name, character, or theme..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
<div id="story-search-dropdown" class="story-search-dropdown"></div>
</div>
</section>
<div class="no-results" id="no-results">
@@ -300,6 +329,7 @@ document.addEventListener('DOMContentLoaded', function() {
var dropdown = document.getElementById('story-search-dropdown');
var stories = document.querySelectorAll('.story-card');
var categorySections = document.querySelectorAll('.category-section');
var tocItems = document.querySelectorAll('.story-toc li');
var noResults = document.getElementById('no-results');
var currentResults = [];
var selectedIndex = -1;
@@ -308,6 +338,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (!query) {
stories.forEach(function(s) { s.classList.remove('hidden'); });
categorySections.forEach(function(c) { c.classList.remove('hidden'); });
tocItems.forEach(function(t) { t.classList.remove('hidden'); });
noResults.classList.remove('visible');
return;
}
@@ -333,12 +364,26 @@ document.addEventListener('DOMContentLoaded', function() {
});
categorySections.forEach(function(category) {
var categoryId = category.id;
var visibleStories = category.querySelectorAll('.story-card:not(.hidden)');
if (visibleStories.length === 0) {
var isEmpty = visibleStories.length === 0;
if (isEmpty) {
category.classList.add('hidden');
} else {
category.classList.remove('hidden');
}
// Also hide/show corresponding TOC item
tocItems.forEach(function(tocItem) {
if (tocItem.dataset.category === categoryId) {
if (isEmpty) {
tocItem.classList.add('hidden');
} else {
tocItem.classList.remove('hidden');
}
}
});
});
if (matchCount === 0) {
@@ -358,17 +403,19 @@ document.addEventListener('DOMContentLoaded', function() {
if (title.includes(query) || description.includes(query) ||
characters.includes(query) || themes.includes(query)) {
var link = story.querySelector('h3 a');
if (link) {
// The story card itself is the anchor tag
var storyTitle = story.querySelector('h3');
var storyDesc = story.querySelector('.description');
if (storyTitle) {
matches.push({
title: link.textContent,
url: link.getAttribute('href'),
description: story.querySelector('.description')?.textContent?.substring(0, 60) + '...'
title: storyTitle.textContent,
url: story.href,
description: storyDesc ? storyDesc.textContent.substring(0, 60) + '...' : ''
});
}
}
});
return matches.slice(0, 5);
return matches.slice(0, 8);
}
function showDropdown(html) {
+217 -8
View File
@@ -229,6 +229,9 @@
.category-section.hidden {
display: none;
}
.toc-item.hidden {
display: none;
}
.category-section {
margin-bottom: 3rem;
}
@@ -256,6 +259,194 @@
.no-results.visible {
display: block;
}
/* Dark mode styles */
[data-theme="dark"] .kids-page-header {
background: linear-gradient(135deg, #1e3a5f 0%, #2d1f4e 50%, #3d1f3a 100%);
}
[data-theme="dark"] .kids-page-header h1 {
color: #a5b4fc;
}
[data-theme="dark"] .kids-page-header .subtitle {
color: #c4b5fd;
}
[data-theme="dark"] .kids-page-header .intro {
color: #d1d5db;
}
[data-theme="dark"] .adult-link {
color: #c4b5fd;
}
[data-theme="dark"] .toc-item {
background: linear-gradient(135deg, #1e2a3a 0%, #2a1f3a 100%);
border-left-color: #8b5cf6;
}
[data-theme="dark"] .toc-item a {
color: #a5b4fc;
}
[data-theme="dark"] .toc-item .count {
color: #9ca3af;
}
[data-theme="dark"] .story-search input {
background: #252525;
border-color: #404040;
color: #e5e7eb;
}
[data-theme="dark"] .story-search input::placeholder {
color: #6b7280;
}
[data-theme="dark"] .story-search-dropdown {
background: #252525;
border-color: #404040;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
[data-theme="dark"] .search-result-category {
background: linear-gradient(135deg, #1e2a3a 0%, #2a1f3a 100%);
border-bottom-color: #404040;
color: #9ca3af;
}
[data-theme="dark"] .search-result-item {
color: #a5b4fc;
border-bottom-color: #404040;
}
[data-theme="dark"] .search-result-item:hover,
[data-theme="dark"] .search-result-item.selected {
background: linear-gradient(135deg, #1e2a3a 0%, #2a1f3a 100%);
}
[data-theme="dark"] .search-result-subtitle {
color: #9ca3af;
}
[data-theme="dark"] .kids-story-card {
background: #252525;
border-color: #404040;
}
[data-theme="dark"] .kids-story-card:hover,
[data-theme="dark"] .kids-story-card:focus-visible {
border-color: #8b5cf6;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.25);
}
[data-theme="dark"] .kids-story-card h3 {
color: #a5b4fc;
}
[data-theme="dark"] .kids-story-card .description {
color: #d1d5db;
}
[data-theme="dark"] .kids-story-card .scripture {
color: #c4b5fd;
}
[data-theme="dark"] .character-badge {
background: linear-gradient(135deg, #1e3a5f, #2a2f5f);
color: #93c5fd;
}
[data-theme="dark"] .theme-badge {
background: linear-gradient(135deg, #4a3f1a, #3a3520);
color: #fcd34d;
}
[data-theme="dark"] .category-section h2 {
color: #a5b4fc;
border-bottom-color: #3730a3;
}
[data-theme="dark"] .category-description {
color: #d1d5db;
}
[data-theme="dark"] .no-results {
background: linear-gradient(135deg, #4a3f1a 0%, #3a3520 100%);
color: #fcd34d;
}
/* System preference dark mode */
@media (prefers-color-scheme: dark) {
html:not([data-theme="light"]) .kids-page-header {
background: linear-gradient(135deg, #1e3a5f 0%, #2d1f4e 50%, #3d1f3a 100%);
}
html:not([data-theme="light"]) .kids-page-header h1 {
color: #a5b4fc;
}
html:not([data-theme="light"]) .kids-page-header .subtitle {
color: #c4b5fd;
}
html:not([data-theme="light"]) .kids-page-header .intro {
color: #d1d5db;
}
html:not([data-theme="light"]) .adult-link {
color: #c4b5fd;
}
html:not([data-theme="light"]) .toc-item {
background: linear-gradient(135deg, #1e2a3a 0%, #2a1f3a 100%);
border-left-color: #8b5cf6;
}
html:not([data-theme="light"]) .toc-item a {
color: #a5b4fc;
}
html:not([data-theme="light"]) .toc-item .count {
color: #9ca3af;
}
html:not([data-theme="light"]) .story-search input {
background: #252525;
border-color: #404040;
color: #e5e7eb;
}
html:not([data-theme="light"]) .story-search input::placeholder {
color: #6b7280;
}
html:not([data-theme="light"]) .story-search-dropdown {
background: #252525;
border-color: #404040;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
html:not([data-theme="light"]) .search-result-category {
background: linear-gradient(135deg, #1e2a3a 0%, #2a1f3a 100%);
border-bottom-color: #404040;
color: #9ca3af;
}
html:not([data-theme="light"]) .search-result-item {
color: #a5b4fc;
border-bottom-color: #404040;
}
html:not([data-theme="light"]) .search-result-item:hover,
html:not([data-theme="light"]) .search-result-item.selected {
background: linear-gradient(135deg, #1e2a3a 0%, #2a1f3a 100%);
}
html:not([data-theme="light"]) .search-result-subtitle {
color: #9ca3af;
}
html:not([data-theme="light"]) .kids-story-card {
background: #252525;
border-color: #404040;
}
html:not([data-theme="light"]) .kids-story-card:hover,
html:not([data-theme="light"]) .kids-story-card:focus-visible {
border-color: #8b5cf6;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.25);
}
html:not([data-theme="light"]) .kids-story-card h3 {
color: #a5b4fc;
}
html:not([data-theme="light"]) .kids-story-card .description {
color: #d1d5db;
}
html:not([data-theme="light"]) .kids-story-card .scripture {
color: #c4b5fd;
}
html:not([data-theme="light"]) .character-badge {
background: linear-gradient(135deg, #1e3a5f, #2a2f5f);
color: #93c5fd;
}
html:not([data-theme="light"]) .theme-badge {
background: linear-gradient(135deg, #4a3f1a, #3a3520);
color: #fcd34d;
}
html:not([data-theme="light"]) .category-section h2 {
color: #a5b4fc;
border-bottom-color: #3730a3;
}
html:not([data-theme="light"]) .category-description {
color: #d1d5db;
}
html:not([data-theme="light"]) .no-results {
background: linear-gradient(135deg, #4a3f1a 0%, #3a3520 100%);
color: #fcd34d;
}
}
</style>
{% endblock %}
@@ -276,7 +467,7 @@
<h3>Categories</h3>
<div class="toc-grid">
{% for category in categories %}
<div class="toc-item">
<div class="toc-item" data-category="{{ category.slug }}">
<a href="#{{ category.slug }}">{{ category.category }}</a>
<span class="count">{{ category.stories|length }} stories</span>
</div>
@@ -331,6 +522,7 @@ document.addEventListener('DOMContentLoaded', function() {
var dropdown = document.getElementById('story-search-dropdown');
var stories = document.querySelectorAll('.kids-story-card');
var categorySections = document.querySelectorAll('.category-section');
var tocItems = document.querySelectorAll('.toc-item');
var noResults = document.getElementById('no-results');
var currentResults = [];
var selectedIndex = -1;
@@ -339,6 +531,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (!query) {
stories.forEach(function(s) { s.classList.remove('hidden'); });
categorySections.forEach(function(c) { c.classList.remove('hidden'); });
tocItems.forEach(function(t) { t.classList.remove('hidden'); });
noResults.classList.remove('visible');
return;
}
@@ -364,12 +557,26 @@ document.addEventListener('DOMContentLoaded', function() {
});
categorySections.forEach(function(category) {
var categoryId = category.id;
var visibleStories = category.querySelectorAll('.kids-story-card:not(.hidden)');
if (visibleStories.length === 0) {
var isEmpty = visibleStories.length === 0;
if (isEmpty) {
category.classList.add('hidden');
} else {
category.classList.remove('hidden');
}
// Also hide/show corresponding TOC item
tocItems.forEach(function(tocItem) {
if (tocItem.dataset.category === categoryId) {
if (isEmpty) {
tocItem.classList.add('hidden');
} else {
tocItem.classList.remove('hidden');
}
}
});
});
if (matchCount === 0) {
@@ -389,17 +596,19 @@ document.addEventListener('DOMContentLoaded', function() {
if (title.includes(query) || description.includes(query) ||
characters.includes(query) || themes.includes(query)) {
var link = story.querySelector('h3 a');
if (link) {
// The story card itself is the anchor tag
var storyTitle = story.querySelector('h3');
var storyDesc = story.querySelector('.description');
if (storyTitle) {
matches.push({
title: link.textContent,
url: link.getAttribute('href'),
description: story.querySelector('.description')?.textContent?.substring(0, 50) + '...'
title: storyTitle.textContent,
url: story.href,
description: storyDesc ? storyDesc.textContent.substring(0, 50) + '...' : ''
});
}
}
});
return matches.slice(0, 5);
return matches.slice(0, 8);
}
function showDropdown(html) {