Files
christit.com/librebible/index.html
T

529 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LibreBible Catalog | Christ IT</title>
<meta name="description" content="Public catalog of free Bible translations, commentaries, dictionaries, and study resources prepared for Libre Study and other Christ IT tools.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='12' fill='%23111827'/%3E%3Crect x='29' y='10' width='8' height='44' rx='3' fill='%23f8fafc'/%3E%3Crect x='16' y='25' width='34' height='8' rx='3' fill='%23f97316'/%3E%3C/svg%3E" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Oswald:wght@500;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--bg: #fbfaf7;
--surface: #ffffff;
--surface-soft: #eef5f2;
--ink: #151d2f;
--body: #526071;
--muted: #7b8794;
--line: #dce4e7;
--blue: #1265b3;
--green: #1e7a5f;
--orange: #d66b1f;
--red: #a43e3e;
--shadow: 0 14px 38px rgba(21, 29, 47, 0.10);
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--body);
font-family: "Inter", Arial, sans-serif;
line-height: 1.55;
}
a { color: inherit; text-decoration: none; }
.container { width: min(1120px, calc(100% - 40px)); margin: 0 auto; }
nav {
position: sticky;
top: 0;
z-index: 20;
background: rgba(251, 250, 247, 0.94);
border-bottom: 1px solid var(--line);
backdrop-filter: blur(14px);
}
.nav-inner {
min-height: 74px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.brand-logo {
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--ink);
font-family: "Oswald", Impact, sans-serif;
font-size: 2rem;
font-weight: 700;
line-height: 1;
text-transform: uppercase;
}
.brand-cross { position: relative; width: 26px; height: 32px; flex: 0 0 auto; }
.brand-cross::before,
.brand-cross::after {
content: "";
position: absolute;
border-radius: 4px;
background: var(--blue);
}
.brand-cross::before { width: 10px; height: 32px; left: 8px; top: 0; }
.brand-cross::after { width: 26px; height: 10px; left: 0; top: 10px; }
.nav-links { display: flex; align-items: center; gap: 18px; font-size: 0.94rem; font-weight: 700; color: var(--body); }
.nav-links a:hover { color: var(--blue); }
.hero {
padding: 62px 0 42px;
border-bottom: 1px solid var(--line);
background:
linear-gradient(135deg, rgba(18, 101, 179, 0.08), transparent 36%),
linear-gradient(315deg, rgba(30, 122, 95, 0.11), transparent 34%);
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 9px;
color: var(--green);
font-size: 0.82rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1, h2, h3 {
margin: 0;
color: var(--ink);
font-family: "Oswald", Impact, sans-serif;
line-height: 1.05;
text-transform: uppercase;
}
h1 {
max-width: 860px;
margin-top: 18px;
font-size: clamp(3rem, 7vw, 5.8rem);
}
.hero-copy {
max-width: 760px;
margin: 22px 0 0;
padding-left: 22px;
border-left: 5px solid var(--orange);
font-size: clamp(1.04rem, 2vw, 1.22rem);
}
.button-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; }
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 46px;
padding: 12px 18px;
border: 2px solid transparent;
border-radius: 6px;
background: var(--blue);
color: #fff;
font-weight: 800;
line-height: 1.1;
}
.button.secondary {
background: transparent;
color: var(--ink);
border-color: var(--line);
}
.section { padding: 54px 0; }
.summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
margin-bottom: 28px;
}
.stat, .resource-card, .notice {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
}
.stat { padding: 18px; }
.stat span {
display: block;
color: var(--muted);
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.07em;
text-transform: uppercase;
}
.stat strong {
display: block;
margin-top: 5px;
color: var(--ink);
font-size: 1.55rem;
line-height: 1.1;
}
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 14px;
margin: 0 0 18px;
}
.toolbar h2 { font-size: clamp(2rem, 4.5vw, 3.2rem); }
.status { color: var(--muted); font-weight: 700; }
.resource-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
gap: 18px;
}
.resource-card {
display: flex;
flex-direction: column;
gap: 18px;
padding: 22px;
}
.resource-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.resource-card h3 { font-size: 1.55rem; }
.resource-id {
color: var(--muted);
font-family: Consolas, "Liberation Mono", monospace;
font-size: 0.84rem;
overflow-wrap: anywhere;
}
.badge-row, .feature-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.badge, .feature {
display: inline-flex;
align-items: center;
gap: 7px;
min-height: 30px;
padding: 5px 9px;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--surface-soft);
color: var(--ink);
font-size: 0.82rem;
font-weight: 800;
}
.feature { border-radius: 6px; background: #fff; }
.detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.detail {
border-top: 1px solid var(--line);
padding-top: 12px;
}
.detail span {
display: block;
color: var(--muted);
font-size: 0.77rem;
font-weight: 800;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.detail strong { color: var(--ink); font-size: 0.96rem; }
.notice {
padding: 18px;
color: var(--body);
}
.notice strong { color: var(--ink); }
footer {
padding: 30px 0;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 0.92rem;
}
@media (max-width: 760px) {
.nav-links { display: none; }
.summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.detail-grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.container { width: min(100% - 28px, 1120px); }
.summary-grid { grid-template-columns: 1fr; }
.resource-head { flex-direction: column; }
}
</style>
</head>
<body>
<nav>
<div class="container nav-inner">
<a href="../index.html" class="brand-logo" aria-label="Christ IT home">
<span class="brand-cross" aria-hidden="true"></span>
<span>Christ IT</span>
</a>
<div class="nav-links" aria-label="Primary navigation">
<a href="../index.html#projects">Projects</a>
<a href="https://git.christit.com/libre-study/libre-bible-data">LibreBible Git</a>
<a href="https://git.christit.com/libre-study/libre-study">Libre Study</a>
</div>
</div>
</nav>
<header class="hero">
<div class="container">
<span class="eyebrow"><i class="fa-solid fa-book-bible"></i> Public Bible resource catalog</span>
<h1>LibreBible</h1>
<p class="hero-copy">A growing catalog of free-to-use Bible translations, commentaries, dictionaries, and attached study resources prepared for Libre Study, GracePress, and future Christ IT tools.</p>
<div class="button-row">
<a class="button" href="https://git.christit.com/libre-study/libre-bible-data"><i class="fa-solid fa-code-branch"></i> Source Repository</a>
<a class="button secondary" href="catalog/catalog.json"><i class="fa-solid fa-database"></i> Catalog JSON</a>
</div>
</div>
</header>
<main class="section">
<div class="container">
<div class="summary-grid" id="summary" aria-live="polite">
<div class="stat"><span>Resources</span><strong id="resource-count">-</strong></div>
<div class="stat"><span>Study Helps</span><strong id="study-help-count">-</strong></div>
<div class="stat"><span>Verses</span><strong id="verse-count">-</strong></div>
<div class="stat"><span>Study Entries</span><strong id="entry-count">-</strong></div>
</div>
<div class="toolbar">
<h2>Available Resources</h2>
<div class="status" id="catalog-status">Loading catalog...</div>
</div>
<div class="resource-list" id="resource-list"></div>
<noscript>
<div class="notice"><strong>JavaScript is required.</strong> This catalog page reads generated JSON metadata and renders the resource list in the browser.</div>
</noscript>
</div>
</main>
<footer>
<div class="container">All Glory to Jesus Christ. LibreBible catalog metadata is generated from the public LibreBible data repository.</div>
</footer>
<script>
const catalogUrl = "catalog/catalog.json";
const formatter = new Intl.NumberFormat("en-US");
function text(value, fallback = "Unknown") {
return value === undefined || value === null || value === "" ? fallback : String(value);
}
function number(value) {
return Number.isFinite(value) ? formatter.format(value) : "-";
}
function makeBadge(icon, label) {
const badge = document.createElement("span");
badge.className = "badge";
badge.innerHTML = `<i class="${icon}" aria-hidden="true"></i><span></span>`;
badge.querySelector("span").textContent = label;
return badge;
}
function makeFeature(label) {
const feature = document.createElement("span");
feature.className = "feature";
feature.textContent = label;
return feature;
}
function makeDetail(label, value) {
const detail = document.createElement("div");
detail.className = "detail";
const caption = document.createElement("span");
caption.textContent = label;
const body = document.createElement("strong");
body.textContent = value;
detail.append(caption, body);
return detail;
}
function renderResource(resource) {
const card = document.createElement("article");
card.className = "resource-card";
const head = document.createElement("div");
head.className = "resource-head";
const titleBlock = document.createElement("div");
const title = document.createElement("h3");
title.textContent = text(resource.title, resource.id);
const id = document.createElement("div");
id.className = "resource-id";
id.textContent = resource.id;
titleBlock.append(title, id);
const repoLink = document.createElement("a");
repoLink.className = "button secondary";
const detailPath = resource.detail_doc_path || resource.package_path || "";
repoLink.href = "https://git.christit.com/libre-study/libre-bible-data/src/branch/main/" +
detailPath.split("/").map(encodeURIComponent).join("/");
repoLink.textContent = "Details";
head.append(titleBlock, repoLink);
const summary = document.createElement("p");
summary.textContent = text(resource.catalog_display && resource.catalog_display.summary, resource.translation && resource.translation.public_description);
const badges = document.createElement("div");
badges.className = "badge-row";
badges.append(
makeBadge(iconForType(resource.resource_type), labelForType(resource.resource_type)),
makeBadge("fa-solid fa-language", `${text(resource.language && resource.language.name)}${resource.language && resource.language.dialect ? " / " + resource.language.dialect : ""}`),
makeBadge("fa-solid fa-scale-balanced", text(resource.license && resource.license.name))
);
if (resource.canon && resource.canon.scope && resource.resource_type === "translation") {
badges.append(makeBadge("fa-solid fa-book-open", text(resource.canon.scope)));
}
const features = document.createElement("div");
features.className = "feature-row";
const featureLabels = (resource.catalog_display && resource.catalog_display.primary_features) || (resource.features || []).map((feature) => feature.label);
featureLabels.slice(0, 8).forEach((label) => features.append(makeFeature(label)));
const details = document.createElement("div");
details.className = "detail-grid";
details.append(...detailsForResource(resource));
card.append(head, summary, badges, features, details);
return card;
}
function renderCatalog(catalog) {
const resources = Array.isArray(catalog.resources) ? catalog.resources : [];
const studyHelps = resources.filter((resource) => resource.resource_type !== "translation");
const verseTotal = resources.reduce((sum, resource) => sum + (resource.counts && Number(resource.counts.verses) || 0), 0);
const entryTotal = resources.reduce((sum, resource) => {
const counts = resource.counts || {};
return sum + (Number(counts.entries) || 0) + (Number(counts.strongs_links) || 0);
}, 0);
document.getElementById("resource-count").textContent = number(resources.length);
document.getElementById("study-help-count").textContent = number(studyHelps.length);
document.getElementById("verse-count").textContent = number(verseTotal);
document.getElementById("entry-count").textContent = number(entryTotal);
document.getElementById("catalog-status").textContent = `Generated ${text(catalog.generated_at)}`;
const list = document.getElementById("resource-list");
list.replaceChildren(...resources.map(renderResource));
if (!resources.length) {
const empty = document.createElement("div");
empty.className = "notice";
empty.innerHTML = "<strong>No resources found.</strong> The catalog loaded, but did not include any resource entries.";
list.append(empty);
}
}
function labelForType(type) {
const labels = {
translation: "Translation",
commentary: "Commentary",
dictionary: "Dictionary",
lexicon: "Lexicon",
map: "Map",
cross_reference: "Cross references",
study_notes: "Study notes"
};
return labels[type] || text(type, "Resource");
}
function iconForType(type) {
const icons = {
translation: "fa-solid fa-book-bible",
commentary: "fa-solid fa-book-open-reader",
dictionary: "fa-solid fa-spell-check",
lexicon: "fa-solid fa-language",
map: "fa-solid fa-map-location-dot",
cross_reference: "fa-solid fa-arrows-turn-to-dots",
study_notes: "fa-solid fa-note-sticky"
};
return icons[type] || "fa-solid fa-cube";
}
function detailsForResource(resource) {
if (resource.resource_type === "translation") {
return [
makeDetail("Translation", `${text(resource.translation && resource.translation.base_title)} (${text(resource.translation && resource.translation.translation_year)})`),
makeDetail("Edition", `${text(resource.translation && resource.translation.edition)}${resource.translation && resource.translation.edition_year ? " / " + resource.translation.edition_year : ""}`),
makeDetail("Source", `${text(resource.source && resource.source.provider)} / ${text(resource.source && resource.source.upstream_id)}`),
makeDetail("Counts", `${number(resource.counts && resource.counts.verses)} verses, ${number(resource.counts && resource.counts.strongs_links)} Strong's links`)
];
}
if (resource.resource_type === "commentary") {
return [
makeDetail("Resource Type", "Commentary"),
makeDetail("Entries", `${number(resource.counts && (resource.counts.commentary_entries || resource.counts.entries))} commentary entries`),
makeDetail("Source", `${text(resource.source && resource.source.provider)} / ${text(resource.source && resource.source.upstream_id)}`),
makeDetail("Anchors", "Book, chapter, verse, and verse range")
];
}
if (resource.resource_type === "dictionary" || resource.resource_type === "lexicon") {
return [
makeDetail("Resource Type", labelForType(resource.resource_type)),
makeDetail("Entries", `${number(resource.counts && (resource.counts.dictionary_entries || resource.counts.entries))} entries`),
makeDetail("Source", `${text(resource.source && resource.source.provider)} / ${text(resource.source && resource.source.upstream_id)}`),
makeDetail("Anchors", "Dictionary key, Strong's number, topic, or lemma")
];
}
return [
makeDetail("Resource Type", labelForType(resource.resource_type)),
makeDetail("Entries", `${number(resource.counts && resource.counts.entries)} entries`),
makeDetail("Source", `${text(resource.source && resource.source.provider)} / ${text(resource.source && resource.source.upstream_id)}`),
makeDetail("License", text(resource.license && resource.license.name))
];
}
fetch(catalogUrl, { cache: "no-cache" })
.then((response) => {
if (!response.ok) {
throw new Error(`Catalog request failed: ${response.status}`);
}
return response.json();
})
.then(renderCatalog)
.catch((error) => {
document.getElementById("catalog-status").textContent = "Catalog unavailable";
const list = document.getElementById("resource-list");
const notice = document.createElement("div");
notice.className = "notice";
notice.innerHTML = `<strong>Catalog could not be loaded.</strong> ${error.message}`;
list.replaceChildren(notice);
});
</script>
</body>
</html>