Wire Strong's dictionary concordance UI
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.2 - 2026-07-12
|
||||||
|
|
||||||
|
- Wired Libre Study to import the LibreBible Strong's dictionary package alongside KJV Strong's links.
|
||||||
|
- Added linked-word selection, focused Strong's definition display, and one-click search for matching Strong's-number verses.
|
||||||
|
- Verified native SQLite import with 31,102 verses, 348,884 KJV Strong's links, and 14,197 Strong's dictionary entries.
|
||||||
|
|
||||||
## 0.1.1 - 2026-07-12
|
## 0.1.1 - 2026-07-12
|
||||||
|
|
||||||
- Phase 4 Libre Study consumption: the app can discover the KJV package from a LibreBible `packages/json/catalog.json` catalog, import files declared by the resource catalog, and retain a local record of the imported LibreBible resource.
|
- Phase 4 Libre Study consumption: the app can discover the KJV package from a LibreBible `packages/json/catalog.json` catalog, import files declared by the resource catalog, and retain a local record of the imported LibreBible resource.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.11.1",
|
"@tauri-apps/api": "^2.11.1",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "libre-study",
|
"name": "libre-study",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 127.0.0.1",
|
"dev": "vite --host 127.0.0.1",
|
||||||
|
|||||||
Generated
+1
-1
@@ -77,7 +77,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
+208
-13
@@ -49,6 +49,8 @@ struct Verse {
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct StrongLink {
|
struct StrongLink {
|
||||||
strong: String,
|
strong: String,
|
||||||
|
surface: String,
|
||||||
|
position: i64,
|
||||||
lemma: String,
|
lemma: String,
|
||||||
transliteration: String,
|
transliteration: String,
|
||||||
definition: String,
|
definition: String,
|
||||||
@@ -104,6 +106,7 @@ struct LibreBibleResourceCatalog {
|
|||||||
struct LibreBiblePackageFiles {
|
struct LibreBiblePackageFiles {
|
||||||
verses_jsonl: Option<LibreBiblePackageFile>,
|
verses_jsonl: Option<LibreBiblePackageFile>,
|
||||||
strongs_jsonl: Option<LibreBiblePackageFile>,
|
strongs_jsonl: Option<LibreBiblePackageFile>,
|
||||||
|
entries_jsonl: Option<LibreBiblePackageFile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -117,6 +120,18 @@ struct LibreBiblePackageSource {
|
|||||||
package_dir: PathBuf,
|
package_dir: PathBuf,
|
||||||
verses_path: PathBuf,
|
verses_path: PathBuf,
|
||||||
strongs_path: Option<PathBuf>,
|
strongs_path: Option<PathBuf>,
|
||||||
|
lexicon_path: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ImportedStrongEntry {
|
||||||
|
strongs_number: String,
|
||||||
|
language: String,
|
||||||
|
lemma: Option<String>,
|
||||||
|
transliteration: Option<String>,
|
||||||
|
pronunciation: Option<String>,
|
||||||
|
strongs_definition: Option<String>,
|
||||||
|
kjv_definition: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -251,7 +266,17 @@ fn migrate(conn: &Connection) -> Result<(), String> {
|
|||||||
);
|
);
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.map_err(|err| err.to_string())
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
add_column_if_missing(conn, "verse_strongs", "surface", "TEXT NOT NULL DEFAULT ''")?;
|
||||||
|
add_column_if_missing(conn, "lexicon", "pronunciation", "TEXT NOT NULL DEFAULT ''")?;
|
||||||
|
add_column_if_missing(
|
||||||
|
conn,
|
||||||
|
"lexicon",
|
||||||
|
"kjv_definition",
|
||||||
|
"TEXT NOT NULL DEFAULT ''",
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn seed(conn: &mut Connection) -> Result<(), String> {
|
fn seed(conn: &mut Connection) -> Result<(), String> {
|
||||||
@@ -263,7 +288,7 @@ fn seed(conn: &mut Connection) -> Result<(), String> {
|
|||||||
let source_count: i64 = conn
|
let source_count: i64 = conn
|
||||||
.query_row("SELECT COUNT(*) FROM library_sources", [], |row| row.get(0))
|
.query_row("SELECT COUNT(*) FROM library_sources", [], |row| row.get(0))
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
if source_count == 0 {
|
if source_count == 0 || needs_resource_refresh(conn)? {
|
||||||
if let Some(package_source) = find_default_package_source() {
|
if let Some(package_source) = find_default_package_source() {
|
||||||
reset_resource_cache(conn)?;
|
reset_resource_cache(conn)?;
|
||||||
import_package(conn, package_source)?;
|
import_package(conn, package_source)?;
|
||||||
@@ -397,12 +422,14 @@ fn load_package_source_from_catalog(
|
|||||||
|
|
||||||
if let Some(package_path) = &resource.package_path {
|
if let Some(package_path) = &resource.package_path {
|
||||||
let resource_catalog_path = resolve_catalog_relative(catalog_dir, package_path);
|
let resource_catalog_path = resolve_catalog_relative(catalog_dir, package_path);
|
||||||
return load_package_source_from_resource_catalog(resource_catalog_path);
|
let mut source = load_package_source_from_resource_catalog(resource_catalog_path)?;
|
||||||
|
source.lexicon_path = find_catalog_lexicon_path(catalog_dir, &index);
|
||||||
|
return Ok(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(files) = &resource.files {
|
if let Some(files) = &resource.files {
|
||||||
let package_dir = catalog_dir.join(&resource.id);
|
let package_dir = catalog_dir.join(&resource.id);
|
||||||
return package_source_from_files(
|
let mut source = package_source_from_files(
|
||||||
resource.id.clone(),
|
resource.id.clone(),
|
||||||
resource
|
resource
|
||||||
.title
|
.title
|
||||||
@@ -410,7 +437,9 @@ fn load_package_source_from_catalog(
|
|||||||
.unwrap_or_else(|| resource.id.clone()),
|
.unwrap_or_else(|| resource.id.clone()),
|
||||||
package_dir,
|
package_dir,
|
||||||
files,
|
files,
|
||||||
);
|
)?;
|
||||||
|
source.lexicon_path = find_catalog_lexicon_path(catalog_dir, &index);
|
||||||
|
return Ok(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,6 +497,7 @@ fn load_package_source_from_package_dir(
|
|||||||
strongs_jsonl: Some(LibreBiblePackageFile {
|
strongs_jsonl: Some(LibreBiblePackageFile {
|
||||||
path: "strongs-links.jsonl".to_string(),
|
path: "strongs-links.jsonl".to_string(),
|
||||||
}),
|
}),
|
||||||
|
entries_jsonl: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
package_source_from_files(
|
package_source_from_files(
|
||||||
@@ -504,6 +534,11 @@ fn package_source_from_files(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|file| package_dir.join(&file.path))
|
.map(|file| package_dir.join(&file.path))
|
||||||
.filter(|path| path.exists());
|
.filter(|path| path.exists());
|
||||||
|
let lexicon_path = files
|
||||||
|
.entries_jsonl
|
||||||
|
.as_ref()
|
||||||
|
.map(|file| package_dir.join(&file.path))
|
||||||
|
.filter(|path| path.exists());
|
||||||
|
|
||||||
Ok(LibreBiblePackageSource {
|
Ok(LibreBiblePackageSource {
|
||||||
resource_id,
|
resource_id,
|
||||||
@@ -511,6 +546,7 @@ fn package_source_from_files(
|
|||||||
package_dir,
|
package_dir,
|
||||||
verses_path,
|
verses_path,
|
||||||
strongs_path,
|
strongs_path,
|
||||||
|
lexicon_path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,6 +611,47 @@ fn import_package(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(lexicon_path) = package_source.lexicon_path {
|
||||||
|
let file = File::open(&lexicon_path)
|
||||||
|
.map_err(|err| format!("Unable to open {}: {err}", lexicon_path.display()))?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
|
for line in reader.lines() {
|
||||||
|
let line = line.map_err(|err| err.to_string())?;
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let imported: ImportedStrongEntry =
|
||||||
|
serde_json::from_str(&line).map_err(|err| err.to_string())?;
|
||||||
|
let language = match imported.language.as_str() {
|
||||||
|
"hebrew" => "Hebrew",
|
||||||
|
"greek" => "Greek",
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
let definition = imported
|
||||||
|
.strongs_definition
|
||||||
|
.clone()
|
||||||
|
.or(imported.kjv_definition.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
tx.execute(
|
||||||
|
"INSERT OR REPLACE INTO lexicon
|
||||||
|
(strong, lemma, transliteration, pronunciation, definition, kjv_definition, language)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||||
|
params![
|
||||||
|
imported.strongs_number,
|
||||||
|
imported.lemma.unwrap_or_default(),
|
||||||
|
imported.transliteration.unwrap_or_default(),
|
||||||
|
imported.pronunciation.unwrap_or_default(),
|
||||||
|
definition,
|
||||||
|
imported.kjv_definition.unwrap_or_default(),
|
||||||
|
language
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(strongs_path) = package_source.strongs_path {
|
if let Some(strongs_path) = package_source.strongs_path {
|
||||||
let file = File::open(&strongs_path)
|
let file = File::open(&strongs_path)
|
||||||
.map_err(|err| format!("Unable to open {}: {err}", strongs_path.display()))?;
|
.map_err(|err| format!("Unable to open {}: {err}", strongs_path.display()))?;
|
||||||
@@ -597,14 +674,21 @@ fn import_package(
|
|||||||
};
|
};
|
||||||
|
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"INSERT OR IGNORE INTO lexicon (strong, lemma, transliteration, definition, language)
|
"INSERT OR IGNORE INTO lexicon
|
||||||
|
(strong, lemma, transliteration, definition, language)
|
||||||
VALUES (?1, ?2, '', 'Linked Strong''s number from the source USFM package.', ?3)",
|
VALUES (?1, ?2, '', 'Linked Strong''s number from the source USFM package.', ?3)",
|
||||||
params![imported.strong, imported.surface, language],
|
params![imported.strong, imported.surface, language],
|
||||||
)
|
)
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"INSERT OR IGNORE INTO verse_strongs (verse_id, strong, position) VALUES (?1, ?2, ?3)",
|
"INSERT OR IGNORE INTO verse_strongs (verse_id, strong, position, surface)
|
||||||
params![verse_id, imported.strong, imported.position],
|
VALUES (?1, ?2, ?3, ?4)",
|
||||||
|
params![
|
||||||
|
verse_id,
|
||||||
|
imported.strong,
|
||||||
|
imported.position,
|
||||||
|
imported.surface
|
||||||
|
],
|
||||||
)
|
)
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
}
|
}
|
||||||
@@ -735,7 +819,7 @@ fn load_strongs(conn: &Connection, verse_id: i64) -> Result<Vec<StrongLink>, Str
|
|||||||
let mut stmt = conn
|
let mut stmt = conn
|
||||||
.prepare(
|
.prepare(
|
||||||
"
|
"
|
||||||
SELECT l.strong, l.lemma, l.transliteration, l.definition, l.language
|
SELECT l.strong, vs.surface, vs.position, l.lemma, l.transliteration, l.definition, l.language
|
||||||
FROM verse_strongs vs
|
FROM verse_strongs vs
|
||||||
JOIN lexicon l ON l.strong = vs.strong
|
JOIN lexicon l ON l.strong = vs.strong
|
||||||
WHERE vs.verse_id = ?1
|
WHERE vs.verse_id = ?1
|
||||||
@@ -748,10 +832,12 @@ fn load_strongs(conn: &Connection, verse_id: i64) -> Result<Vec<StrongLink>, Str
|
|||||||
.query_map(params![verse_id], |row| {
|
.query_map(params![verse_id], |row| {
|
||||||
Ok(StrongLink {
|
Ok(StrongLink {
|
||||||
strong: row.get(0)?,
|
strong: row.get(0)?,
|
||||||
lemma: row.get(1)?,
|
surface: row.get(1)?,
|
||||||
transliteration: row.get(2)?,
|
position: row.get(2)?,
|
||||||
definition: row.get(3)?,
|
lemma: row.get(3)?,
|
||||||
language: row.get(4)?,
|
transliteration: row.get(4)?,
|
||||||
|
definition: row.get(5)?,
|
||||||
|
language: row.get(6)?,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
@@ -763,6 +849,103 @@ fn load_strongs(conn: &Connection, verse_id: i64) -> Result<Vec<StrongLink>, Str
|
|||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_column_if_missing(
|
||||||
|
conn: &Connection,
|
||||||
|
table: &str,
|
||||||
|
column: &str,
|
||||||
|
definition: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(&format!("PRAGMA table_info({table})"))
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
let columns = stmt
|
||||||
|
.query_map([], |row| row.get::<_, String>(1))
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
for existing in columns {
|
||||||
|
if existing.map_err(|err| err.to_string())? == column {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
&format!("ALTER TABLE {table} ADD COLUMN {column} {definition}"),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_resource_refresh(conn: &Connection) -> Result<bool, String> {
|
||||||
|
let lexicon_count: i64 = conn
|
||||||
|
.query_row("SELECT COUNT(*) FROM lexicon", [], |row| row.get(0))
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
let real_definition_count: i64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM lexicon WHERE definition <> 'Linked Strong''s number from the source USFM package.'",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
Ok(lexicon_count < 10_000 || real_definition_count < 10_000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_catalog_lexicon_path(
|
||||||
|
catalog_dir: &Path,
|
||||||
|
index: &LibreBibleIndexCatalog,
|
||||||
|
) -> Option<PathBuf> {
|
||||||
|
for resource in &index.resources {
|
||||||
|
if resource.resource_type.as_deref() != Some("lexicon") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(package_path) = &resource.package_path {
|
||||||
|
if let Ok(catalog) =
|
||||||
|
load_resource_catalog_for_files(resolve_catalog_relative(catalog_dir, package_path))
|
||||||
|
{
|
||||||
|
let package_dir = resolve_catalog_relative(catalog_dir, package_path)
|
||||||
|
.parent()
|
||||||
|
.map(Path::to_path_buf)?;
|
||||||
|
if let Some(file) = catalog.files.entries_jsonl {
|
||||||
|
let path = package_dir.join(file.path);
|
||||||
|
if path.exists() {
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(files) = &resource.files {
|
||||||
|
if let Some(file) = &files.entries_jsonl {
|
||||||
|
let path = catalog_dir.join(&resource.id).join(&file.path);
|
||||||
|
if path.exists() {
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_resource_catalog_for_files(
|
||||||
|
catalog_path: PathBuf,
|
||||||
|
) -> Result<LibreBibleResourceCatalog, String> {
|
||||||
|
let catalog_text = fs::read_to_string(&catalog_path).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"Unable to read LibreBible resource catalog {}: {err}",
|
||||||
|
catalog_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
serde_json::from_str(&catalog_text).map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"Invalid LibreBible resource catalog {}: {err}",
|
||||||
|
catalog_path.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn load_tags(conn: &Connection, verse_id: i64) -> Result<Vec<Tag>, String> {
|
fn load_tags(conn: &Connection, verse_id: i64) -> Result<Vec<Tag>, String> {
|
||||||
let mut stmt = conn
|
let mut stmt = conn
|
||||||
.prepare(
|
.prepare(
|
||||||
@@ -851,9 +1034,21 @@ mod tests {
|
|||||||
let source_count: i64 = conn
|
let source_count: i64 = conn
|
||||||
.query_row("SELECT COUNT(*) FROM library_sources", [], |row| row.get(0))
|
.query_row("SELECT COUNT(*) FROM library_sources", [], |row| row.get(0))
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
|
let lexicon_count: i64 = conn
|
||||||
|
.query_row("SELECT COUNT(*) FROM lexicon", [], |row| row.get(0))
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
let g25_definition: String = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT definition FROM lexicon WHERE strong = 'G25'",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
assert_eq!(verse_count, 31_102);
|
assert_eq!(verse_count, 31_102);
|
||||||
assert_eq!(strongs_count, 348_884);
|
assert_eq!(strongs_count, 348_884);
|
||||||
|
assert_eq!(lexicon_count, 14_197);
|
||||||
|
assert!(g25_definition.contains("to love"));
|
||||||
assert_eq!(source_count, 1);
|
assert_eq!(source_count, 1);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||||
"productName": "Libre Study",
|
"productName": "Libre Study",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"identifier": "org.librestudy.desktop",
|
"identifier": "org.librestudy.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../dist",
|
"frontendDist": "../dist",
|
||||||
|
|||||||
+72
-4
@@ -218,6 +218,37 @@ input:focus-visible {
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.linked-word-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 22px;
|
||||||
|
padding-top: 18px;
|
||||||
|
border-top: 1px solid #e4e0d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 7px 9px;
|
||||||
|
border-color: #cfc8b8;
|
||||||
|
background: #f3efe4;
|
||||||
|
color: #202124;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-link small {
|
||||||
|
color: #657061;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-link.active {
|
||||||
|
border-color: #2f6f5e;
|
||||||
|
background: #e7f0eb;
|
||||||
|
}
|
||||||
|
|
||||||
.study-grid {
|
.study-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.65fr);
|
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.65fr);
|
||||||
@@ -255,13 +286,17 @@ input:focus-visible {
|
|||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strong-entry {
|
.strong-focus {
|
||||||
border: 1px solid #e4e0d6;
|
display: grid;
|
||||||
|
gap: 9px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #b9cabf;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 14px;
|
background: #eef6f1;
|
||||||
background: #fbfaf4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.strong-focus div,
|
||||||
.strong-entry div {
|
.strong-entry div {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -271,6 +306,39 @@ input:focus-visible {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.strong-focus h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #1f211e;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong-focus p {
|
||||||
|
margin: 0;
|
||||||
|
color: #3f443d;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong-focus button {
|
||||||
|
justify-self: start;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: #2f6f5e;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong-entry {
|
||||||
|
border: 1px solid #e4e0d6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
background: #fbfaf4;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong-entry.active {
|
||||||
|
border-color: #2f6f5e;
|
||||||
|
background: #f3faf6;
|
||||||
|
}
|
||||||
|
|
||||||
.strong-entry h4 {
|
.strong-entry h4 {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
color: #1f211e;
|
color: #1f211e;
|
||||||
|
|||||||
+48
-2
@@ -14,6 +14,7 @@ function App() {
|
|||||||
const [verses, setVerses] = useState<Verse[]>([])
|
const [verses, setVerses] = useState<Verse[]>([])
|
||||||
const [tags, setTags] = useState<Tag[]>([])
|
const [tags, setTags] = useState<Tag[]>([])
|
||||||
const [selectedId, setSelectedId] = useState<number | null>(null)
|
const [selectedId, setSelectedId] = useState<number | null>(null)
|
||||||
|
const [selectedStrong, setSelectedStrong] = useState<string | null>(null)
|
||||||
const [tagInput, setTagInput] = useState('')
|
const [tagInput, setTagInput] = useState('')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -60,6 +61,19 @@ function App() {
|
|||||||
() => verses.find((verse) => verse.id === selectedId) ?? verses[0],
|
() => verses.find((verse) => verse.id === selectedId) ?? verses[0],
|
||||||
[selectedId, verses],
|
[selectedId, verses],
|
||||||
)
|
)
|
||||||
|
const selectedStrongEntry = useMemo(() => {
|
||||||
|
if (!selectedVerse?.strongs.length) return null
|
||||||
|
return (
|
||||||
|
selectedVerse.strongs.find((entry) => entry.strong === selectedStrong) ??
|
||||||
|
selectedVerse.strongs[0]
|
||||||
|
)
|
||||||
|
}, [selectedStrong, selectedVerse])
|
||||||
|
|
||||||
|
function searchStrong(strong: string) {
|
||||||
|
setQuery(strong)
|
||||||
|
setSelectedStrong(strong)
|
||||||
|
runSearch(strong).catch((reason) => setError(String(reason)))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="app-shell">
|
<main className="app-shell">
|
||||||
@@ -154,6 +168,20 @@ function App() {
|
|||||||
|
|
||||||
<article className="verse-card">
|
<article className="verse-card">
|
||||||
<p>{selectedVerse.text}</p>
|
<p>{selectedVerse.text}</p>
|
||||||
|
<div className="linked-word-list" aria-label="Linked Strong's words">
|
||||||
|
{selectedVerse.strongs.map((entry) => (
|
||||||
|
<button
|
||||||
|
key={`${entry.strong}-${entry.position}`}
|
||||||
|
className={entry.strong === selectedStrongEntry?.strong ? 'word-link active' : 'word-link'}
|
||||||
|
type="button"
|
||||||
|
title={`${entry.surface}: ${entry.strong}`}
|
||||||
|
onClick={() => setSelectedStrong(entry.strong)}
|
||||||
|
>
|
||||||
|
<span>{entry.surface}</span>
|
||||||
|
<small>{entry.strong}</small>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<div className="study-grid">
|
<div className="study-grid">
|
||||||
@@ -162,12 +190,30 @@ function App() {
|
|||||||
<h3>Strong's Links</h3>
|
<h3>Strong's Links</h3>
|
||||||
<span>{selectedVerse.strongs.length}</span>
|
<span>{selectedVerse.strongs.length}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{selectedStrongEntry ? (
|
||||||
|
<article className="strong-focus">
|
||||||
|
<div>
|
||||||
|
<strong>{selectedStrongEntry.strong}</strong>
|
||||||
|
<span>{selectedStrongEntry.language}</span>
|
||||||
|
</div>
|
||||||
|
<h4>{selectedStrongEntry.lemma}</h4>
|
||||||
|
<p className="translit">{selectedStrongEntry.transliteration}</p>
|
||||||
|
<p>{selectedStrongEntry.definition}</p>
|
||||||
|
<button type="button" onClick={() => searchStrong(selectedStrongEntry.strong)}>
|
||||||
|
Show matching verses
|
||||||
|
</button>
|
||||||
|
</article>
|
||||||
|
) : null}
|
||||||
<div className="strong-list">
|
<div className="strong-list">
|
||||||
{selectedVerse.strongs.map((entry) => (
|
{selectedVerse.strongs.map((entry) => (
|
||||||
<article key={entry.strong} className="strong-entry">
|
<article
|
||||||
|
key={`${entry.strong}-${entry.position}`}
|
||||||
|
className={entry.strong === selectedStrongEntry?.strong ? 'strong-entry active' : 'strong-entry'}
|
||||||
|
onClick={() => setSelectedStrong(entry.strong)}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<strong>{entry.strong}</strong>
|
<strong>{entry.strong}</strong>
|
||||||
<span>{entry.language}</span>
|
<span>{entry.surface}</span>
|
||||||
</div>
|
</div>
|
||||||
<h4>{entry.lemma}</h4>
|
<h4>{entry.lemma}</h4>
|
||||||
<p className="translit">{entry.transliteration}</p>
|
<p className="translit">{entry.transliteration}</p>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ const browserVerses: Verse[] = [
|
|||||||
strongs: [
|
strongs: [
|
||||||
{
|
{
|
||||||
strong: 'H7225',
|
strong: 'H7225',
|
||||||
|
surface: 'beginning',
|
||||||
|
position: 1,
|
||||||
lemma: 'רֵאשִׁית',
|
lemma: 'רֵאשִׁית',
|
||||||
transliteration: 'reshith',
|
transliteration: 'reshith',
|
||||||
definition: 'Beginning, first, chief, the first part.',
|
definition: 'Beginning, first, chief, the first part.',
|
||||||
@@ -26,6 +28,8 @@ const browserVerses: Verse[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
strong: 'H430',
|
strong: 'H430',
|
||||||
|
surface: 'God',
|
||||||
|
position: 2,
|
||||||
lemma: 'אֱלֹהִים',
|
lemma: 'אֱלֹהִים',
|
||||||
transliteration: 'elohim',
|
transliteration: 'elohim',
|
||||||
definition: 'God, gods, judges, rulers; plural form often used of the one true God.',
|
definition: 'God, gods, judges, rulers; plural form often used of the one true God.',
|
||||||
@@ -43,6 +47,8 @@ const browserVerses: Verse[] = [
|
|||||||
strongs: [
|
strongs: [
|
||||||
{
|
{
|
||||||
strong: 'H3068',
|
strong: 'H3068',
|
||||||
|
surface: 'LORD',
|
||||||
|
position: 1,
|
||||||
lemma: 'יְהוָֹה',
|
lemma: 'יְהוָֹה',
|
||||||
transliteration: 'YHWH',
|
transliteration: 'YHWH',
|
||||||
definition: 'The covenant name of the LORD.',
|
definition: 'The covenant name of the LORD.',
|
||||||
@@ -50,6 +56,8 @@ const browserVerses: Verse[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
strong: 'H7462',
|
strong: 'H7462',
|
||||||
|
surface: 'shepherd',
|
||||||
|
position: 2,
|
||||||
lemma: 'רָעָה',
|
lemma: 'רָעָה',
|
||||||
transliteration: 'raah',
|
transliteration: 'raah',
|
||||||
definition: 'To shepherd, pasture, feed, tend a flock.',
|
definition: 'To shepherd, pasture, feed, tend a flock.',
|
||||||
@@ -67,6 +75,8 @@ const browserVerses: Verse[] = [
|
|||||||
strongs: [
|
strongs: [
|
||||||
{
|
{
|
||||||
strong: 'G25',
|
strong: 'G25',
|
||||||
|
surface: 'loved',
|
||||||
|
position: 1,
|
||||||
lemma: 'ἀγαπάω',
|
lemma: 'ἀγαπάω',
|
||||||
transliteration: 'agapao',
|
transliteration: 'agapao',
|
||||||
definition: 'To love, value, welcome, or take pleasure in.',
|
definition: 'To love, value, welcome, or take pleasure in.',
|
||||||
@@ -74,6 +84,8 @@ const browserVerses: Verse[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
strong: 'G4100',
|
strong: 'G4100',
|
||||||
|
surface: 'believeth',
|
||||||
|
position: 3,
|
||||||
lemma: 'πιστεύω',
|
lemma: 'πιστεύω',
|
||||||
transliteration: 'pisteuo',
|
transliteration: 'pisteuo',
|
||||||
definition: 'To believe, trust, have faith in.',
|
definition: 'To believe, trust, have faith in.',
|
||||||
@@ -91,6 +103,8 @@ const browserVerses: Verse[] = [
|
|||||||
strongs: [
|
strongs: [
|
||||||
{
|
{
|
||||||
strong: 'G2822',
|
strong: 'G2822',
|
||||||
|
surface: 'called',
|
||||||
|
position: 2,
|
||||||
lemma: 'κλητός',
|
lemma: 'κλητός',
|
||||||
transliteration: 'kletos',
|
transliteration: 'kletos',
|
||||||
definition: 'Called, invited, appointed.',
|
definition: 'Called, invited, appointed.',
|
||||||
@@ -98,6 +112,8 @@ const browserVerses: Verse[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
strong: 'G4286',
|
strong: 'G4286',
|
||||||
|
surface: 'purpose',
|
||||||
|
position: 3,
|
||||||
lemma: 'πρόθεσις',
|
lemma: 'πρόθεσις',
|
||||||
transliteration: 'prothesis',
|
transliteration: 'prothesis',
|
||||||
definition: 'Purpose, setting forth, deliberate plan.',
|
definition: 'Purpose, setting forth, deliberate plan.',
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export type Verse = {
|
|||||||
|
|
||||||
export type StrongLink = {
|
export type StrongLink = {
|
||||||
strong: string
|
strong: string
|
||||||
|
surface: string
|
||||||
|
position: number
|
||||||
lemma: string
|
lemma: string
|
||||||
transliteration: string
|
transliteration: string
|
||||||
definition: string
|
definition: string
|
||||||
|
|||||||
Reference in New Issue
Block a user