mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-21 06:50:56 +00:00
3ebf27c0ed
- Auto-enable large font mode on mobile (base.html) - Add swipe gestures for chapter navigation - Improve verse number tap targets with background - Make family tree pages full width on mobile - Enhance homepage mobile layout with larger touch targets - Increase text sizes and line heights for readability - Remove sidenote tap target styling per user request 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
968 lines
28 KiB
HTML
968 lines
28 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Biblical Family Tree - KJV Study{% endblock %}
|
|
{% block description %}Explore biblical genealogies from Adam to Jesus Christ.{% endblock %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
{% set female_names = ['eve', 'sarah', 'rebekah', 'rachel', 'leah', 'ruth', 'mary', 'tamar', 'rahab', 'bathsheba', 'dinah', 'keturah', 'hagar', 'zilpah', 'bilhah', 'jochebed', 'miriam', 'deborah', 'hannah', 'abigail', 'esther', 'naomi', 'naamah', 'milcah', 'adah', 'zillah', 'asenath', 'basemath'] %}
|
|
|
|
.family-tree-page {
|
|
max-width: 55%;
|
|
}
|
|
|
|
.page-header {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.page-header h1 {
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.page-header .subtitle {
|
|
color: #666;
|
|
margin: 0;
|
|
}
|
|
|
|
.page-intro {
|
|
margin-bottom: 1.5rem;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
/* Search card */
|
|
.search-card {
|
|
padding: 1rem 1.25rem;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
margin-bottom: 1.5rem;
|
|
background: #fff;
|
|
max-width: 320px;
|
|
}
|
|
|
|
.search-card h2 {
|
|
font-size: 1rem;
|
|
margin: 0 0 0.75rem 0;
|
|
color: #333;
|
|
}
|
|
|
|
.search-form {
|
|
position: relative;
|
|
}
|
|
|
|
.search-form input {
|
|
width: 100%;
|
|
padding: 0.6rem 0.75rem;
|
|
font-size: 1rem;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
font-family: inherit;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.search-form input:focus {
|
|
outline: none;
|
|
border-color: #4a7c59;
|
|
box-shadow: 0 0 3px rgba(74, 124, 89, 0.3);
|
|
}
|
|
|
|
.search-dropdown {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: #ffffff;
|
|
border: 1px solid #ccc;
|
|
border-top: none;
|
|
border-radius: 0 0 4px 4px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
max-height: 280px;
|
|
overflow-y: auto;
|
|
display: none;
|
|
z-index: 10;
|
|
}
|
|
|
|
.search-dropdown.open {
|
|
display: block;
|
|
}
|
|
|
|
.search-dropdown button {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 0.5rem 0.75rem;
|
|
border: none;
|
|
background: transparent;
|
|
text-align: left;
|
|
font-size: 0.95rem;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.search-dropdown button:hover,
|
|
.search-dropdown button.active {
|
|
background: #f5f5f5;
|
|
}
|
|
|
|
/* Feature cards */
|
|
.feature-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.feature-card {
|
|
padding: 1rem 1.25rem;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
background: #fff;
|
|
transition: box-shadow 0.15s ease, border-color 0.15s ease;
|
|
}
|
|
|
|
.feature-card:hover {
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|
border-color: #ccc;
|
|
}
|
|
|
|
.feature-card h3 {
|
|
font-size: 1.1rem;
|
|
margin: 0 0 0.5rem 0;
|
|
}
|
|
|
|
.feature-card h3 a {
|
|
color: #4a7c59;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.feature-card h3 a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.feature-card p {
|
|
font-size: 0.9rem;
|
|
color: #666;
|
|
margin: 0;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Stats card */
|
|
.stats-card {
|
|
padding: 1rem 1.25rem;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
margin-bottom: 1.5rem;
|
|
background: #fff;
|
|
}
|
|
|
|
.stats-card h2 {
|
|
font-size: 1.1rem;
|
|
margin: 0 0 1rem 0;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid #eee;
|
|
color: #333;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 0.75rem 1.5rem;
|
|
}
|
|
|
|
.stat-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
padding: 0.35rem 0;
|
|
}
|
|
|
|
.stat-label {
|
|
color: #666;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.stat-value {
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.stat-value a {
|
|
color: #4a7c59;
|
|
}
|
|
|
|
/* Generation cards */
|
|
.section-card {
|
|
padding: 1rem 1.25rem;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
margin-bottom: 1rem;
|
|
background: #fff;
|
|
}
|
|
|
|
.section-card h2 {
|
|
font-size: 1.1rem;
|
|
margin: 0 0 0.75rem 0;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid #eee;
|
|
color: #333;
|
|
}
|
|
|
|
.generation-grid {
|
|
display: grid;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.gen-card {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.75rem;
|
|
padding: 0.6rem 0.75rem;
|
|
border: 1px solid #eee;
|
|
border-radius: 4px;
|
|
background: #fafafa;
|
|
transition: background 0.15s ease;
|
|
}
|
|
|
|
.gen-card:hover {
|
|
background: #f5f5f5;
|
|
}
|
|
|
|
.gen-card-num {
|
|
font-weight: 600;
|
|
min-width: 40px;
|
|
color: #4a7c59;
|
|
}
|
|
|
|
.gen-card-num a {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.gen-card-num a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.gen-card-count {
|
|
font-size: 0.85rem;
|
|
color: #888;
|
|
min-width: 90px;
|
|
}
|
|
|
|
.gen-card-people {
|
|
flex: 1;
|
|
font-size: 0.9rem;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.gen-card-people a {
|
|
color: #555;
|
|
}
|
|
|
|
/* Notable figures grid */
|
|
.notable-grid {
|
|
display: grid;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.person-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
padding: 0.75rem 1rem;
|
|
border: 1px solid #eee;
|
|
border-radius: 4px;
|
|
background: #fafafa;
|
|
transition: background 0.15s ease;
|
|
}
|
|
|
|
.person-card:hover {
|
|
background: #f5f5f5;
|
|
}
|
|
|
|
.person-card.male {
|
|
border-left: 3px solid #3b6ea5;
|
|
}
|
|
|
|
.person-card.female {
|
|
border-left: 3px solid #a55b80;
|
|
}
|
|
|
|
.person-card.kekule {
|
|
border-left-color: #d4af37;
|
|
background: linear-gradient(to right, #fffde7, #fafafa);
|
|
}
|
|
|
|
.person-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.person-card-name {
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.person-card-name a {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.person-card-name a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.person-card-badge {
|
|
font-size: 0.7rem;
|
|
padding: 0.1rem 0.4rem;
|
|
border-radius: 2px;
|
|
background: #d4af37;
|
|
color: #fff;
|
|
}
|
|
|
|
.person-card-meta {
|
|
font-size: 0.85rem;
|
|
color: #666;
|
|
}
|
|
|
|
.person-card-details {
|
|
font-size: 0.9rem;
|
|
color: #555;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.person-card-details a {
|
|
color: #4a7c59;
|
|
}
|
|
|
|
.person-card-verse {
|
|
font-size: 0.85rem;
|
|
color: #666;
|
|
font-style: italic;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.person-card-verse a {
|
|
color: #4a7c59;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.family-tree-page {
|
|
max-width: 100%;
|
|
}
|
|
.feature-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.stats-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
/* Dark mode */
|
|
[data-theme="dark"] .search-card,
|
|
[data-theme="dark"] .feature-card,
|
|
[data-theme="dark"] .stats-card,
|
|
[data-theme="dark"] .section-card {
|
|
background: #1a1a1a;
|
|
border-color: #333;
|
|
}
|
|
|
|
[data-theme="dark"] .search-card h2,
|
|
[data-theme="dark"] .stats-card h2,
|
|
[data-theme="dark"] .section-card h2 {
|
|
color: #e0e0e0;
|
|
border-bottom-color: #333;
|
|
}
|
|
|
|
[data-theme="dark"] .search-form input {
|
|
background: #2a2a2a;
|
|
border-color: #444;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
[data-theme="dark"] .search-form input:focus {
|
|
border-color: #4a7c59;
|
|
}
|
|
|
|
[data-theme="dark"] .search-dropdown {
|
|
background: #2a2a2a;
|
|
border-color: #444;
|
|
}
|
|
|
|
[data-theme="dark"] .search-dropdown button {
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
[data-theme="dark"] .search-dropdown button:hover,
|
|
[data-theme="dark"] .search-dropdown button.active {
|
|
background: #333;
|
|
}
|
|
|
|
[data-theme="dark"] .feature-card:hover {
|
|
border-color: #555;
|
|
}
|
|
|
|
[data-theme="dark"] .feature-card h3 a {
|
|
color: #6b9b7a;
|
|
}
|
|
|
|
[data-theme="dark"] .feature-card p {
|
|
color: #aaa;
|
|
}
|
|
|
|
[data-theme="dark"] .stat-label {
|
|
color: #aaa;
|
|
}
|
|
|
|
[data-theme="dark"] .stat-value {
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
[data-theme="dark"] .gen-card {
|
|
background: #252525;
|
|
border-color: #333;
|
|
}
|
|
|
|
[data-theme="dark"] .gen-card:hover {
|
|
background: #2a2a2a;
|
|
}
|
|
|
|
[data-theme="dark"] .gen-card-count {
|
|
color: #888;
|
|
}
|
|
|
|
[data-theme="dark"] .gen-card-people a {
|
|
color: #bbb;
|
|
}
|
|
|
|
[data-theme="dark"] .person-card {
|
|
background: #252525;
|
|
border-color: #333;
|
|
}
|
|
|
|
[data-theme="dark"] .person-card:hover {
|
|
background: #2a2a2a;
|
|
}
|
|
|
|
[data-theme="dark"] .person-card.kekule {
|
|
background: linear-gradient(to right, #2a2a1a, #252525);
|
|
}
|
|
|
|
[data-theme="dark"] .person-card-name {
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
[data-theme="dark"] .person-card-meta {
|
|
color: #888;
|
|
}
|
|
|
|
[data-theme="dark"] .person-card-details {
|
|
color: #bbb;
|
|
}
|
|
|
|
[data-theme="dark"] .person-card-verse {
|
|
color: #888;
|
|
}
|
|
|
|
[data-theme="dark"] .page-header .subtitle {
|
|
color: #888;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{% set female_names = ['eve', 'sarah', 'rebekah', 'rachel', 'leah', 'ruth', 'mary', 'tamar', 'rahab', 'bathsheba', 'dinah', 'keturah', 'hagar', 'zilpah', 'bilhah', 'jochebed', 'miriam', 'deborah', 'hannah', 'abigail', 'esther', 'naomi', 'naamah', 'milcah', 'adah', 'zillah', 'asenath', 'basemath'] %}
|
|
|
|
<div class="family-tree-page">
|
|
<div class="page-header">
|
|
<h1>Biblical Family Tree</h1>
|
|
<p class="subtitle">From Adam to Jesus Christ</p>
|
|
</div>
|
|
|
|
<p class="page-intro">
|
|
The Bible contains detailed genealogies that trace God's plan through specific family lines, culminating in the birth of Jesus Christ. This record spans from the creation of Adam through countless generations to the birth of our Lord.
|
|
</p>
|
|
|
|
<!-- Search -->
|
|
<div class="search-card">
|
|
<h2>Find a Person</h2>
|
|
<div class="search-form">
|
|
<form method="get" action="/family-tree/search" id="family-tree-search-form">
|
|
<input
|
|
type="text"
|
|
name="q"
|
|
placeholder="Search for a person..."
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
autocapitalize="off"
|
|
spellcheck="false"
|
|
data-form-type="other"
|
|
id="family-tree-search-input"
|
|
>
|
|
</form>
|
|
<div class="search-dropdown" id="family-tree-search-dropdown"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Feature Cards -->
|
|
<div class="feature-grid">
|
|
<div class="feature-card">
|
|
<h3><a href="/family-tree/interactive">Interactive Tree</a></h3>
|
|
<p>Explore the genealogy with a collapsible text-based tree. Expand and collapse branches, search for any person, and navigate with keyboard shortcuts.</p>
|
|
</div>
|
|
<div class="feature-card">
|
|
<h3><a href="/family-tree/lineage">Messianic Lineage</a></h3>
|
|
<p>View the direct paternal line from Adam to Jesus Christ in a vertical genealogy chart showing each generation.</p>
|
|
</div>
|
|
</div>
|
|
|
|
{% if family_tree_data and generations %}
|
|
<!-- Statistics -->
|
|
<div class="stats-card">
|
|
<h2>Overview</h2>
|
|
<div class="stats-grid">
|
|
<div class="stat-item">
|
|
<span class="stat-label">Total People:</span>
|
|
<span class="stat-value" id="stat-total-people">{{ family_tree_data|length }}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Total Generations:</span>
|
|
<span class="stat-value" id="stat-total-generations">{{ generations|length }}</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Longest Lived:</span>
|
|
<span class="stat-value" id="stat-longest-lived">Loading...</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Most Children:</span>
|
|
<span class="stat-value" id="stat-most-children">Loading...</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Most Siblings:</span>
|
|
<span class="stat-value" id="stat-most-siblings">Loading...</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-label">Close Family Marriages:</span>
|
|
<span class="stat-value" id="stat-close-marriages">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Generations -->
|
|
<div class="section-card">
|
|
<h2>The Generations</h2>
|
|
<div class="generation-grid">
|
|
{% for gen_num in generations.keys() | sort %}
|
|
<div class="gen-card">
|
|
<div class="gen-card-num">
|
|
<a href="/family-tree/generation/{{ gen_num }}">Gen {{ gen_num }}</a>
|
|
</div>
|
|
<div class="gen-card-count">{{ generations[gen_num]|length }} person{% if generations[gen_num]|length != 1 %}s{% endif %}</div>
|
|
<div class="gen-card-people">
|
|
{% for person_id in generations[gen_num][:8] %}
|
|
<a href="/family-tree/person/{{ person_id }}">{{ family_tree_data[person_id].name }}</a>{% if not loop.last %}, {% endif %}
|
|
{% endfor %}{% if generations[gen_num]|length > 8 %}, +{{ generations[gen_num]|length - 8 }} more{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notable Figures -->
|
|
<div class="section-card">
|
|
<h2>Notable Figures</h2>
|
|
<div class="notable-grid">
|
|
{% set notable = ["Adam", "Noah", "Abraham", "Isaac", "Jacob", "Joseph", "Moses", "David", "Solomon", "Jesus"] %}
|
|
{% for person_id, person in family_tree_data.items() %}
|
|
{% if person.name in notable %}
|
|
{% set is_female = person.name|lower in female_names or (female_names | select('in', person.name|lower) | list | length > 0) %}
|
|
<div class="person-card{% if person.kekule_number is not none %} kekule{% elif is_female %} female{% else %} male{% endif %}">
|
|
<div class="person-card-header">
|
|
<span class="person-card-name">
|
|
<a href="/family-tree/person/{{ person_id }}">{{ person.name }}</a>
|
|
</span>
|
|
{% if person.kekule_number is not none %}
|
|
<span class="person-card-badge">Kekulé #{{ person.kekule_number }}</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="person-card-meta">
|
|
{% if person.generation %}Generation {{ person.generation }}{% endif %}{% if person.birth_year != "Unknown" %} · Born {{ person.birth_year }}{% endif %}{% if person.age_at_death != "Unknown" %} · Lived {{ person.age_at_death }}{% endif %}
|
|
</div>
|
|
|
|
<div class="person-card-details">
|
|
{% if person.spouse %}
|
|
Married to {{ person.spouse }}.
|
|
{% endif %}
|
|
{% if person.parents|length > 0 %}
|
|
Child of
|
|
{% for parent_id in person.parents %}
|
|
{% if parent_id in family_tree_data %}<a href="/family-tree/person/{{ parent_id }}">{{ family_tree_data[parent_id].name }}</a>{% if not loop.last %} and {% endif %}{% endif %}
|
|
{% endfor %}.
|
|
{% endif %}
|
|
{% if person.children|length > 0 %}
|
|
Father of
|
|
{% for child_id in person.children[:3] %}
|
|
{% if child_id in family_tree_data %}<a href="/family-tree/person/{{ child_id }}">{{ family_tree_data[child_id].name }}</a>{% if not loop.last %}, {% endif %}{% endif %}
|
|
{% endfor %}{% if person.children|length > 3 %}, +{{ person.children|length - 3 }} more{% endif %}.
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if person.verses %}
|
|
<div class="person-card-verse">
|
|
{% for verse in person.verses[:1] %}
|
|
{% set ref_parts = verse.reference.split(' ') %}
|
|
{% if ref_parts|length >= 2 %}
|
|
{% set chapter_verse = ref_parts[-1] %}
|
|
{% if ':' in chapter_verse %}
|
|
{% set chapter = chapter_verse.split(':')[0] %}
|
|
{% set verse_part = chapter_verse.split(':')[1] %}
|
|
{% if '-' in verse_part %}
|
|
{% set verse_num = verse_part.split('-')[0] %}
|
|
{% else %}
|
|
{% set verse_num = verse_part %}
|
|
{% endif %}
|
|
{% set book = ' '.join(ref_parts[:-1]) %}
|
|
<a href="/book/{{ book }}/chapter/{{ chapter }}/verse/{{ verse_num }}">{{ verse.reference }}</a>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="section-card">
|
|
<p>Family tree data could not be loaded.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Fetch and display family tree statistics
|
|
fetch('/api/family-tree/stats')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Update the statistics
|
|
document.getElementById('stat-total-people').textContent = data.total_people;
|
|
document.getElementById('stat-total-generations').textContent = data.total_generations;
|
|
|
|
// Longest lived person
|
|
if (data.longest_lived && data.longest_lived.value > 0) {
|
|
const personName = data.longest_lived.name;
|
|
const personId = data.longest_lived.person_id;
|
|
document.getElementById('stat-longest-lived').innerHTML =
|
|
`<a href="/family-tree/person/${personId}">${personName}</a> (${data.longest_lived.value} yrs)`;
|
|
} else {
|
|
document.getElementById('stat-longest-lived').textContent = '—';
|
|
}
|
|
|
|
// Most children
|
|
if (data.most_children && data.most_children.value > 0) {
|
|
const personName = data.most_children.name;
|
|
const personId = data.most_children.person_id;
|
|
document.getElementById('stat-most-children').innerHTML =
|
|
`<a href="/family-tree/person/${personId}">${personName}</a> (${data.most_children.value})`;
|
|
} else {
|
|
document.getElementById('stat-most-children').textContent = '—';
|
|
}
|
|
|
|
// Most siblings
|
|
if (data.most_siblings && data.most_siblings.value > 0) {
|
|
const personName = data.most_siblings.name;
|
|
const personId = data.most_siblings.person_id;
|
|
document.getElementById('stat-most-siblings').innerHTML =
|
|
`<a href="/family-tree/person/${personId}">${personName}</a> (${data.most_siblings.value})`;
|
|
} else {
|
|
document.getElementById('stat-most-siblings').textContent = '—';
|
|
}
|
|
|
|
// Close family marriages
|
|
if (data.close_family_marriages !== undefined) {
|
|
document.getElementById('stat-close-marriages').textContent = data.close_family_marriages;
|
|
} else {
|
|
document.getElementById('stat-close-marriages').textContent = '—';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading family tree statistics:', error);
|
|
document.getElementById('stat-longest-lived').textContent = '—';
|
|
document.getElementById('stat-most-children').textContent = '—';
|
|
document.getElementById('stat-most-siblings').textContent = '—';
|
|
document.getElementById('stat-close-marriages').textContent = '—';
|
|
});
|
|
|
|
// Search functionality
|
|
const names = {{ person_names|default([])|tojson }};
|
|
const input = document.getElementById('family-tree-search-input');
|
|
const dropdown = document.getElementById('family-tree-search-dropdown');
|
|
const form = document.getElementById('family-tree-search-form');
|
|
|
|
if (!input || !dropdown || !form || names.length === 0) {
|
|
return;
|
|
}
|
|
|
|
let filtered = [];
|
|
let activeIndex = -1;
|
|
|
|
function hideDropdown() {
|
|
dropdown.classList.remove('open');
|
|
dropdown.innerHTML = '';
|
|
filtered = [];
|
|
activeIndex = -1;
|
|
}
|
|
|
|
function renderDropdown(items) {
|
|
if (!items.length) {
|
|
hideDropdown();
|
|
return;
|
|
}
|
|
|
|
dropdown.innerHTML = items
|
|
.map((name, idx) => `<button type="button" data-index="${idx}">${name}</button>`)
|
|
.join('');
|
|
dropdown.classList.add('open');
|
|
activeIndex = -1;
|
|
}
|
|
|
|
function setActive(index) {
|
|
const buttons = dropdown.querySelectorAll('button');
|
|
buttons.forEach(btn => btn.classList.remove('active'));
|
|
if (buttons[index]) {
|
|
buttons[index].classList.add('active');
|
|
}
|
|
}
|
|
|
|
function chooseName(name) {
|
|
input.value = name;
|
|
hideDropdown();
|
|
form.submit();
|
|
}
|
|
|
|
input.addEventListener('input', function() {
|
|
const query = input.value.trim().toLowerCase();
|
|
if (!query) {
|
|
hideDropdown();
|
|
return;
|
|
}
|
|
filtered = names.filter(name => name.toLowerCase().includes(query)).slice(0, 8);
|
|
renderDropdown(filtered);
|
|
});
|
|
|
|
input.addEventListener('keydown', function(event) {
|
|
if (!dropdown.classList.contains('open')) {
|
|
return;
|
|
}
|
|
if (event.key === 'ArrowDown') {
|
|
event.preventDefault();
|
|
activeIndex = (activeIndex + 1) % filtered.length;
|
|
setActive(activeIndex);
|
|
} else if (event.key === 'ArrowUp') {
|
|
event.preventDefault();
|
|
activeIndex = (activeIndex - 1 + filtered.length) % filtered.length;
|
|
setActive(activeIndex);
|
|
} else if (event.key === 'Enter') {
|
|
if (activeIndex >= 0 && filtered[activeIndex]) {
|
|
event.preventDefault();
|
|
chooseName(filtered[activeIndex]);
|
|
}
|
|
} else if (event.key === 'Escape') {
|
|
hideDropdown();
|
|
}
|
|
});
|
|
|
|
dropdown.addEventListener('mousedown', function(event) {
|
|
const button = event.target.closest('button');
|
|
if (!button) {
|
|
return;
|
|
}
|
|
const index = Number(button.dataset.index);
|
|
if (!Number.isNaN(index) && filtered[index]) {
|
|
chooseName(filtered[index]);
|
|
}
|
|
});
|
|
|
|
document.addEventListener('click', function(event) {
|
|
if (!dropdown.contains(event.target) && event.target !== input) {
|
|
hideDropdown();
|
|
}
|
|
});
|
|
|
|
// Keyboard navigation for page elements
|
|
const featureCards = Array.from(document.querySelectorAll('.feature-card'));
|
|
const genCards = Array.from(document.querySelectorAll('.gen-card'));
|
|
const personCards = Array.from(document.querySelectorAll('.person-card'));
|
|
|
|
// Combine all navigable items
|
|
const allItems = featureCards.concat(genCards).concat(personCards);
|
|
if (allItems.length === 0) return;
|
|
|
|
let selectedIndex = -1;
|
|
|
|
// Define section boundaries
|
|
const featureEnd = featureCards.length;
|
|
const genEnd = featureEnd + genCards.length;
|
|
|
|
function getLink(item) {
|
|
const link = item.querySelector('a');
|
|
return link ? link.href : null;
|
|
}
|
|
|
|
function selectItem(index) {
|
|
// Remove previous selection
|
|
if (selectedIndex >= 0 && selectedIndex < allItems.length) {
|
|
allItems[selectedIndex].style.outline = '';
|
|
allItems[selectedIndex].style.outlineOffset = '';
|
|
allItems[selectedIndex].classList.remove('selected');
|
|
}
|
|
|
|
// Bounds check
|
|
selectedIndex = Math.max(0, Math.min(index, allItems.length - 1));
|
|
|
|
// Add selection
|
|
allItems[selectedIndex].style.outline = '2px solid #4a7c59';
|
|
allItems[selectedIndex].style.outlineOffset = '2px';
|
|
allItems[selectedIndex].classList.add('selected');
|
|
|
|
// Scroll into view
|
|
allItems[selectedIndex].scrollIntoView({
|
|
behavior: 'auto',
|
|
block: 'center'
|
|
});
|
|
}
|
|
|
|
function getGridColumns() {
|
|
// Feature cards are 2 columns, others are 1 column
|
|
if (selectedIndex < featureEnd) {
|
|
return window.innerWidth <= 768 ? 1 : 2;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
function moveUp() {
|
|
const cols = getGridColumns();
|
|
if (selectedIndex < featureEnd) {
|
|
// In feature section - move up by cols, or jump to before section
|
|
const newIndex = selectedIndex - cols;
|
|
if (newIndex >= 0) {
|
|
selectItem(newIndex);
|
|
}
|
|
} else if (selectedIndex < genEnd) {
|
|
// In generation section - up moves to previous gen or last feature
|
|
if (selectedIndex === featureEnd) {
|
|
selectItem(featureEnd - 1);
|
|
} else {
|
|
selectItem(selectedIndex - 1);
|
|
}
|
|
} else {
|
|
// In person section - up moves to previous person or last gen
|
|
if (selectedIndex === genEnd) {
|
|
selectItem(genEnd - 1);
|
|
} else {
|
|
selectItem(selectedIndex - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
function moveDown() {
|
|
const cols = getGridColumns();
|
|
if (selectedIndex < featureEnd) {
|
|
// In feature section - move down by cols or jump to generations
|
|
const newIndex = selectedIndex + cols;
|
|
if (newIndex < featureEnd) {
|
|
selectItem(newIndex);
|
|
} else {
|
|
selectItem(featureEnd);
|
|
}
|
|
} else if (selectedIndex < genEnd) {
|
|
// In generation section - down moves to next gen or first person
|
|
if (selectedIndex === genEnd - 1) {
|
|
selectItem(genEnd);
|
|
} else {
|
|
selectItem(selectedIndex + 1);
|
|
}
|
|
} else {
|
|
// In person section - down moves to next person
|
|
selectItem(selectedIndex + 1);
|
|
}
|
|
}
|
|
|
|
function moveLeft() {
|
|
if (selectedIndex < featureEnd) {
|
|
// In feature section - move left within row
|
|
if (selectedIndex > 0) {
|
|
selectItem(selectedIndex - 1);
|
|
} else {
|
|
history.back();
|
|
}
|
|
} else {
|
|
// Other sections have no left movement, go back
|
|
history.back();
|
|
}
|
|
}
|
|
|
|
function moveRight() {
|
|
if (selectedIndex < featureEnd) {
|
|
// In feature section - move right within row
|
|
const cols = getGridColumns();
|
|
const row = Math.floor(selectedIndex / cols);
|
|
const nextInRow = (row * cols) + ((selectedIndex % cols) + 1);
|
|
if (nextInRow < featureEnd && nextInRow < (row + 1) * cols) {
|
|
selectItem(nextInRow);
|
|
}
|
|
}
|
|
// Other sections have no right movement
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
// Don't handle if user is typing in an input
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
if (KJVNav.sidebarActive) return;
|
|
|
|
if (e.key === 'ArrowDown' || e.key === 'j') {
|
|
e.preventDefault();
|
|
if (selectedIndex < 0) {
|
|
selectItem(0);
|
|
} else {
|
|
moveDown();
|
|
}
|
|
} else if (e.key === 'ArrowUp' || e.key === 'k') {
|
|
e.preventDefault();
|
|
if (selectedIndex < 0) {
|
|
selectItem(0);
|
|
} else {
|
|
moveUp();
|
|
}
|
|
} else if (e.key === 'ArrowLeft' || e.key === 'h') {
|
|
e.preventDefault();
|
|
if (selectedIndex < 0) {
|
|
history.back();
|
|
} else {
|
|
moveLeft();
|
|
}
|
|
} else if (e.key === 'ArrowRight' || e.key === 'l') {
|
|
e.preventDefault();
|
|
if (selectedIndex >= 0) {
|
|
moveRight();
|
|
}
|
|
} else if (e.key === 'Enter' && selectedIndex >= 0) {
|
|
e.preventDefault();
|
|
const href = getLink(allItems[selectedIndex]);
|
|
if (href) window.location.href = href;
|
|
} else if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
if (selectedIndex >= 0 && selectedIndex < allItems.length) {
|
|
allItems[selectedIndex].style.outline = '';
|
|
allItems[selectedIndex].style.outlineOffset = '';
|
|
allItems[selectedIndex].classList.remove('selected');
|
|
}
|
|
selectedIndex = -1;
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|