├── .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 | npm version 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 | 72 | 73 | 79 | 85 | 86 |
74 | 75 | Fawaz Ahmed
76 | Fawaz Ahmed 77 |
78 |
80 | 81 | Nikolas Petriti
82 | Nikolas Petriti 83 |
84 |
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 | } --------------------------------------------------------------------------------