From ccfeed3c9275e4f35aead9571c34547c24cd6e3d Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Tue, 24 Feb 2026 15:56:48 -0500 Subject: [PATCH] Add Bible picker flow and chapter navigation UI --- App.js | 11 ++ Views/BibleChapterView.js | 155 ++++++++++++++++++++ Views/BiblePicker.js | 252 ++++++++++++++++++++++++++++++++ Views/GlobalChat.js | 23 ++- Views/NewPost.js | 73 ++++++++- components/BibleEmbeddedView.js | 118 +++++++++++++++ components/Post.js | 14 +- contexts/GlobalState.js | 2 + utils/bibleReferences.js | 229 +++++++++++++++++++++++++++++ 9 files changed, 865 insertions(+), 12 deletions(-) create mode 100644 Views/BibleChapterView.js create mode 100644 Views/BiblePicker.js create mode 100644 components/BibleEmbeddedView.js create mode 100644 utils/bibleReferences.js diff --git a/App.js b/App.js index d2bcbbd..f447214 100644 --- a/App.js +++ b/App.js @@ -30,6 +30,8 @@ import NewGroup from './Views/NewGroup.js'; import Slideshow from './Views/Slideshow.js'; import SongPlayer from './Views/SongPlayer.js'; import GlobalChat from './Views/GlobalChat.js'; +import BiblePicker from './Views/BiblePicker.js'; +import BibleChapterView from './Views/BibleChapterView.js'; import { Platform } from 'react-native'; import { PostHogProvider } from 'posthog-react-native' import * as Updates from 'expo-updates'; @@ -426,6 +428,15 @@ export default function App() { name="GlobalChat" component={GlobalChat} /> + + diff --git a/Views/BibleChapterView.js b/Views/BibleChapterView.js new file mode 100644 index 0000000..1013a58 --- /dev/null +++ b/Views/BibleChapterView.js @@ -0,0 +1,155 @@ +import React from "react"; +import { FlatList, Pressable, View } from "react-native"; +import { ActivityIndicator, Text } from "react-native-paper"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useNavigation } from "@react-navigation/native"; +import { parseBibleReference } from "../utils/bibleReferences.js"; +import GlobalState from "../contexts/GlobalState.js"; + +const BibleChapterView = ({ route }) => { + const navigation = useNavigation(); + const reference = route?.params?.reference || ""; + const selectable = route?.params?.selectable === true; + const { chapterReference, verse: selectedVerse } = parseBibleReference(reference); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(""); + const [chapterData, setChapterData] = React.useState(null); + const listRef = React.useRef(null); + const autoScrolledRef = React.useRef(false); + + React.useEffect(() => { + let mounted = true; + const loadChapter = async () => { + setLoading(true); + setError(""); + try { + const response = await fetch(`https://bible-api.com/${encodeURIComponent(chapterReference)}`); + if (!response.ok) throw new Error("Failed chapter request"); + const payload = await response.json(); + if (!mounted) return; + setChapterData(payload); + } catch (_error) { + if (!mounted) return; + setError("Unable to load chapter."); + setChapterData(null); + } finally { + if (mounted) setLoading(false); + } + }; + loadChapter(); + return () => { + mounted = false; + }; + }, [chapterReference]); + + const verses = Array.isArray(chapterData?.verses) ? chapterData.verses : []; + const selectedVerseNumber = Number(selectedVerse || 1); + const selectedIndex = verses.findIndex((item) => Number(item?.verse || 0) === selectedVerseNumber); + + React.useEffect(() => { + autoScrolledRef.current = false; + }, [chapterReference, selectedVerseNumber]); + + const scrollToSelectedVerse = React.useCallback((animated = false) => { + if (autoScrolledRef.current) return; + if (!listRef.current || selectedIndex < 0 || !verses.length) return; + try { + listRef.current.scrollToIndex({ + index: selectedIndex, + animated, + viewPosition: 0.35, + }); + autoScrolledRef.current = true; + } catch (_error) { + // FlatList can throw before enough measurements are available. + } + }, [selectedIndex, verses.length]); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + {error} + + ); + } + + const handleVersePress = (verseNumber) => { + if (!selectable) return; + GlobalState.bibleChapterSelection = { + ...parseBibleReference(`${chapterReference}:${verseNumber}`), + ts: Date.now(), + }; + navigation.goBack(); + }; + + return ( + + + + {chapterData?.reference || chapterReference} + + + {chapterData?.translation_name || chapterData?.translation_id || "KJV"} + + {selectable ? ( + Tap a verse to select it. + ) : null} + `${item?.verse || idx}`} + initialNumToRender={24} + onLayout={() => { + setTimeout(() => scrollToSelectedVerse(false), 40); + }} + onContentSizeChange={() => { + setTimeout(() => scrollToSelectedVerse(false), 40); + }} + onScrollToIndexFailed={({ index, averageItemLength }) => { + if (!listRef.current) return; + listRef.current.scrollToOffset({ + offset: Math.max(0, (averageItemLength || 36) * index), + animated: false, + }); + setTimeout(() => { + scrollToSelectedVerse(false); + }, 120); + }} + renderItem={({ item }) => { + const verseNumber = Number(item?.verse || 0); + const isSelected = verseNumber === selectedVerseNumber; + return ( + handleVersePress(verseNumber)} + style={{ + paddingVertical: 8, + paddingHorizontal: 10, + borderRadius: 8, + marginBottom: 6, + backgroundColor: isSelected ? "#fff3cd" : "transparent", + borderWidth: isSelected ? 1 : 0, + borderColor: isSelected ? "#f59e0b" : "transparent", + }} + > + + {verseNumber} + + {item?.text || ""} + + ); + }} + /> + + + ); +}; + +export default BibleChapterView; diff --git a/Views/BiblePicker.js b/Views/BiblePicker.js new file mode 100644 index 0000000..6ae86e3 --- /dev/null +++ b/Views/BiblePicker.js @@ -0,0 +1,252 @@ +import React from "react"; +import { FlatList, Pressable, View } from "react-native"; +import { ActivityIndicator, Button, Chip, Divider, Text, TextInput } from "react-native-paper"; +import { useNavigation } from "@react-navigation/native"; +import { useSnapshot } from "valtio"; +import GlobalState from "../contexts/GlobalState.js"; +import { BIBLE_BOOKS, createBibleToken, fetchBiblePassage, getBookChapterCount } from "../utils/bibleReferences.js"; + +const BiblePicker = ({ route }) => { + const navigation = useNavigation(); + const gState = useSnapshot(GlobalState); + const target = route?.params?.target || "post"; + const [query, setQuery] = React.useState(route?.params?.initialReference || ""); + const [chapter, setChapter] = React.useState("1"); + const [verse, setVerse] = React.useState("1"); + const [selectedBook, setSelectedBook] = React.useState(""); + const [activeStep, setActiveStep] = React.useState("book"); + const [preview, setPreview] = React.useState(null); + const [loadingPreview, setLoadingPreview] = React.useState(false); + const [loadingVerses, setLoadingVerses] = React.useState(false); + const [error, setError] = React.useState(""); + const [verseOptions, setVerseOptions] = React.useState(["1"]); + const chapterSelectionTs = gState?.bibleChapterSelection?.ts; + + const computedReference = React.useMemo(() => { + if (query.trim()) return query.trim(); + if (!selectedBook) return ""; + return `${selectedBook} ${chapter || "1"}:${verse || "1"}`; + }, [chapter, query, selectedBook, verse]); + + const filteredBooks = React.useMemo(() => { + const q = query.toLowerCase().trim(); + if (!q) return BIBLE_BOOKS; + return BIBLE_BOOKS.filter((book) => book.toLowerCase().includes(q)); + }, [query]); + + const selectedBookChapterCount = React.useMemo(() => getBookChapterCount(selectedBook), [selectedBook]); + const chapterOptions = React.useMemo( + () => Array.from({ length: selectedBookChapterCount }, (_v, i) => String(i + 1)), + [selectedBookChapterCount] + ); + + const addReferenceToCaller = (reference) => { + const cleanReference = String(reference || "").trim(); + if (!cleanReference) return; + GlobalState.biblePickerSelection = { + token: createBibleToken(cleanReference), + reference: cleanReference, + target, + ts: Date.now(), + }; + navigation.goBack(); + }; + + const loadPreviewForReference = async (reference) => { + if (!reference) return; + setError(""); + setLoadingPreview(true); + try { + const passage = await fetchBiblePassage(reference); + setPreview(passage); + } catch (_err) { + setPreview(null); + setError("Unable to load this Bible passage."); + } finally { + setLoadingPreview(false); + } + }; + + const loadVerseOptions = async (book, chapterNumber) => { + if (!book || !chapterNumber) { + setVerseOptions(["1"]); + return; + } + setLoadingVerses(true); + try { + const response = await fetch(`https://bible-api.com/${encodeURIComponent(`${book} ${chapterNumber}`)}`); + if (!response.ok) throw new Error("Failed chapter fetch"); + const payload = await response.json(); + const options = Array.isArray(payload?.verses) + ? payload.verses.map((v) => String(v?.verse || "")).filter(Boolean) + : []; + setVerseOptions(options.length ? options : ["1"]); + } catch (_error) { + setVerseOptions(["1"]); + } finally { + setLoadingVerses(false); + } + }; + + React.useEffect(() => { + const picked = gState?.bibleChapterSelection; + if (!picked || !picked.book) return; + setSelectedBook(picked.book); + setChapter(String(picked.chapter || 1)); + setVerse(String(picked.verse || 1)); + setQuery(""); + setActiveStep("verse"); + loadVerseOptions(picked.book, String(picked.chapter || 1)); + loadPreviewForReference(picked.reference || `${picked.book} ${picked.chapter || 1}:${picked.verse || 1}`); + GlobalState.bibleChapterSelection = null; + }, [chapterSelectionTs]); + + return ( + + Bible Reference + + Pick a reference to insert into your {target === "chat" ? "chat message" : "post"}. + + + + + + + + + {computedReference ? Selected: {computedReference} : null} + {preview?.text ? ( + navigation.navigate("BibleChapter", { reference: computedReference, selectable: true })} + style={{ marginTop: 10, padding: 10, backgroundColor: "#f8fafc", borderRadius: 10 }} + > + {preview.text.slice(0, 420)} + + {preview.reference} ({preview.translation}) + + Tap preview to pick verse from chapter + + ) : null} + {error ? {error} : null} + + + + + + + + + + {activeStep === "book" ? ( + <> + Books + item} + renderItem={({ item }) => ( + { + setSelectedBook(item); + setQuery(""); + setChapter("1"); + setVerse("1"); + setActiveStep("chapter"); + await loadVerseOptions(item, "1"); + await loadPreviewForReference(`${item} 1:1`); + }} + > + {item} + + )} + /> + + ) : null} + + {activeStep === "chapter" ? ( + <> + + Chapters {selectedBook ? `(${selectedBook})` : ""} + + item} + renderItem={({ item }) => ( + { + setChapter(item); + setVerse("1"); + setActiveStep("verse"); + await loadVerseOptions(selectedBook, item); + await loadPreviewForReference(`${selectedBook} ${item}:1`); + }} + > + {item} + + )} + /> + + ) : null} + + {activeStep === "verse" ? ( + <> + + Verses {selectedBook && chapter ? `(${selectedBook} ${chapter})` : ""} + + {loadingVerses ? ( + + ) : ( + item} + renderItem={({ item }) => ( + { + setVerse(item); + await loadPreviewForReference(`${selectedBook} ${chapter || "1"}:${item}`); + }} + > + {item} + + )} + /> + )} + + ) : null} + + ); +}; + +export default BiblePicker; diff --git a/Views/GlobalChat.js b/Views/GlobalChat.js index de3bd6c..2f46640 100644 --- a/Views/GlobalChat.js +++ b/Views/GlobalChat.js @@ -8,6 +8,8 @@ import { useSnapshot } from "valtio"; import API from "../API.js"; import GlobalState from "../contexts/GlobalState.js"; import i18n from "../i18nMessages.js"; +import BibleEmbeddedView from "../components/BibleEmbeddedView.js"; +import { stripBibleTokens } from "../utils/bibleReferences.js"; const PANEL_WIDTH = 260; @@ -59,7 +61,8 @@ const CircleIconAction = ({ icon, onPress, color = "#6b7280", disabled = false } const ChatMessage = ({ item, myProfileId, showOriginal, onPressIn, onPressOut }) => { const isMine = item?.senderProfileId === myProfileId; const isTranslated = !!(item?.textOriginal && item?.text && item.textOriginal !== item.text); - const messageText = isTranslated && showOriginal ? item?.textOriginal : item?.text; + const messageTextRaw = isTranslated && showOriginal ? item?.textOriginal : item?.text; + const messageText = stripBibleTokens(messageTextRaw || ""); const youtubeVideoId = getYouTubeVideoIdFromText(item?.textOriginal || "") || getYouTubeVideoIdFromText(item?.text || ""); const createdAt = item?.createdAt ? new Date(item.createdAt) : null; return ( @@ -90,6 +93,9 @@ const ChatMessage = ({ item, myProfileId, showOriginal, onPressIn, onPressOut }) {createdAt && !Number.isNaN(createdAt.getTime()) ? createdAt.toLocaleTimeString() : ""} + + + {youtubeVideoId ? ( { +let GlobalChat = ({ navigation }) => { const gState = useSnapshot(GlobalState); const myProfileId = gState?.me?._id || ""; const [messages, setMessages] = React.useState([]); @@ -133,6 +139,7 @@ let GlobalChat = () => { const panelOpacity = React.useRef(new Animated.Value(0)).current; const listRef = React.useRef(null); const hasDoneInitialScrollRef = React.useRef(false); + const biblePickerSelectionTs = gState?.biblePickerSelection?.ts; const scrollToBottom = React.useCallback((animated = true) => { if (!listRef.current) return; @@ -188,6 +195,16 @@ let GlobalChat = () => { }; }, [loadMessages, refreshPresence]); + React.useEffect(() => { + const selection = gState?.biblePickerSelection; + if (!selection || selection.target !== "chat" || !selection.token) return; + setText((prev) => { + if ((prev || "").includes(selection.token)) return prev; + return `${(prev || "").trim()} ${selection.token}`.trim(); + }); + GlobalState.biblePickerSelection = null; + }, [biblePickerSelectionTs, gState?.biblePickerSelection]); + const sendMessage = async () => { const trimmedText = (text || "").trim(); if (!trimmedText || sending) return; @@ -343,7 +360,7 @@ let GlobalChat = () => { elevation: 2, }} > - { }} /> + navigation.navigate("BiblePicker", { target: "chat" })} /> { + const gState = useSnapshot(GlobalState); let [postContent, setPostContent] = useState(''); - let initialContent = props.route.params.intialContent; - let [extraContent, setExtraContent] = useState([initialContent]); + let initialContent = props.route?.params?.intialContent; + let [extraContent, setExtraContent] = useState(initialContent ? [initialContent] : []); let [toProfile, setToProfile] = useState([]); const [photo, setPhoto] = useState(null); const [uploadProgress, setUploadProgress] = useState(0); const [isUploading, setIsUploading] = useState(false); // Variable to prevent posting during upload const [cancelUpload, setCancelUpload] = useState(false); // Variable to handle upload cancellation + const [bibleReferences, setBibleReferences] = useState([]); const navigation = useNavigation(); + const biblePickerSelectionTs = gState?.biblePickerSelection?.ts; useEffect(() => { let subscribed = true; const getProfileData = async () => { - if (!props.route.params?.toProfile) return; + if (!props.route?.params?.toProfile) return; try { // Fetch profile data and update state if component is still subscribed const profileObj = await API.getUserProfile(props.route.params.toProfile); @@ -44,7 +49,31 @@ let NewPostView = (props) => { // Cleanup subscription flag subscribed = false; } - }, [props.route.params?.sendNow]); + }, [props.route?.params?.sendNow]); + + useEffect(() => { + const selection = gState?.biblePickerSelection; + if (!selection || selection.target !== "post" || !selection.token) return; + if (selection.reference) { + setBibleReferences((prev) => { + if (prev.includes(selection.reference)) return prev; + return prev.concat(selection.reference); + }); + } + GlobalState.biblePickerSelection = null; + }, [biblePickerSelectionTs]); + + const handlePostContentChange = (nextValue = "") => { + const detectedReferences = extractBibleReferences(nextValue); + if (detectedReferences.length) { + setBibleReferences((prev) => { + const seen = new Set(prev); + detectedReferences.forEach((reference) => seen.add(reference)); + return Array.from(seen); + }); + } + setPostContent(stripBibleTokens(nextValue)); + }; const pickImage = async () => { try { @@ -142,10 +171,15 @@ let NewPostView = (props) => { return; } try { + const bibleTokens = bibleReferences.map((reference) => createBibleToken(reference)).filter(Boolean); // Create a new post with the combined content - await API.newPost(postContent + " " + extraContent.join(" "), props.route.params?.toProfile); + await API.newPost( + [postContent, extraContent.join(" "), bibleTokens.join(" ")].join(" ").trim(), + props.route?.params?.toProfile + ); setPostContent(''); // Clear post content after submission setExtraContent([]); // Clear extra content after submission + setBibleReferences([]); navigation.navigate('Feed', { reRender: Math.random() }); // Navigate back to the Feed and trigger re-render } catch (error) { console.error("Error creating new post", error); @@ -177,7 +211,7 @@ let NewPostView = (props) => { {/* Text input for post content */} { }} autoFocus={true} /> + {bibleReferences.length ? ( + + {bibleReferences.map((reference) => ( + navigation.navigate("BibleChapter", { reference })} + onClose={() => { + setBibleReferences((prev) => prev.filter((item) => item !== reference)); + }} + > + {reference} + + ))} + + ) : null} {/* Button to pick images from the gallery */} + diff --git a/components/BibleEmbeddedView.js b/components/BibleEmbeddedView.js new file mode 100644 index 0000000..8af02f8 --- /dev/null +++ b/components/BibleEmbeddedView.js @@ -0,0 +1,118 @@ +import React, { useMemo, useState } from "react"; +import { StyleSheet, Text, View } from "react-native"; +import { ActivityIndicator, Chip } from "react-native-paper"; +import { useNavigation } from "@react-navigation/native"; +import { extractBibleReferences, fetchBiblePassage } from "../utils/bibleReferences.js"; + +const BibleEmbeddedView = ({ content = "", compact = false, openChapterOnPress = false }) => { + const navigation = useNavigation(); + const references = useMemo(() => extractBibleReferences(content), [content]); + const [selectedRef, setSelectedRef] = useState(""); + const [byReference, setByReference] = useState({}); + + if (!references.length) return null; + + const handleSelectReference = async (reference) => { + if (openChapterOnPress) { + navigation.navigate("BibleChapter", { reference }); + return; + } + setSelectedRef(reference); + const current = byReference[reference]; + if (current?.loading || current?.text || current?.error) return; + + setByReference((prev) => ({ ...prev, [reference]: { loading: true } })); + try { + const data = await fetchBiblePassage(reference); + setByReference((prev) => ({ ...prev, [reference]: { loading: false, ...data } })); + } catch (_error) { + setByReference((prev) => ({ ...prev, [reference]: { loading: false, error: true } })); + } + }; + + const selectedData = selectedRef ? byReference[selectedRef] : null; + + return ( + + Bible + + {references.map((reference) => ( + handleSelectReference(reference)} + > + {reference} + + ))} + + {selectedData?.loading ? : null} + {selectedData?.text ? ( + + {selectedData.text.slice(0, compact ? 160 : 280)} + + {selectedData.reference} ({selectedData.translation}) + + + ) : null} + {selectedData?.error ? Unable to load this passage. : null} + + ); +}; + +export default BibleEmbeddedView; + +const styles = StyleSheet.create({ + container: { + paddingTop: 6, + paddingHorizontal: 8, + }, + compactContainer: { + paddingTop: 4, + paddingHorizontal: 0, + }, + label: { + fontSize: 12, + color: "#4b5563", + fontWeight: "700", + marginBottom: 4, + }, + chipsWrap: { + flexDirection: "row", + flexWrap: "wrap", + }, + chip: { + marginRight: 6, + marginBottom: 6, + backgroundColor: "#f8fafc", + }, + loader: { + alignSelf: "flex-start", + }, + previewBox: { + marginTop: 2, + backgroundColor: "#f8fafc", + borderWidth: 1, + borderColor: "#e5e7eb", + borderRadius: 10, + padding: 8, + }, + previewText: { + fontSize: 13, + color: "#111827", + }, + previewMeta: { + marginTop: 4, + fontSize: 11, + color: "#6b7280", + fontWeight: "600", + }, + errorText: { + marginTop: 2, + fontSize: 12, + color: "#b91c1c", + }, +}); diff --git a/components/Post.js b/components/Post.js index fc00b08..38f2db3 100644 --- a/components/Post.js +++ b/components/Post.js @@ -14,6 +14,8 @@ import ProfilePhotoCircle from './ProfilePhotoCircle.js'; import { posthog } from './../PostHog.js'; import { useNavigation } from '@react-navigation/native'; import ParsedText from 'react-native-parsed-text'; +import BibleEmbeddedView from './BibleEmbeddedView.js'; +import { stripBibleTokens } from '../utils/bibleReferences.js'; let Post = (props) => { @@ -30,7 +32,7 @@ let Post = (props) => { const SWIPE_WIDTH = 86; let toProfileText = post.toProfile && post.toProfile !== post.profileid ? : undefined; - let cleanContent = post.content.replace(/@[A-z]+:.+\w/g, '').trim(); + let cleanContent = stripInlineTags(stripBibleTokens(post.content)); const navigation = useNavigation(); //cleanContent = convertLinks(cleanContent); const newComentAdded = (commentData) => { @@ -172,6 +174,9 @@ let Post = (props) => { {cleanContent} + + + { @@ -356,3 +361,10 @@ const styles = StyleSheet.create({ textDecorationLine: 'underline', }, }); + const stripInlineTags = (content = "") => { + return String(content || "") + .replace(/@[A-Za-z]+:[^\s]+/g, "") + .replace(/[ \t]{2,}/g, " ") + .replace(/[ \t]+\n/g, "\n") + .trim(); + }; diff --git a/contexts/GlobalState.js b/contexts/GlobalState.js index 3cf6e4f..169e207 100644 --- a/contexts/GlobalState.js +++ b/contexts/GlobalState.js @@ -6,6 +6,8 @@ const GlobalState = proxy({ profiles: {}, currentMedia: '', mediaPost: {}, + biblePickerSelection: null, + bibleChapterSelection: null, }); export default GlobalState; diff --git a/utils/bibleReferences.js b/utils/bibleReferences.js new file mode 100644 index 0000000..27fb4f1 --- /dev/null +++ b/utils/bibleReferences.js @@ -0,0 +1,229 @@ +const BIBLE_TOKEN_REGEX = /@bible:([^\s]+)/gi; + +const normalizeReference = (value = "") => { + try { + return decodeURIComponent(String(value || "")) + .replace(/_/g, " ") + .replace(/\s+/g, " ") + .trim(); + } catch (_error) { + return String(value || "").replace(/_/g, " ").replace(/\s+/g, " ").trim(); + } +}; + +export const encodeBibleReference = (reference = "") => { + return String(reference || "").replace(/\s+/g, "_").trim(); +}; + +export const createBibleToken = (reference = "") => { + const encoded = encodeBibleReference(reference); + if (!encoded) return ""; + return `@bible:${encoded}`; +}; + +export const extractBibleReferences = (content = "") => { + const seen = new Set(); + const refs = []; + if (!content || typeof content !== "string") return refs; + + let match; + while ((match = BIBLE_TOKEN_REGEX.exec(content)) !== null) { + const normalized = normalizeReference(match[1]); + if (!normalized || seen.has(normalized)) continue; + seen.add(normalized); + refs.push(normalized); + } + return refs; +}; + +export const stripBibleTokens = (content = "") => { + if (!content || typeof content !== "string") return ""; + return content + .replace(BIBLE_TOKEN_REGEX, "") + .replace(/[ \t]{2,}/g, " ") + .replace(/[ \t]+\n/g, "\n") + .trim(); +}; + +export const fetchBiblePassage = async (reference = "") => { + const safeReference = normalizeReference(reference); + if (!safeReference) { + throw new Error("Missing Bible reference"); + } + const response = await fetch(`https://bible-api.com/${encodeURIComponent(safeReference)}`); + if (!response.ok) { + throw new Error("Failed to load Bible passage"); + } + const payload = await response.json(); + return { + reference: payload?.reference || safeReference, + text: (payload?.text || "").trim(), + translation: payload?.translation_name || payload?.translation_id || "KJV", + }; +}; + +export const parseBibleReference = (reference = "") => { + const normalized = normalizeReference(reference); + const match = normalized.match(/^(.*)\s+(\d+)(?::(\d+)(?:-\d+)?)?$/); + if (!match) { + return { + reference: normalized, + book: normalized, + chapter: 1, + verse: 1, + chapterReference: normalized, + }; + } + const book = (match[1] || "").trim(); + const chapter = Number(match[2] || "1"); + const verse = Number(match[3] || "1"); + return { + reference: normalized, + book, + chapter, + verse, + chapterReference: `${book} ${chapter}`, + }; +}; + +export const BIBLE_BOOKS = [ + "Genesis", + "Exodus", + "Leviticus", + "Numbers", + "Deuteronomy", + "Joshua", + "Judges", + "Ruth", + "1 Samuel", + "2 Samuel", + "1 Kings", + "2 Kings", + "1 Chronicles", + "2 Chronicles", + "Ezra", + "Nehemiah", + "Esther", + "Job", + "Psalms", + "Proverbs", + "Ecclesiastes", + "Song of Solomon", + "Isaiah", + "Jeremiah", + "Lamentations", + "Ezekiel", + "Daniel", + "Hosea", + "Joel", + "Amos", + "Obadiah", + "Jonah", + "Micah", + "Nahum", + "Habakkuk", + "Zephaniah", + "Haggai", + "Zechariah", + "Malachi", + "Matthew", + "Mark", + "Luke", + "John", + "Acts", + "Romans", + "1 Corinthians", + "2 Corinthians", + "Galatians", + "Ephesians", + "Philippians", + "Colossians", + "1 Thessalonians", + "2 Thessalonians", + "1 Timothy", + "2 Timothy", + "Titus", + "Philemon", + "Hebrews", + "James", + "1 Peter", + "2 Peter", + "1 John", + "2 John", + "3 John", + "Jude", + "Revelation", +]; + +export const BIBLE_BOOK_CHAPTERS = { + Genesis: 50, + Exodus: 40, + Leviticus: 27, + Numbers: 36, + Deuteronomy: 34, + Joshua: 24, + Judges: 21, + Ruth: 4, + "1 Samuel": 31, + "2 Samuel": 24, + "1 Kings": 22, + "2 Kings": 25, + "1 Chronicles": 29, + "2 Chronicles": 36, + Ezra: 10, + Nehemiah: 13, + Esther: 10, + Job: 42, + Psalms: 150, + Proverbs: 31, + Ecclesiastes: 12, + "Song of Solomon": 8, + Isaiah: 66, + Jeremiah: 52, + Lamentations: 5, + Ezekiel: 48, + Daniel: 12, + Hosea: 14, + Joel: 3, + Amos: 9, + Obadiah: 1, + Jonah: 4, + Micah: 7, + Nahum: 3, + Habakkuk: 3, + Zephaniah: 3, + Haggai: 2, + Zechariah: 14, + Malachi: 4, + Matthew: 28, + Mark: 16, + Luke: 24, + John: 21, + Acts: 28, + Romans: 16, + "1 Corinthians": 16, + "2 Corinthians": 13, + Galatians: 6, + Ephesians: 6, + Philippians: 4, + Colossians: 4, + "1 Thessalonians": 5, + "2 Thessalonians": 3, + "1 Timothy": 6, + "2 Timothy": 4, + Titus: 3, + Philemon: 1, + Hebrews: 13, + James: 5, + "1 Peter": 5, + "2 Peter": 3, + "1 John": 5, + "2 John": 1, + "3 John": 1, + Jude: 1, + Revelation: 22, +}; + +export const getBookChapterCount = (book = "") => { + return BIBLE_BOOK_CHAPTERS[book] || 1; +};