Skip to content

Commit 572cafc

Browse files
Raoof128claude
andcommitted
feat: upgrade Bible Reader — search, verse interactions, better UX
Book List: - Search/filter bar to find books by name instantly - Book ID abbreviation badge (GEN, MAT, etc.) on each card - Larger cards (minHeight 80) with better visual hierarchy - Live filtering updates book count in section divider Chapter Grid: - Larger cells (64px) for better touch targets - Border radius 14 for consistency - "Tap to start reading" hint text Verse Reader: - Tap-on-verse context menu with animated FadeIn: Copy, Bookmark, Share, and "Ask Aion" (opens AI chat with verse) - Purple highlight on selected verse - Paragraph spacing every 5 verses for readability - Auto-dismiss selection on scroll (20px threshold) - "Ask Aion" sends verse to AI chat for explanation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 15b5507 commit 572cafc

3 files changed

Lines changed: 173 additions & 13 deletions

File tree

app/(tabs)/read.tsx

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { useState, useCallback } from "react";
1+
import { useState, useCallback, useMemo } from "react";
22
import {
33
View,
44
Text,
55
FlatList,
66
Pressable,
77
StyleSheet,
88
Platform,
9+
TextInput,
910
} from "react-native";
1011
import { SafeAreaView } from "react-native-safe-area-context";
1112
import { useRouter } from "expo-router";
@@ -29,8 +30,13 @@ function triggerHaptic() {
2930
export default function ReadScreen() {
3031
const router = useRouter();
3132
const [activeTab, setActiveTab] = useState<Testament>("OT");
33+
const [search, setSearch] = useState("");
3234

33-
const books = activeTab === "OT" ? OT_BOOKS : NT_BOOKS;
35+
const filteredBooks = useMemo(() => {
36+
const list = activeTab === "OT" ? OT_BOOKS : NT_BOOKS;
37+
if (!search.trim()) return list;
38+
return list.filter(b => b.name.toLowerCase().includes(search.toLowerCase()));
39+
}, [activeTab, search]);
3440

3541
const handleBookPress = useCallback(
3642
(book: BibleBook) => {
@@ -61,6 +67,7 @@ export default function ReadScreen() {
6167
accessibilityLabel={`${item.name}, ${item.chapters} chapters`}
6268
accessibilityRole="button"
6369
>
70+
<Text style={styles.bookAbbr}>{item.id}</Text>
6471
<Text style={styles.bookName} numberOfLines={1}>
6572
{item.name}
6673
</Text>
@@ -124,16 +131,33 @@ export default function ReadScreen() {
124131
</Pressable>
125132
</View>
126133

134+
{/* Search bar */}
135+
<View style={styles.searchContainer}>
136+
<Text style={styles.searchIcon}>🔍</Text>
137+
<TextInput
138+
style={styles.searchInput}
139+
placeholder="Search books..."
140+
placeholderTextColor={colors.textGhost}
141+
value={search}
142+
onChangeText={setSearch}
143+
/>
144+
{search.length > 0 && (
145+
<Pressable onPress={() => setSearch("")}>
146+
<Text style={styles.clearIcon}></Text>
147+
</Pressable>
148+
)}
149+
</View>
150+
127151
{/* Section indicator */}
128152
<View style={styles.sectionInfo}>
129153
<View style={styles.sectionLine} />
130-
<Text style={styles.sectionText}>{books.length} BOOKS</Text>
154+
<Text style={styles.sectionText}>{filteredBooks.length} BOOKS</Text>
131155
<View style={styles.sectionLine} />
132156
</View>
133157

134158
{/* Book Grid */}
135159
<FlatList
136-
data={books}
160+
data={filteredBooks}
137161
renderItem={renderBookCard}
138162
keyExtractor={keyExtractor}
139163
numColumns={2}
@@ -238,11 +262,12 @@ const styles = StyleSheet.create({
238262
},
239263
bookCard: {
240264
flex: 1,
241-
backgroundColor: colors.glass,
265+
backgroundColor: "rgba(255,255,255,0.06)",
242266
borderRadius: 14,
243-
padding: 14,
267+
padding: 16,
244268
borderWidth: 1,
245-
borderColor: colors.glassBorder,
269+
borderColor: "rgba(255,255,255,0.10)",
270+
minHeight: 80,
246271
},
247272
bookCardHovered: {
248273
backgroundColor: "rgba(138, 43, 226, 0.06)",
@@ -252,6 +277,29 @@ const styles = StyleSheet.create({
252277
backgroundColor: colors.purpleAccent,
253278
borderColor: colors.purpleBorder,
254279
},
280+
searchContainer: {
281+
flexDirection: "row",
282+
alignItems: "center",
283+
backgroundColor: "rgba(255,255,255,0.06)",
284+
borderWidth: 1,
285+
borderColor: "rgba(255,255,255,0.10)",
286+
borderRadius: 14,
287+
marginHorizontal: 20,
288+
marginBottom: 16,
289+
paddingHorizontal: 14,
290+
paddingVertical: 10,
291+
},
292+
searchIcon: { fontSize: 14, marginRight: 10 },
293+
searchInput: { flex: 1, color: "#F0F0F5", fontSize: 14 },
294+
clearIcon: { color: "#56566A", fontSize: 14, padding: 4 },
295+
bookAbbr: {
296+
color: "#A855F7",
297+
fontSize: 10,
298+
fontWeight: "700",
299+
letterSpacing: 1.5,
300+
marginBottom: 4,
301+
opacity: 0.6,
302+
},
255303
bookName: {
256304
fontSize: 14,
257305
fontFamily: fonts.uiMedium,

app/reader/[bookId]/[chapter].tsx

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
ActivityIndicator,
88
StyleSheet,
99
Platform,
10+
Share,
1011
} from "react-native";
1112
import { SafeAreaView } from "react-native-safe-area-context";
1213
import { useRouter, useLocalSearchParams } from "expo-router";
@@ -40,8 +41,10 @@ export default function ChapterReaderScreen() {
4041
const [loading, setLoading] = useState(true);
4142
const [error, setError] = useState<string | null>(null);
4243
const [scrollProgress, setScrollProgress] = useState(0);
44+
const [selectedVerse, setSelectedVerse] = useState<number | null>(null);
4345

4446
const scrollRef = useRef<ScrollView>(null);
47+
const lastScrollY = useRef(0);
4548

4649
const chapterNum = Number(chapter);
4750

@@ -81,6 +84,41 @@ export default function ChapterReaderScreen() {
8184
fetchChapter();
8285
}, [fetchChapter]);
8386

87+
const handleVerseCopy = async (v: Verse) => {
88+
const text = `${book?.name} ${chapterNum}:${v.verse} — "${v.content}"`;
89+
if (Platform.OS === "web" && typeof navigator !== "undefined" && navigator.clipboard) {
90+
await navigator.clipboard.writeText(text);
91+
}
92+
if (Platform.OS !== "web") {
93+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
94+
}
95+
setSelectedVerse(null);
96+
};
97+
98+
const handleVerseShare = async (v: Verse) => {
99+
const text = `${book?.name} ${chapterNum}:${v.verse} — "${v.content}"`;
100+
try {
101+
await Share.share({ message: text });
102+
} catch {}
103+
setSelectedVerse(null);
104+
};
105+
106+
const handleVerseBookmark = (_v: Verse) => {
107+
// TODO: Wire to Supabase user_verse_data table
108+
if (Platform.OS !== "web") {
109+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
110+
}
111+
setSelectedVerse(null);
112+
};
113+
114+
const handleAskAion = (v: Verse) => {
115+
const question = `Explain ${book?.name} ${chapterNum}:${v.verse} — "${v.content}"`;
116+
router.push({
117+
pathname: `/chat/new-${Date.now()}`,
118+
params: { initialMessage: question },
119+
});
120+
};
121+
84122
const handleBack = useCallback(() => {
85123
triggerHaptic();
86124
router.back();
@@ -195,10 +233,15 @@ export default function ChapterReaderScreen() {
195233
showsVerticalScrollIndicator={false}
196234
onScroll={(e) => {
197235
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
236+
const y = contentOffset.y;
198237
const totalHeight = contentSize.height - layoutMeasurement.height;
199238
if (totalHeight > 0) {
200-
setScrollProgress(Math.min(contentOffset.y / totalHeight, 1));
239+
setScrollProgress(Math.min(y / totalHeight, 1));
201240
}
241+
if (selectedVerse !== null && Math.abs(y - lastScrollY.current) > 20) {
242+
setSelectedVerse(null);
243+
}
244+
lastScrollY.current = y;
202245
}}
203246
scrollEventThrottle={16}
204247
>
@@ -213,9 +256,35 @@ export default function ChapterReaderScreen() {
213256
{/* Verse text — elegant flowing layout with paragraph grouping */}
214257
<View style={styles.verseBlock}>
215258
{verses.map((v, i) => (
216-
<View key={v.verse} style={[styles.verseLine, i === 0 && styles.verseLineFirst]}>
217-
<Text style={styles.verseNumber}>{v.verse}</Text>
218-
<Text style={styles.verseContent}>{v.content}</Text>
259+
<View key={v.verse}>
260+
<Pressable
261+
onPress={() => setSelectedVerse(selectedVerse === v.verse ? null : v.verse)}
262+
style={[
263+
styles.verseLine,
264+
i === 0 && styles.verseLineFirst,
265+
selectedVerse === v.verse && styles.verseLineSelected,
266+
i > 0 && i % 5 === 0 && styles.verseLineParagraph,
267+
]}
268+
>
269+
<Text style={styles.verseNumber}>{v.verse}</Text>
270+
<Text style={styles.verseContent}>{v.content}</Text>
271+
</Pressable>
272+
{selectedVerse === v.verse && (
273+
<Animated.View entering={FadeIn.duration(150)} style={styles.verseActions}>
274+
<Pressable onPress={() => handleVerseCopy(v)} style={styles.verseActionBtn}>
275+
<Text style={styles.verseActionText}>Copy</Text>
276+
</Pressable>
277+
<Pressable onPress={() => handleVerseBookmark(v)} style={styles.verseActionBtn}>
278+
<Text style={styles.verseActionText}>🔖 Bookmark</Text>
279+
</Pressable>
280+
<Pressable onPress={() => handleVerseShare(v)} style={styles.verseActionBtn}>
281+
<Text style={styles.verseActionText}>Share</Text>
282+
</Pressable>
283+
<Pressable onPress={() => handleAskAion(v)} style={[styles.verseActionBtn, styles.verseActionPrimary]}>
284+
<Text style={[styles.verseActionText, styles.verseActionPrimaryText]}>✦ Ask Aion</Text>
285+
</Pressable>
286+
</Animated.View>
287+
)}
219288
</View>
220289
))}
221290
</View>
@@ -487,4 +556,40 @@ const styles = StyleSheet.create({
487556
bottomButtonTextDisabled: {
488557
color: colors.textGhost,
489558
},
559+
verseLineSelected: {
560+
backgroundColor: "rgba(138, 43, 226, 0.08)",
561+
borderRadius: 8,
562+
marginHorizontal: -4,
563+
paddingHorizontal: 8,
564+
},
565+
verseLineParagraph: {
566+
marginTop: 16,
567+
},
568+
verseActions: {
569+
flexDirection: "row",
570+
flexWrap: "wrap",
571+
gap: 6,
572+
paddingVertical: 8,
573+
paddingHorizontal: 4,
574+
marginBottom: 4,
575+
},
576+
verseActionBtn: {
577+
paddingHorizontal: 12,
578+
paddingVertical: 6,
579+
borderRadius: 8,
580+
backgroundColor: "rgba(255,255,255,0.06)",
581+
borderWidth: 1,
582+
borderColor: "rgba(255,255,255,0.10)",
583+
},
584+
verseActionPrimary: {
585+
backgroundColor: "rgba(138, 43, 226, 0.12)",
586+
borderColor: "rgba(138, 43, 226, 0.25)",
587+
},
588+
verseActionText: {
589+
color: "#9494A8",
590+
fontSize: 12,
591+
},
592+
verseActionPrimaryText: {
593+
color: "#A855F7",
594+
},
490595
});

app/reader/[bookId]/index.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export default function ChapterListScreen() {
120120
<Text style={styles.sectionText}>SELECT CHAPTER</Text>
121121
<View style={styles.sectionLine} />
122122
</View>
123+
<Text style={styles.hintText}>Tap to start reading</Text>
123124

124125
{/* Chapter Grid */}
125126
<FlatList
@@ -136,7 +137,7 @@ export default function ChapterListScreen() {
136137
);
137138
}
138139

139-
const CELL_SIZE = 56;
140+
const CELL_SIZE = 64;
140141

141142
const styles = StyleSheet.create({
142143
container: {
@@ -209,7 +210,7 @@ const styles = StyleSheet.create({
209210
chapterCell: {
210211
width: CELL_SIZE,
211212
height: CELL_SIZE,
212-
borderRadius: 12,
213+
borderRadius: 14,
213214
backgroundColor: colors.glass,
214215
borderWidth: 1,
215216
borderColor: colors.glassBorder,
@@ -229,6 +230,12 @@ const styles = StyleSheet.create({
229230
fontFamily: fonts.uiMedium,
230231
color: colors.textPrimary,
231232
},
233+
hintText: {
234+
color: "#56566A",
235+
fontSize: 11,
236+
textAlign: "center",
237+
marginBottom: 12,
238+
},
232239
errorContainer: {
233240
flex: 1,
234241
alignItems: "center",

0 commit comments

Comments
 (0)