Add Bible picker flow and chapter navigation UI
This commit is contained in:
155
Views/BibleChapterView.js
Normal file
155
Views/BibleChapterView.js
Normal file
@@ -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 (
|
||||
<SafeAreaView style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<ActivityIndicator />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, justifyContent: "center", alignItems: "center", padding: 16 }}>
|
||||
<Text style={{ color: "#b91c1c" }}>{error}</Text>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const handleVersePress = (verseNumber) => {
|
||||
if (!selectable) return;
|
||||
GlobalState.bibleChapterSelection = {
|
||||
...parseBibleReference(`${chapterReference}:${verseNumber}`),
|
||||
ts: Date.now(),
|
||||
};
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<View style={{ flex: 1, padding: 12 }}>
|
||||
<Text style={{ fontSize: 22, fontWeight: "700", marginBottom: 4 }}>
|
||||
{chapterData?.reference || chapterReference}
|
||||
</Text>
|
||||
<Text style={{ color: "#6b7280", marginBottom: 10 }}>
|
||||
{chapterData?.translation_name || chapterData?.translation_id || "KJV"}
|
||||
</Text>
|
||||
{selectable ? (
|
||||
<Text style={{ color: "#6b7280", marginBottom: 8 }}>Tap a verse to select it.</Text>
|
||||
) : null}
|
||||
<FlatList
|
||||
ref={listRef}
|
||||
data={verses}
|
||||
keyExtractor={(item, idx) => `${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 (
|
||||
<Pressable
|
||||
onPress={() => handleVersePress(verseNumber)}
|
||||
style={{
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 8,
|
||||
marginBottom: 6,
|
||||
backgroundColor: isSelected ? "#fff3cd" : "transparent",
|
||||
borderWidth: isSelected ? 1 : 0,
|
||||
borderColor: isSelected ? "#f59e0b" : "transparent",
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontWeight: "700", color: isSelected ? "#92400e" : "#374151" }}>
|
||||
{verseNumber}
|
||||
</Text>
|
||||
<Text style={{ color: "#111827", lineHeight: 21 }}>{item?.text || ""}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default BibleChapterView;
|
||||
Reference in New Issue
Block a user