├── .gitattributes
├── screenshots
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── 6.png
├── feature.png
└── splashscreen.png
├── assets
├── img
│ ├── icon.png
│ ├── splash.png
│ ├── favicon.png
│ ├── ic_launcher.png
│ └── adaptive-icon.png
└── fonts
│ └── Pacifico-Regular.ttf
├── contexts
└── SSContexts.js
├── babel.config.js
├── react-native.config.js
├── metro.config.js
├── index.js
├── .gitignore
├── app.json
├── package.json
├── README.md
├── app
└── screens
│ ├── constants
│ └── MyStyles.js
│ ├── SettingsScreen.js
│ ├── HomeScreen.js
│ ├── FavoritesScreen.js
│ └── HadithsScreen.js
└── App.js
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 |
--------------------------------------------------------------------------------
/screenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/screenshots/1.png
--------------------------------------------------------------------------------
/screenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/screenshots/2.png
--------------------------------------------------------------------------------
/screenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/screenshots/3.png
--------------------------------------------------------------------------------
/screenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/screenshots/4.png
--------------------------------------------------------------------------------
/screenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/screenshots/5.png
--------------------------------------------------------------------------------
/screenshots/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/screenshots/6.png
--------------------------------------------------------------------------------
/assets/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/assets/img/icon.png
--------------------------------------------------------------------------------
/assets/img/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/assets/img/splash.png
--------------------------------------------------------------------------------
/assets/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/assets/img/favicon.png
--------------------------------------------------------------------------------
/screenshots/feature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/screenshots/feature.png
--------------------------------------------------------------------------------
/assets/img/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/assets/img/ic_launcher.png
--------------------------------------------------------------------------------
/assets/img/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/assets/img/adaptive-icon.png
--------------------------------------------------------------------------------
/contexts/SSContexts.js:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | export const SSContexts =createContext({});
--------------------------------------------------------------------------------
/screenshots/splashscreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/screenshots/splashscreen.png
--------------------------------------------------------------------------------
/assets/fonts/Pacifico-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/baqx/SunnahSnap/HEAD/assets/fonts/Pacifico-Regular.ttf
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo']
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/react-native.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | project: {
3 | ios: {},
4 | android: {},
5 | },
6 | assets: ['./assets'],
7 | };
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | // Learn more https://docs.expo.io/guides/customizing-metro
2 | const { getDefaultConfig } = require('expo/metro-config');
3 |
4 | /** @type {import('expo/metro-config').MetroConfig} */
5 | const config = getDefaultConfig(__dirname);
6 |
7 | module.exports = config;
8 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import { registerRootComponent } from 'expo';
2 |
3 | import App from './App';
4 |
5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
6 | // It also ensures that whether you load the app in Expo Go or in a native build,
7 | // the environment is set up appropriately
8 | registerRootComponent(App);
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "SunnahSnap",
4 | "slug": "Sunnah-Snap",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/img/icon.png",
8 | "userInterfaceStyle": "light",
9 | "splash": {
10 | "image": "./assets/img/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#6a3eb2"
13 | },
14 | "assetBundlePatterns": [
15 | "**/*"
16 | ],
17 | "plugins": [
18 | "expo-font"
19 | ],
20 | "ios": {
21 | "supportsTablet": true
22 | },
23 | "android": {
24 | "adaptiveIcon": {
25 | "foregroundImage": "./assets/img/adaptive-icon.png",
26 | "backgroundColor": "#ffffff",
27 | "image": "./assets/img/splash.png",
28 | "resizeMode": "contain"
29 | },
30 | "splash": {
31 | "image": "./assets/img/splash.png",
32 | "resizeMode": "contain",
33 | "backgroundColor": "#6a3eb2"
34 | },
35 | "package": "com.itsbgold.SunnahSnap"
36 | },
37 | "web": {
38 | "favicon": "./assets/img/favicon.png"
39 | }
40 |
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SunnahSnap",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "start": "expo start --dev-client",
7 | "android": "expo run:android",
8 | "ios": "expo run:ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "@expo/vector-icons": "^15.0.2",
13 | "@react-native-async-storage/async-storage": "^2.2.0",
14 | "@react-native-picker/picker": "2.11.1",
15 | "@react-navigation/bottom-tabs": "^6.5.20",
16 | "@react-navigation/native": "^6.1.17",
17 | "@react-navigation/native-stack": "^6.9.26",
18 | "babel-preset-expo": "^54.0.3",
19 | "expo": "54.0.15",
20 | "expo-font": "~14.0.9",
21 | "expo-status-bar": "~3.0.8",
22 | "expo-system-ui": "~6.0.7",
23 | "react": "19.1.0",
24 | "react-native": "0.81.4",
25 | "react-native-ico-material-design": "^5.1.2",
26 | "react-native-paper": "^3.12.0",
27 | "react-native-picker-select": "^9.1.3",
28 | "react-native-safe-area-context": "~5.6.0",
29 | "react-native-screens": "~4.16.0",
30 | "react-native-svg": "15.12.1",
31 | "react-native-vector-icons": "^10.0.3",
32 | "react-navigation-material-bottom-tabs": "^2.3.5"
33 | },
34 | "devDependencies": {
35 | "@babel/core": "^7.20.0"
36 | },
37 | "private": true
38 | }
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
SunnahSnap
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ## Introduction
14 |
15 | A platform where users can access Sunnah teachings or practices swiftly and conveniently, perhaps through short, easily digestible content or snippets.
16 |
17 | ## Requirements
18 |
19 | - NPM (Node Package Manager)
20 | - React-Native
21 | - Expo
22 |
23 | ## Features
24 |
25 | - View Hadiths of different books
26 | - Switch Hadiths Languages
27 |
28 | ### To do list
29 | - [x] Add multiple languages (Eng, Ara)
30 | - [ ] Add daily push notifications
31 | - [ ] Bookmarking Hadiths
32 | - [ ] Sharing Hadiths
33 | - [ ] Reactions to Hadiths
34 |
35 | ## Install all packages
36 |
37 | > npm install
38 |
39 | ## Screenshots
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | ## Contributors
70 |
71 |
87 |
--------------------------------------------------------------------------------
/app/screens/constants/MyStyles.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | export default StyleSheet.create({
4 | container: {
5 | flex:1,
6 | flexGrow:1,
7 | backgroundColor: '#f2f3f5',
8 | },
9 | headerContainer: {
10 | backgroundColor: '#6a3eb2',
11 | padding: 10,
12 | paddingTop:50,
13 | paddingRight:10,
14 | paddingLeft:10,
15 | borderBottomLeftRadius: 0,
16 | borderBottomRightRadius: 0,
17 | elevation: 10,
18 | },
19 | appTitle: {
20 | fontWeight:'bold',
21 | fontSize:18,
22 | color:'#FFFFE0'
23 | //fontFamily:'Pacifico-Regular',
24 | },
25 | appSubtitle: {
26 | fontWeight:'300',
27 | fontSize:14,
28 | color:'#FFFFE0'
29 | //fontFamily:'Pacifico-Regular',
30 | },
31 | recCard:{
32 | padding:20,
33 | backgroundColor:'#fff',
34 | margin:5,
35 | borderRadius:10
36 | },
37 | sectionTitle: {
38 | fontWeight:'600',
39 | fontSize:20,
40 | margin:10,
41 | marginBottom:5,
42 | },
43 | recCardTitle: {
44 | fontWeight:'400',
45 | fontSize:20,
46 | margin:2,
47 |
48 | },
49 | recCardContent: {
50 | fontWeight:'400',
51 | fontSize:16,
52 | margin:2,
53 |
54 | },
55 | recCardFoot: {
56 | fontWeight:'400',
57 | fontSize:15,
58 | margin:2,
59 | color:'grey',
60 |
61 | },
62 | search: {
63 | flexDirection: 'row',
64 | alignItems: 'center',
65 | backgroundColor: '#f1f1f1',
66 | borderRadius: 12,
67 | paddingHorizontal: 10,
68 | marginHorizontal: 16,
69 | marginVertical: 12,
70 | borderWidth: 1,
71 | borderColor: '#CCCCCC',
72 | },
73 | input: {
74 | flex: 1,
75 | height: 44, // Standard touch target size
76 | fontSize: 16,
77 | color: '#333',
78 | paddingVertical: 10,
79 | },
80 | icon: {
81 | marginRight: 8,
82 | },
83 | title: {
84 | fontSize: 18,
85 | },
86 | readMoreButton: {
87 | padding: 10,
88 | paddingRight: -10,
89 | paddingTop: -5,
90 | alignItems: 'flex-end',
91 | },
92 | readMoreText: {
93 | fontSize: 13,
94 | fontWeight: 'bold',
95 | color: '#6a3eb2',
96 | },
97 | pickerContainer: {
98 | height: 40,
99 | width: '100%',
100 | borderColor: 'gray',
101 | borderWidth: 1,
102 | borderRadius: 8,
103 | paddingHorizontal: 10,
104 | marginTop: 5,
105 | marginBottom: 5,
106 | justifyContent: 'center',
107 | fontWeight: 'bold',
108 | },
109 | inputIOS: {
110 | fontSize: 16,
111 | paddingVertical: 12,
112 | paddingHorizontal: 10,
113 | color: 'black',
114 | backgroundColor: 'white',
115 | },
116 | iconContainer: {
117 | top: 10,
118 | right: 12,
119 | },
120 | line: {
121 | height: 1,
122 | backgroundColor: '#CCCCCC',
123 | marginVertical: 10,
124 | },
125 | });
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState,useEffect } from 'react';
2 | import { NavigationContainer } from '@react-navigation/native';
3 | import { createNativeStackNavigator } from '@react-navigation/native-stack';
4 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
5 | import HomeScreen from './app/screens/HomeScreen';
6 | import HadithsScreen from './app/screens/HadithsScreen';
7 | import SettingsScreen from './app/screens/SettingsScreen';
8 | import FavoritesScreen from './app/screens/FavoritesScreen';
9 | import { Platform,Alert,StatusBar } from 'react-native';
10 | import AsyncStorage from '@react-native-async-storage/async-storage';
11 | import { SSContexts } from './contexts/SSContexts';
12 | import { Feather } from '@expo/vector-icons';
13 |
14 |
15 |
16 | const Stack = createNativeStackNavigator();
17 | const Tab = createBottomTabNavigator();
18 | const HomeTabs = () => {
19 | return (
20 |
28 | (
34 |
35 | ),
36 | }}
37 | />
38 | (
44 |
45 | ),
46 | }}
47 | />
48 | (
54 |
55 | ),
56 | }}
57 | />
58 |
59 | );
60 | };
61 |
62 | export default function App() {
63 | const [hadithBook, setHadithBook] = useState("bukhari");
64 | const [hadithLang, setHadithLang] = useState("eng");
65 | const [internet, setInternet] = useState(true);
66 | useEffect(() => {
67 | // Check if the book has been set before
68 | AsyncStorage.getItem('book')
69 | .then((book) => {
70 | if (book != null) {
71 | // If book exists, set it to hadithBook variable
72 | setHadithBook(book);
73 | }else{
74 | AsyncStorage.setItem('book', 'bukhari');
75 | setHadithBook('bukhari');
76 | }
77 | })
78 | .catch((error) => console.error('Error reading book:', error));
79 |
80 | }, []);
81 | useEffect(() => {
82 | // Check if the book has been set before
83 | AsyncStorage.getItem('lang')
84 | .then((lang) => {
85 | if (lang !== null) {
86 | // If book exists, set it to hadithBook variable
87 | setHadithLang(lang);
88 | }else{
89 | AsyncStorage.setItem('lang', 'eng');
90 | setHadithLang('eng');
91 | }
92 | })
93 | .catch((error) => console.error('Error reading lang:', error));
94 | }, []);
95 |
96 | async function checkInternetConnection() {
97 | try {
98 | const response = await fetch('https://www.google.com', { mode: 'no-cors' });
99 | if (response.status >= 200 && response.status < 300) {
100 | // Internet connection is available
101 | return true;
102 | } else {
103 | // Internet connection is not available
104 | return false;
105 | }
106 | } catch (error) {
107 | // Fetch failed, internet connection is not available
108 | return false;
109 | }
110 | }
111 |
112 | // Example usage:
113 | useEffect(() => {
114 | checkInternetConnection().then(hasInternet => {
115 | if (hasInternet) {
116 | setInternet(true)
117 |
118 | } else {
119 | setInternet(false)
120 | Alert.alert("No Internet","Please check your internet connection")
121 | }
122 | }) });
123 | return (
124 |
125 |
126 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | );
143 | }
--------------------------------------------------------------------------------
/app/screens/SettingsScreen.js:
--------------------------------------------------------------------------------
1 | import { useState,useContext } from 'react';
2 | import { View, Text, TouchableOpacity, Alert, ScrollView } from 'react-native';
3 | import styles from './constants/MyStyles.js';
4 | import RNPickerSelect from 'react-native-picker-select';
5 | import AsyncStorage from '@react-native-async-storage/async-storage';
6 | import { SSContexts } from '../../contexts/SSContexts.js';
7 | import { Feather } from '@expo/vector-icons';
8 | import { Platform } from 'react-native';
9 |
10 | export default function HadithsScreen() {
11 | const [selectedValue, setSelectedValue] = useState(null);
12 | const [selectedValue2, setSelectedValue2] = useState(null);
13 | const {hadithBook}=useContext(SSContexts);
14 | const {hadithLang}=useContext(SSContexts);
15 | const {setHadithBook}=useContext(SSContexts);
16 | const {setHadithLang}=useContext(SSContexts);
17 |
18 | const saveBook = async () => {
19 | try {
20 |
21 |
22 | if (selectedValue !== null) {
23 |
24 | await AsyncStorage.setItem('book', selectedValue);
25 | setHadithBook(selectedValue);
26 | Alert.alert( 'Done!','The new hadith book has been saved', );
27 | } else {
28 | Alert.alert( 'Alert!','Please select something', );
29 | }
30 | } catch (error) {
31 | console.error('Error saving book:', error);
32 | }
33 | };
34 | const saveLang = async () => {
35 | try {
36 |
37 |
38 | if (selectedValue2 !== null) {
39 |
40 | await AsyncStorage.setItem('lang', selectedValue2);
41 | setHadithLang(selectedValue2);
42 | Alert.alert( 'Done!','The new hadith language has been saved', );
43 | } else {
44 | Alert.alert( 'Alert!','Please select something', );
45 | }
46 | } catch (error) {
47 | console.error('Error saving book:', error);
48 | }
49 | };
50 |
51 | const placeholder = {
52 | label: hadithBook,
53 | value: null,
54 | };
55 | const placeholder2 = {
56 | label: hadithLang,
57 | value: null,
58 | };
59 | const options = [
60 | { label: 'Sunan Abu Dawud', value: 'abudawud' },
61 | { label: 'Musnad Imam Abu Hanifa', value: 'abuhanifa' },
62 | { label: 'Sahih al Bukhari', value: 'bukhari' },
63 | { label: 'Forty Hadith of Shah Waliullah Dehlawi', value: 'dehlawi' },
64 | { label: 'Sunan Ibn Majah', value: 'ibnmajah' },
65 | { label: 'Muwatta Malik', value: 'malik' },
66 | { label: 'Sahih Muslim', value: 'muslim' },
67 | { label: 'Sunan an Nasai', value: 'nasai' },
68 | { label: 'Forty Hadith of an-Nawawi', value: 'nawawi' },
69 | { label: 'Forty Hadith Qudsi', value: 'qudsi' },
70 | { label: 'Jami At Tirmidhi', value: 'tirmidhi' },
71 | ];
72 |
73 | //array for the hadith language option
74 | const options2 = [
75 | { label: 'English', value: 'eng' },
76 | { label: 'Arabic', value: 'ara' },
77 | ];
78 |
79 | return (
80 |
81 |
82 | SunnahSnap
83 | Sayings of Prophet Muhammad (ﷺ)
84 |
85 |
86 | Settings
87 |
88 | Select a Hadith Book
89 | {Platform.OS === 'ios' ? (
90 | {
94 | return ;
95 | }}
96 | placeholder={placeholder}
97 | items={options}
98 | onValueChange={(value) => setSelectedValue(value)}
99 | value={selectedValue}
100 | />
101 | ) : (
102 | setSelectedValue(value)}
107 | value={selectedValue}
108 | />
109 | )}
110 | {/* {selectedValue && Selected: {selectedValue}} */}
111 |
112 |
113 | Change
114 |
115 |
116 |
117 |
118 | Select a Language for the hadiths
119 | {Platform.OS === 'ios' ? ((
120 | {
124 | return ;
125 | }}
126 | placeholder={placeholder2}
127 | items={options2}
128 | onValueChange={(value) => setSelectedValue2(value)}
129 | value={selectedValue2}
130 | />
131 | )) : (
132 | setSelectedValue2(value)}
137 | value={selectedValue2}
138 | />
139 | )}
140 | {/* {selectedValue2 && Selected: {selectedValue2}} */}
141 |
142 |
143 | Change
144 |
145 |
146 |
147 |
148 | Developer Information
149 | This app was made with love by BAQDEV
150 | Check out my github profile @ https://github.com/baqx
151 | Hire me for your App and Web development projects - Whatsapp(+2349019659410)
152 | Facebook Profile - https://web.facebook.com/baqeecodes
153 |
154 |
155 | Copyright {new Date().getFullYear()}
156 |
157 |
158 |
159 | );
160 | }
161 |
--------------------------------------------------------------------------------
/app/screens/HomeScreen.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useContext, useCallback, useRef } from 'react';
2 | import AsyncStorage from '@react-native-async-storage/async-storage';
3 | import {
4 | Text,
5 | TextInput,
6 | View,
7 | TouchableOpacity,
8 | ActivityIndicator,
9 | StatusBar,
10 | ScrollView,
11 | Share,
12 | Alert
13 | } from 'react-native';
14 | import styles from './constants/MyStyles.js';
15 | import { useNavigation, useFocusEffect } from '@react-navigation/native';
16 | import { SSContexts } from '../../contexts/SSContexts.js';
17 | import { Feather } from '@expo/vector-icons';
18 | import { Ionicons } from '@expo/vector-icons';
19 |
20 | export default function HomeScreen() {
21 | const { hadithBook, hadithLang } = useContext(SSContexts);
22 |
23 | const [sections, setSections] = useState([]);
24 | const [backup, setBackup] = useState([]);
25 | // const [sectionNo, setSectionNo] = useState(1);
26 | const [loading, setLoading] = useState(true);
27 | const [hadithData, setHadithData] = useState(null);
28 | const [searchText, setSearchText] = useState("");
29 | const [showRandom, setShowRandom] = useState(false)
30 | const [headerHeight, setHeaderHeight] = useState(0)
31 |
32 | const headerRef = useRef(null)
33 |
34 | const navigation = useNavigation();
35 |
36 | const url =
37 | 'https://cdn.jsdelivr.net/gh/fawazahmed0/hadith-api@1/editions/eng-' +
38 | hadithBook +
39 | '.json';
40 |
41 | useEffect(() => {
42 | const fetchData = async () => {
43 | try {
44 | setLoading(true);
45 | const response = await fetch(url);
46 | if (!response.ok) {
47 | throw new Error('Network response was not ok');
48 | }
49 | const json = await response.json();
50 |
51 | setSections(
52 | Object.entries(json.metadata.sections).filter(([key]) => key !== '0')
53 | );
54 | setBackup(
55 | Object.entries(json.metadata.sections).filter(([key]) => key !== '0')
56 | );
57 | } catch (error) {
58 | console.error('Error fetching data:', error);
59 | } finally {
60 | setLoading(false);
61 | }
62 | };
63 |
64 | fetchData();
65 | }, [hadithBook, hadithLang]);
66 |
67 | // --- Effect to Fetch Random Hadith Data ---
68 | // Generate a random number only once on component mount
69 | const [randomNumber] = useState(getRandomNumber(1, 40));
70 |
71 | useEffect(() => {
72 | fetch(
73 | 'https://cdn.jsdelivr.net/gh/fawazahmed0/hadith-api@1/editions/eng-' +
74 | hadithBook +
75 | '/' +
76 | randomNumber +
77 | '.min.json'
78 | )
79 | .then((response) => response.json())
80 | .then((data) => {
81 | const sectionKey = Object.keys(data.metadata.section)[0];
82 | const sectionName = data.metadata.section[sectionKey];
83 | const reference = data.hadiths[0].reference;
84 |
85 | setHadithData({
86 | sectionName: `Section ${sectionKey}, ${sectionName}`,
87 | hadithNumber: data.hadiths[0].hadithnumber,
88 | hadithText: data.hadiths[0].text,
89 | hadithReference: {
90 | book: reference.book,
91 | hadith: reference.hadith,
92 | },
93 | });
94 | })
95 | .catch((error) => console.error('Error fetching data:', error));
96 | }, [hadithBook, randomNumber]);
97 |
98 | useEffect(() => {
99 | if (searchText !== "") {
100 | setSections(backup.filter((arr) => arr[1].toLowerCase().includes(searchText.toLowerCase())))
101 | } else {
102 | setSections(backup)
103 | }
104 | }, [searchText])
105 |
106 | // --- Helper Functions ---
107 |
108 | const onHeaderLayout = () => {
109 | headerRef.current.measure((x, y, width, height, pageX, pageY) => {
110 | setHeaderHeight(height);
111 | })
112 | }
113 |
114 | const goToHadiths = (sid) => {
115 | navigation.navigate('Hadiths', { sectionNo: sid });
116 | };
117 |
118 | function getRandomNumber(min, max) {
119 | return Math.floor(Math.random() * (max - min + 1)) + min;
120 | }
121 |
122 | const goToSettings = () => {
123 | navigation.navigate('Settings');
124 | };
125 |
126 | const updateStorage = async (hadithNumber, text) => {
127 | const key = `${hadithBook}:${hadithNumber}`
128 | try {
129 | const item = await AsyncStorage.getItem(key);
130 |
131 | if (item !== null) {
132 | return await AsyncStorage.removeItem(key);
133 | } else {
134 | return await AsyncStorage.setItem(key, text);
135 | }
136 | } catch (e) {
137 | console.error('Error saving data:', e);
138 | }
139 | }
140 |
141 | const checkItem = async (hadithNumber) => {
142 | const key = `${hadithBook}:${hadithNumber}`
143 | try {
144 | const item = await AsyncStorage.getItem(key);
145 |
146 | return item !== null
147 | } catch (e) {
148 | console.error('Error fetching data:', e);
149 |
150 | return false
151 | }
152 | }
153 |
154 | const onShare = async (text, book, number) => {
155 | try {
156 | const result = await Share.share({
157 | title: `${book.toUpperCase()}, ${number}`,
158 | subject: `${book.toUpperCase()}, ${number}`,
159 | dialogTitle: `${book.toUpperCase()}, ${number}`,
160 | message: `${text}.
161 | - ${book.toUpperCase()}, ${number}`,
162 | });
163 |
164 | if (result.action === Share.sharedAction) {
165 | if (result.activityType) {
166 | console.log(`Shared with: ${result.activityType}`);
167 | } else {
168 | console.log('Content shared successfully');
169 | }
170 | } else if (result.action === Share.dismissedAction) {
171 | console.log('Share dialog dismissed');
172 | }
173 | } catch (error) {
174 | Alert.alert(error.message);
175 | }
176 | };
177 |
178 | // --- Components ---
179 |
180 | const RandomHadithsCard = ({
181 | sectionName,
182 | hadithNumber,
183 | hadithText,
184 | hadithReference,
185 | }) => {
186 |
187 | const [isSaved, setIsSaved] = useState(false);
188 |
189 | const checkStatus = async () => {
190 | const exists = await checkItem(hadithNumber);
191 | setIsSaved(exists);
192 | };
193 |
194 | useEffect(() => {
195 | checkStatus();
196 | }, [randomNumber, hadithNumber]);
197 |
198 | useFocusEffect(
199 | useCallback(() => {
200 | checkStatus();
201 | return () => {};
202 | }, [])
203 | );
204 |
205 | return (
206 |
207 | {sectionName}
208 | {hadithText}.
209 | {!showRandom ?
210 | setShowRandom(true)} style={styles.readMoreButton}>
211 | Read More
212 |
213 | :
214 | setShowRandom(false)} style={styles.readMoreButton}>
215 | Show Less
216 |
217 | }
218 |
219 |
220 | No {hadithNumber}
221 |
222 |
223 | Book {hadithReference.book}, Hadith {hadithReference.hadith}
224 |
225 |
226 |
227 | onShare(hadithText, hadithBook, hadithNumber)}>
228 |
229 |
230 | updateStorage(hadithNumber, hadithText).then(() => checkStatus())}>
231 | {isSaved ?
232 |
233 | :
234 |
235 | }
236 |
237 |
238 |
239 |
240 | );
241 | };
242 |
243 | // --- Main Render ---
244 |
245 | return (
246 |
247 | {loading ? (
248 |
249 | ) : (
250 |
251 |
252 |
253 | SunnahSnap
254 | Sayings of Prophet Muhammad (ﷺ)
255 |
256 |
257 | Featured Hadith
258 | {hadithData && (
259 |
265 | )}
266 |
267 |
268 | Featured Topics ({hadithBook.charAt(0).toUpperCase() + hadithBook.slice(1)})
269 |
270 | {/*
271 |
272 | */}
273 |
274 |
275 |
282 |
283 |
284 |
285 | {sections.map((item) => (
286 | goToHadiths(item[0])} style={styles.recCard}>
287 |
288 | {item[1]}
289 |
290 |
291 | ))}
292 |
293 |
294 |
295 | )}
296 |
297 | );
298 | }
--------------------------------------------------------------------------------
/app/screens/FavoritesScreen.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback, useRef } from 'react';
2 | import { useFocusEffect } from '@react-navigation/native';
3 | import AsyncStorage from '@react-native-async-storage/async-storage';
4 | import {
5 | View,
6 | Text,
7 | ScrollView,
8 | TextInput,
9 | Share,
10 | TouchableOpacity,
11 | Alert,
12 | StatusBar,
13 | } from 'react-native';
14 | import styles from './constants/MyStyles.js';
15 | import { Feather } from '@expo/vector-icons';
16 | import { Ionicons } from '@expo/vector-icons';
17 |
18 | const splitAndHighlightRecursive = (str, searchWord) => {
19 | const lowerStr = str.toLowerCase();
20 | const lowerSearchWord = searchWord.toLowerCase();
21 | const searchWordLength = searchWord.length;
22 |
23 | const index = lowerStr.indexOf(lowerSearchWord);
24 |
25 | if (index === -1) {
26 | return str.length > 0 ? [str] : [];
27 | }
28 |
29 | const startIndex = index;
30 | const endIndex = index + searchWordLength;
31 |
32 | const part1 = str.slice(0, startIndex);
33 |
34 | const part2 = str.slice(startIndex, endIndex);
35 |
36 | const part3 = str.slice(endIndex);
37 |
38 | const remainingParts = splitAndHighlightRecursive(part3, searchWord);
39 |
40 | const result = [];
41 | if (part1.length > 0) result.push(part1);
42 | result.push(part2);
43 | result.push(...remainingParts);
44 |
45 | return result;
46 | };
47 |
48 | const onShare = async (text, book, number) => {
49 | try {
50 | const result = await Share.share({
51 | title: `${book.toUpperCase()}, ${number}`,
52 | subject: `${book.toUpperCase()}, ${number}`,
53 | dialogTitle: `${book.toUpperCase()}, ${number}`,
54 | message: `${text}.
55 | - ${book.toUpperCase()}, ${number}`,
56 | });
57 |
58 | if (result.action === Share.sharedAction) {
59 | if (result.activityType) {
60 | console.log(`Shared with: ${result.activityType}`);
61 | } else {
62 | console.log('Content shared successfully');
63 | }
64 | } else if (result.action === Share.dismissedAction) {
65 | console.log('Share dialog dismissed');
66 | }
67 | } catch (error) {
68 | Alert.alert(error.message);
69 | }
70 | };
71 |
72 | const HadithItem = ({ number, text, book, searchWord, searching, onRemove }) => {
73 | let parts = [];
74 | const wordExists = text.toLowerCase().includes(searchWord.toLowerCase());
75 |
76 | if (searching && wordExists && searchWord.length > 1) {
77 | parts = splitAndHighlightRecursive(text, searchWord);
78 | }
79 |
80 | const shouldHighlight = parts.length > 1;
81 |
82 | const [isSaved, setIsSaved] = useState(false);
83 |
84 | const itemKey = `${book}:${number}`;
85 |
86 | const checkSavedStatus = async () => {
87 | try {
88 | const item = await AsyncStorage.getItem(itemKey);
89 | setIsSaved(item !== null);
90 | } catch (e) {
91 | console.error('Error checking saved status:', e);
92 | setIsSaved(false);
93 | }
94 | };
95 |
96 | const toggleSave = async () => {
97 | try {
98 | if (isSaved) {
99 | await AsyncStorage.removeItem(itemKey);
100 | onRemove();
101 | setIsSaved(false);
102 | console.log(`Removed item: ${itemKey}`);
103 | } else {
104 | await AsyncStorage.setItem(itemKey, text);
105 | setIsSaved(true);
106 | console.log(`Saved item: ${itemKey}`);
107 | }
108 | } catch (e) {
109 | console.error('Error toggling save status:', e);
110 | Alert.alert("Error", "Could not update saved status.");
111 | }
112 | };
113 |
114 | useEffect(() => {
115 | checkSavedStatus();
116 | }, [itemKey]);
117 |
118 | return (
119 |
120 |
121 | {book.toUpperCase()}, {number}
122 |
123 |
124 | {shouldHighlight ? (
125 | <>
126 | {parts.map((part, i) => {
127 | if (parts.length === 2 && parts[0].length < parts[1].length) {
128 | if (i === 0) {
129 | return {part}
130 | } else {
131 | return {part}
132 | }
133 | };
134 | if (i % 2 === 0) {
135 | return {part}
136 | }
137 | else {
138 | return {part}
139 | }
140 | })}
141 | >
142 | ) : (
143 | text
144 | )}.
145 |
146 |
147 |
148 |
149 | No. {number}
150 |
151 |
152 | onShare(text, book, number)}>
153 |
154 |
155 |
156 | {isSaved ?
157 |
158 | :
159 |
160 | }
161 |
162 |
163 |
164 |
165 | );
166 | };
167 |
168 |
169 | const getFormattedBookItems = async () => {
170 | try {
171 | const allKeys = await AsyncStorage.getAllKeys();
172 | const bookKeys = allKeys.filter(key => {
173 | const parts = key.split(':');
174 | return parts.length === 2 && parts[0].length > 0 && parts[1].length > 0;
175 | });
176 |
177 | const bookPairs = await AsyncStorage.multiGet(bookKeys);
178 |
179 | const formattedData = bookPairs.map(([key, textValue]) => {
180 | const [bookName, hadithNumberStr] = key.split(':');
181 |
182 | const hadithNumber = parseInt(hadithNumberStr, 10);
183 |
184 | return {
185 | book: bookName,
186 | hadith: hadithNumber,
187 | text: textValue,
188 | };
189 | });
190 | return formattedData;
191 | } catch (error) {
192 | console.error("Error retrieving and formatting book items:", error);
193 | return [];
194 | }
195 | };
196 |
197 | const clearAsyncStorage = async () => {
198 | try {
199 | await AsyncStorage.clear();
200 | } catch (e) {
201 | console.error("Failed to clear AsyncStorage:", e);
202 | }
203 | };
204 |
205 |
206 | export default function FavoritesScreen() {
207 |
208 | const [hadiths, setHadiths] = useState([])
209 | const [allHadiths, setAllHadiths] = useState([])
210 | const [searchText, setSearchText] = useState("")
211 | const [searching, setSearching] = useState(false)
212 | const [headerHeight, setHeaderHeight] = useState(0)
213 |
214 | const headerRef = useRef(null)
215 |
216 | const onHeaderLayout = () => {
217 | headerRef.current.measure((x, y, width, height, pageX, pageY) => {
218 | setHeaderHeight(height);
219 | })
220 | }
221 |
222 | const removeItem = (idToRemove) => {
223 | setHadiths(prevData => prevData.filter(item => item.hadith !== idToRemove));
224 | setHadiths(prevData => prevData.filter(item => item.hadith !== idToRemove));
225 | };
226 |
227 | const loadHadiths = async () => {
228 | try {
229 | const fetchedHadiths = await getFormattedBookItems();
230 | setHadiths(fetchedHadiths);
231 | setAllHadiths(fetchedHadiths);
232 | } catch (error) {
233 | console.error("Failed to load hadiths:", error);
234 | }
235 | };
236 |
237 | useEffect(() => {
238 | if (searchText !== "") {
239 | setSearching(true)
240 | setHadiths(allHadiths.filter((hadith) => hadith.text.toLowerCase().includes(searchText.toLowerCase())))
241 | } else {
242 | setSearching(false)
243 | setHadiths(allHadiths)
244 | }
245 | }, [searchText])
246 |
247 | useEffect(() => {
248 | //clearAsyncStorage()
249 | getFormattedBookItems().then((result) => {
250 | setHadiths(result)
251 | setAllHadiths(result)
252 | })
253 |
254 | }, [])
255 |
256 | useFocusEffect(
257 | useCallback(() => {
258 | loadHadiths();
259 | return () => {
260 | };
261 | }, [])
262 | );
263 |
264 | return (
265 |
266 |
267 |
268 | SunnahSnap
269 | Sayings of Prophet Muhammad (ﷺ)
270 |
271 |
272 | Favorite Hadiths
273 |
274 |
281 |
282 |
283 | {hadiths.length === 0 ?
284 | You don't have any favorite hadith!
285 | :
286 |
287 | {hadiths.map((item, i) => (
288 | removeItem(item.hadith)}
296 | />
297 | ))}
298 |
299 | }
300 |
301 |
302 | )
303 | }
--------------------------------------------------------------------------------
/app/screens/HadithsScreen.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useContext, useCallback } from 'react'; // Added useCallback
2 | import AsyncStorage from '@react-native-async-storage/async-storage';
3 | import {
4 | View,
5 | Text,
6 | FlatList,
7 | TextInput,
8 | ActivityIndicator,
9 | Share,
10 | TouchableOpacity,
11 | Alert,
12 | } from 'react-native';
13 | import styles from './constants/MyStyles.js';
14 | import Icon from 'react-native-ico-material-design';
15 | import { useNavigation, useRoute } from '@react-navigation/native';
16 | import { SSContexts } from '../../contexts/SSContexts.js';
17 | import { Feather } from '@expo/vector-icons';
18 | import { Ionicons } from '@expo/vector-icons';
19 |
20 | const splitAndHighlightRecursive = (str, searchWord) => {
21 | const lowerStr = str.toLowerCase();
22 | const lowerSearchWord = searchWord.toLowerCase();
23 | const searchWordLength = searchWord.length;
24 |
25 | const index = lowerStr.indexOf(lowerSearchWord);
26 |
27 | if (index === -1) {
28 | return str.length > 0 ? [str] : [];
29 | }
30 |
31 | const startIndex = index;
32 | const endIndex = index + searchWordLength;
33 |
34 | const part1 = str.slice(0, startIndex);
35 |
36 | const part2 = str.slice(startIndex, endIndex);
37 |
38 | const part3 = str.slice(endIndex);
39 |
40 | const remainingParts = splitAndHighlightRecursive(part3, searchWord);
41 |
42 | const result = [];
43 | if (part1.length > 0) result.push(part1);
44 | result.push(part2);
45 | result.push(...remainingParts);
46 |
47 | return result;
48 | };
49 |
50 | const onShare = async (text, book, number) => {
51 | try {
52 | const result = await Share.share({
53 | title: `${book.toUpperCase()}, ${number}`,
54 | subject: `${book.toUpperCase()}, ${number}`,
55 | dialogTitle: `${book.toUpperCase()}, ${number}`,
56 | message: `${text}.
57 | - ${book.toUpperCase()}, ${number}`,
58 | });
59 |
60 | if (result.action === Share.sharedAction) {
61 | if (result.activityType) {
62 | console.log(`Shared with: ${result.activityType}`);
63 | } else {
64 | console.log('Content shared successfully');
65 | }
66 | } else if (result.action === Share.dismissedAction) {
67 | console.log('Share dialog dismissed');
68 | }
69 | } catch (error) {
70 | Alert.alert(error.message);
71 | }
72 | };
73 |
74 | const HadithItem = ({ hadithNumber, text, book, hadith, searchWord, searching, lang, bookName }) => {
75 |
76 | let parts = [];
77 | const wordExists = text.toLowerCase().includes(searchWord.toLowerCase());
78 |
79 | if (searching && wordExists && searchWord.length > 1) {
80 | parts = splitAndHighlightRecursive(text, searchWord);
81 | }
82 |
83 | const shouldHighlight = parts.length > 1;
84 |
85 | const [isSaved, setIsSaved] = useState(false);
86 |
87 | const itemKey = `${bookName}:${hadithNumber}`;
88 |
89 | const checkSavedStatus = async () => {
90 | try {
91 | const item = await AsyncStorage.getItem(itemKey);
92 | setIsSaved(item !== null);
93 | } catch (e) {
94 | console.error('Error checking saved status:', e);
95 | setIsSaved(false);
96 | }
97 | };
98 |
99 | const toggleSave = async () => {
100 | try {
101 | if (isSaved) {
102 | await AsyncStorage.removeItem(itemKey);
103 | setIsSaved(false);
104 | console.log(`Removed item: ${itemKey}`);
105 | } else {
106 | await AsyncStorage.setItem(itemKey, text);
107 | setIsSaved(true);
108 | console.log(`Saved item: ${itemKey}`);
109 | }
110 | } catch (e) {
111 | console.error('Error toggling save status:', e);
112 | Alert.alert("Error", "Could not update saved status.");
113 | }
114 | };
115 |
116 | useEffect(() => {
117 | checkSavedStatus();
118 | }, [itemKey]);
119 |
120 | return (
121 |
122 |
123 | Book {book}, Hadith {hadith}
124 |
125 |
126 | {shouldHighlight ? (
127 | <>
128 | {parts.map((part, i) => {
129 | if (parts.length === 2 && parts[0].length < parts[1].length) {
130 | if (i === 0) {
131 | return {part}
132 | } else {
133 | return {part}
134 | }
135 | };
136 | if (i % 2 === 0) {
137 | return {part}
138 | }
139 | else {
140 | return {part}
141 | }
142 | })}
143 | >
144 | ) : (
145 | text
146 | )}{lang === "eng" ? "." : ""}
147 |
148 |
149 |
150 |
151 | No. {hadithNumber}
152 |
153 |
154 | onShare(text, bookName, hadithNumber)}>
155 |
156 |
157 |
158 | {isSaved ?
159 |
160 | :
161 |
162 | }
163 |
164 |
165 |
166 |
167 | );
168 | };
169 |
170 | // --- Main Screen Component ---
171 | export default function HadithsScreen() {
172 | const route = useRoute();
173 | const navigation = useNavigation();
174 |
175 | const { hadithBook, hadithLang } = useContext(SSContexts);
176 |
177 | const { sectionNo } = route.params;
178 |
179 | const [hadiths, setHadiths] = useState([]);
180 | const [allHadiths, setAllHadiths] = useState([])
181 | const [searchText, setSearchText] = useState("")
182 | const [searching, setSearching] = useState(false)
183 | const [loading, setLoading] = useState(false);
184 | const [pageNumber, setPageNumber] = useState(1);
185 | const [totalPages, setTotalPages] = useState(1);
186 | const [hasError, setHasError] = useState(false);
187 | const [metadata, setMetadata] = useState({});
188 |
189 | const fetchData = useCallback(async () => {
190 | if (searching) return;
191 |
192 | if (pageNumber > totalPages && hadiths.length > 0) return;
193 |
194 | if (pageNumber === 1 && hadiths.length > 0) {
195 | setHadiths([]);
196 | }
197 |
198 | try {
199 | setLoading(true);
200 | setHasError(false);
201 |
202 | const url = `https://cdn.jsdelivr.net/gh/fawazahmed0/hadith-api@1/editions/${hadithLang}-${hadithBook}/sections/${sectionNo}.json`;
203 |
204 | const response = await fetch(url);
205 |
206 | if (!response.ok) {
207 | throw new Error('Network response was not ok');
208 | }
209 |
210 | const data = await response.json();
211 |
212 | if (pageNumber === 1) {
213 | setMetadata(data.metadata);
214 | setTotalPages(Math.ceil(data.hadiths.length / 20));
215 | }
216 |
217 | const startIndex = (pageNumber - 1) * 20;
218 | const endIndex = pageNumber * 20;
219 | const newHadiths = data.hadiths.slice(startIndex, endIndex);
220 | const all = data.hadiths
221 | setAllHadiths(all)
222 |
223 | setHadiths((prevHadiths) => [...prevHadiths, ...newHadiths]);
224 | } catch (error) {
225 | console.error('Error fetching data:', error);
226 | setHasError(true);
227 | if (pageNumber > 1) {
228 | setPageNumber(1);
229 | }
230 | } finally {
231 | setLoading(false);
232 | }
233 | }, [pageNumber, hadithLang, hadithBook, sectionNo, totalPages, hadiths.length]);
234 |
235 | useEffect(() => {
236 | setHadiths([]);
237 | setPageNumber(1);
238 | setTotalPages(1);
239 | setHasError(false);
240 | setLoading(false);
241 |
242 | fetchData();
243 | }, [hadithBook, hadithLang, sectionNo]);
244 |
245 | useEffect(() => {
246 | if (pageNumber > 1) {
247 | fetchData();
248 | }
249 | }, [pageNumber, fetchData]);
250 |
251 | useEffect(() => {
252 | if (searchText !== "") {
253 | setSearching(true)
254 | setHadiths(allHadiths.filter((hadith) => hadith.text.toLowerCase().includes(searchText.toLowerCase())))
255 | } else {
256 | setSearching(false)
257 | setHadiths(allHadiths)
258 | }
259 | }, [searchText])
260 |
261 | const loadMoreData = () => {
262 | if (!loading && pageNumber < totalPages) {
263 | setPageNumber((prevPageNumber) => prevPageNumber + 1);
264 | }
265 | };
266 |
267 | const backButton = () => {
268 | navigation.navigate('Home');
269 | };
270 |
271 | // --- Render Functions ---
272 |
273 | // const renderFooter = () => {
274 | // if (loading) {
275 | // if (hadiths.length > 0) {
276 | // return ;
277 | // }
278 | // return null;
279 | // }
280 | //
281 | // if (hasError) {
282 | // return (
283 | //
284 | // Something went wrong! What can you do?
285 | // 1. Check your internet connection.
286 | //
287 | //
288 | // 2. Try changing the language of the hadith book you are using to Arabic, some books are available only in Arabic.{' '}
289 | //
290 | //
291 | //
292 | //
293 | //
294 | //
295 | // );
296 | // }
297 |
298 | // return null;
299 | // };
300 |
301 | // const renderHeader = () => {
302 | //
303 | // const sectionTitle =
304 | // metadata.section && Object.keys(metadata.section).length > 0
305 | // ? `${Object.keys(metadata.section)[0]}: ${metadata.section[Object.keys(metadata.section)[0]]}`
306 | // : 'Loading Section...';
307 |
308 | // return (
309 | //
310 | //
311 | //
312 | //
313 | //
314 | //
315 | // {metadata.name && SunnahSnap - {metadata.name}}
316 | // Section {sectionTitle}
317 | //
318 | //
319 | //
320 | // );
321 | // };
322 |
323 | const sectionTitle =
324 | metadata.section && Object.keys(metadata.section).length > 0
325 | ? `${Object.keys(metadata.section)[0]}: ${metadata.section[Object.keys(metadata.section)[0]]}`
326 | : 'Loading Section...';
327 |
328 | // --- Main Render ---
329 | if (loading && hadiths.length === 0) {
330 | return (
331 |
332 |
333 |
334 | );
335 | }
336 |
337 | return (
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 | {metadata.name && SunnahSnap - {metadata.name}}
346 | Section {sectionTitle}
347 |
348 |
349 |
350 |
351 |
358 |
359 |
360 | `${item.hadithnumber}-${index}`}
363 | renderItem={({ item }) => (
364 |
374 | )}
375 | onEndReached={loadMoreData}
376 | onEndReachedThreshold={0.2}
377 | // ListFooterComponent={renderFooter} // Footer is obsolete.
378 | // ListHeaderComponent={renderHeader} // I had to comment out both the Footer and Header separate components and instead render them directly within the main component's render method because their previous setup was interfering with the searchText state.
379 | // stickyHeaderIndices is useful here to keep the title visible
380 | // stickyHeaderIndices={[0]}
381 | />
382 |
383 | );
384 | }
--------------------------------------------------------------------------------