Overview
The navigation system consists of three main components:
- PageNavigator: Handles page-based navigation with pagination
- SurahAyaNavigator: Provides Surah and Aya selection
- TopMenu: Main app menu with progress tracking and quick actions
PageNavigator Component
Props Interface
PageNavigator.tsx (lines 18-24)
interface PageNavigatorProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
primaryColor: string;
iconColor: string;
}
The currently active page number
Total number of pages (typically 604 for the Quran)
onPageChange
(page: number) => void
required
Callback function when a page is selected
Primary theme color for active states
Color for icons in the navigator
The component intelligently displays page numbers with ellipsis:
PageNavigator.tsx (lines 43-57)
const getPageNumbers = () => {
const pages: (number | string)[] = [];
for (let i = 1; i <= totalPages; i++) {
if (
i === 1 ||
i === totalPages ||
(i >= currentPage - range && i <= currentPage + range)
) {
pages.push(i);
} else if (pages[pages.length - 1] !== '...') {
pages.push('...');
}
}
return pages;
};
Always shows first page, last page, and a range around the current page. Ellipsis inserted for gaps.
Responsive Design
The component adapts to different screen sizes:
PageNavigator.tsx (lines 32-34)
const { width } = useWindowDimensions();
const range = getPaginationRange(width);
const isCompact = isCompactView(width);
Shows scrollable page numbers with the pagination logicPageNavigator.tsx (lines 115-147)
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={[
styles.pageNumbersContainer,
{ flexGrow: 1 },
]}
>
{getPageNumbers().map((page, index) => (
<TouchableOpacity
key={index}
style={[
styles.pageNumber,
{ backgroundColor: cardColor },
page === currentPage && styles.currentPageNumber,
page === currentPage && { backgroundColor: primaryColor },
page === '...' && styles.ellipsis,
]}
onPress={() => handlePageNumberPress(page)}
disabled={page === '...'}
>
<ThemedText
style={[
styles.pageNumberText,
page === currentPage && styles.currentPageNumberText,
]}
>
{page}
</ThemedText>
</TouchableOpacity>
))}
</ScrollView>
Shows only current page number with tap-to-editPageNavigator.tsx (lines 148-164)
<ThemedView style={styles.compactContainer}>
<TouchableOpacity
style={[
styles.compactPageIndicator,
{ borderColor: primaryColor },
]}
onPress={toggleInput}
>
<ThemedText
style={[styles.compactPageText, { color: primaryColor }]}
>
{currentPage}
</ThemedText>
</TouchableOpacity>
</ThemedView>
Direct Page Input
Users can input a specific page number:
PageNavigator.tsx (lines 65-78)
const handleInputChange = (text: string) => {
const numericValue = text.replace(/[^0-9]/g, '');
setInputValue(numericValue);
};
const handleInputSubmit = () => {
const pageNumber = parseInt(inputValue, 10);
if (!isNaN(pageNumber) && pageNumber >= 1 && pageNumber <= totalPages) {
onPageChange(pageNumber);
} else {
setInputValue(currentPage.toString());
}
setShowInput(false);
};
PageNavigator.tsx (lines 89-112)
{showInput ? (
<ThemedView style={[styles.inputContainer, { flex: 1 }]}>
<TextInput
style={[
styles.pageInput,
{ color: primaryColor, borderColor: primaryColor },
width < 600 && { minWidth: 40, paddingHorizontal: 2 },
]}
value={inputValue}
onChangeText={handleInputChange}
keyboardType="numeric"
maxLength={4}
autoFocus
onBlur={handleInputSubmit}
onSubmitEditing={handleInputSubmit}
accessibilityLabel="Page number input"
/>
<TouchableOpacity
style={styles.submitButton}
onPress={handleInputSubmit}
>
<Feather name="check" size={18} color={primaryColor} />
</TouchableOpacity>
</ThemedView>
) : (
// ... page numbers display
)}
SurahAyaNavigator Component
Props Interface
SurahAyaNavigator.tsx (lines 13-22)
interface SurahAyaNavigatorProps {
currentSurah: number;
currentAya: number;
ayaCount: number[];
onSurahChange: (surahNumber: number) => void;
onAyaChange: (ayaNumber: number) => void;
primaryColor: string;
iconColor: string;
cardColor: string;
}
Currently selected Surah number (1-114)
Currently selected Aya number
Array of available Aya numbers for the current Surah
onSurahChange
(surahNumber: number) => void
required
Callback when a Surah is selected
onAyaChange
(ayaNumber: number) => void
required
Callback when an Aya is selected
Dual Selector Layout
SurahAyaNavigator.tsx (lines 114-158)
<ThemedView style={[styles.container, { backgroundColor: 'transparent' }]}>
{/* Surah Selector */}
<ThemedView
style={[styles.selectorContainer, { backgroundColor: 'transparent' }]}
>
<TouchableOpacity
style={[styles.selector, { borderColor: primaryColor }]}
onPress={() => setSurahModalVisible(true)}
accessibilityLabel="اختر سورة"
accessibilityHint="اضغط لفتح قائمة السور"
>
<ThemedText style={[styles.selectorText, { color: primaryColor }]}>
{currentSurahName}
</ThemedText>
<Feather name="chevron-down" size={18} color={primaryColor} />
</TouchableOpacity>
</ThemedView>
{/* Separator */}
<ThemedView
style={[
styles.separator,
{
backgroundColor: 'transparent',
borderColor: primaryColor + '40',
borderLeftWidth: 1,
borderRightWidth: 1,
borderStyle: 'dotted',
},
]}
/>
{/* Aya Selector */}
<ThemedView
style={[styles.selectorContainer, { backgroundColor: 'transparent' }]}
>
<TouchableOpacity
style={[styles.selector, { borderColor: primaryColor }]}
onPress={() => setAyaModalVisible(true)}
accessibilityLabel="اختر آية"
accessibilityHint="اضغط لفتح قائمة الآيات"
>
<ThemedText style={[styles.selectorText, { color: primaryColor }]}>
{currentAya}
</ThemedText>
<Feather name="chevron-down" size={18} color={primaryColor} />
</TouchableOpacity>
</ThemedView>
</ThemedView>
Surah Modal
Displays a searchable list of all Surahs:
SurahAyaNavigator.tsx (lines 71-98)
const renderSurahItem = ({ item }: { item: Surah }) => (
<TouchableOpacity
style={[
styles.modalItem,
item.number === currentSurah && {
backgroundColor: primaryColor + '20',
},
]}
onPress={() => handleSurahSelect(item.number)}
accessibilityLabel={`سورة ${item.name}`}
>
<ThemedText style={styles.surahNumber}>{item.number}</ThemedText>
<ThemedView
style={[
styles.separator,
{
backgroundColor: 'transparent',
borderColor: primaryColor + '40',
borderLeftWidth: 1,
borderRightWidth: 1,
borderStyle: 'dotted',
},
]
/>
<ThemedText style={styles.surahName}>{item.name}</ThemedText>
<ThemedText style={styles.surahInfo}>{item.numberOfAyahs} آية</ThemedText>
</TouchableOpacity>
);
Optimized FlatList
Both modals use getItemLayout for performance:
SurahAyaNavigator.tsx (lines 186-197)
<FlatList
data={surahData}
renderItem={renderSurahItem}
keyExtractor={(item) => item.number.toString()}
showsVerticalScrollIndicator={isWeb}
initialScrollIndex={currentSurah - 1}
getItemLayout={(_data, index) => ({
length: 60,
offset: 60 * index,
index,
})}
/>
Using getItemLayout allows immediate scrolling to the current selection without rendering all items.
Aya Grid Layout
Ayas are displayed in a 5-column grid:
SurahAyaNavigator.tsx (lines 100-111)
const renderAyaItem = ({ item }: { item: number }) => (
<TouchableOpacity
style={[
styles.ayaItem,
item === currentAya && { backgroundColor: primaryColor + '20' },
]}
onPress={() => handleAyaSelect(item)}
accessibilityLabel={`آية ${item}`}
>
<ThemedText style={styles.ayaNumber}>{item}</ThemedText>
</TouchableOpacity>
);
SurahAyaNavigator.tsx (lines 228-241)
<FlatList
data={ayaCount}
renderItem={renderAyaItem}
keyExtractor={(item) => item.toString()}
showsVerticalScrollIndicator={isWeb}
numColumns={5}
initialScrollIndex={Math.floor((currentAya - 1) / 5)}
getItemLayout={(_data, index) => ({
length: 50,
offset: 50 * Math.floor(index / 5),
index,
})}
contentContainerStyle={styles.ayaGrid}
/>
Features
- Current Surah name display
- Juz and Thumn position indicator
- Daily progress tracker with circular progress
- Navigation to different app sections
- Bottom menu toggle
Progress Display
TopMenu.tsx (lines 42-51)
useEffect(() => {
const newProgress =
dailyTrackerGoalValue > 0
? Math.min(
1,
dailyTrackerCompletedValue.value / 8 / (dailyTrackerGoalValue / 8),
)
: 0;
setProgressValue(newProgress);
}, [dailyTrackerGoalValue, dailyTrackerCompletedValue.value]);
Circular Progress Indicator
TopMenu.tsx (lines 125-148)
{!isTemporary && (
<TouchableOpacity
style={styles.icon}
onPress={() => {
setShowTopMenuState(false);
router.push('/tracker');
}}
>
<View style={styles.progressContainer}>
<Progress.Circle
size={26}
progress={progressValue}
color={tintColor}
showsText={false}
thickness={3.5}
borderWidth={0}
unfilledColor={'rgba(128, 128, 128, 0.4)'}
/>
{progressValue === 1 && (
<View style={styles.checkmarkContainer}>
<Feather name="check" size={16} color={tintColor} />
</View>
)}
</View>
</TouchableOpacity>
)}
A checkmark appears inside the circle when daily goal is completed (progress === 1).
Surah and Juz Display
TopMenu.tsx (lines 61-67)
const currentPage = page ? parseInt(page) : currentSavedPageValue;
const isTemporary = temporary === 'true';
const currentSurahName = getSurahNameByPage(surahData, currentPage);
const { thumnInJuz, juzNumber } = getJuzPositionByPage(
thumnData,
currentPage,
);
TopMenu.tsx (lines 79-121)
<View style={styles.rightSection}>
<Text
style={[styles.surahName, { color: tintColor }]}
accessibilityLabel={`السورة الحالية: ${currentSurahName}`}
accessibilityRole="header"
>
{removeTashkeel(currentSurahName)}
</Text>
<View style={styles.secondLineContainer}>
<Text style={[styles.juzPosition, { color: tintColor }]}>
الجزء - {juzNumber}
</Text>
<View style={styles.positionContainer}>
<Text style={[styles.thumnPosition, { color: tintColor }]}>
{thumnInJuz}
</Text>
<Text
style={[
styles.thumnSeparator,
{
color: tintColor,
includeFontPadding: false,
textAlignVertical: 'center',
},
]}
>
/
</Text>
<Text
style={[
styles.thumnTotal,
{
color: tintColor,
includeFontPadding: false,
textAlignVertical: 'center',
},
]}
>
16
</Text>
</View>
</View>
</View>
Surah Name
Juz Position
Thumn Position
Displays the current Surah with Tashkeel removed for cleaner display
Shows current Juz number (1-30)
Shows position within the Juz (1-16 of 16 Thumns per Juz)
Quick Actions
TopMenu.tsx (lines 123-195)
<ThemedView style={styles.leftIconsContainer}>
{/* Daily Tracker - only shown for non-temporary pages */}
{!isTemporary && (
<TouchableOpacity
style={styles.icon}
onPress={() => {
setShowTopMenuState(false);
router.push('/tracker');
}}
>
{/* Progress Circle */}
</TouchableOpacity>
)}
{/* Navigation */}
<TouchableOpacity
style={styles.icon}
onPress={() => {
setShowTopMenuState(false);
router.push('/navigation');
}}
>
<Ionicons
name="navigate-circle-outline"
size={ICON_SIZE}
color={tintColor}
/>
</TouchableOpacity>
{/* Search */}
<TouchableOpacity
style={styles.icon}
onPress={() => {
setShowTopMenuState(false);
router.push('/search');
}}
>
<Ionicons name="search" size={ICON_SIZE} color={tintColor} />
</TouchableOpacity>
{/* Toggle Bottom Menu */}
<TouchableOpacity
style={styles.icon}
onPress={() => {
setShowTopMenuState(false);
toggleMenu();
}}
>
{showBottomMenuState ? (
<MaterialCommunityIcons
name="fit-to-screen-outline"
size={ICON_SIZE}
color={tintColor}
/>
) : (
<MaterialIcons
name="fullscreen-exit"
size={ICON_SIZE}
color={tintColor}
/>
)}
</TouchableOpacity>
</ThemedView>
Visibility Control
TopMenu.tsx (lines 69, 198)
return showTopMenuState ? (
<ThemedView style={styles.container}>
{/* Menu content */}
</ThemedView>
) : null;
The menu visibility is controlled by the topMenuState atom, allowing it to be toggled from anywhere in the app.
Common Patterns
Modal Overlay Pattern
Both SurahAyaNavigator modals use this pattern:
SurahAyaNavigator.tsx (lines 167-199)
<Modal
animationType="slide"
transparent={true}
visible={surahModalVisible}
onRequestClose={() => setSurahModalVisible(false)}
>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPress={() => setSurahModalVisible(false)}
>
<ThemedView
style={[styles.modalContent, { backgroundColor: cardColor }]}
onStartShouldSetResponder={() => true}
>
{/* Modal content */}
</ThemedView>
</TouchableOpacity>
</Modal>
onStartShouldSetResponder={() => true} prevents touch events from propagating to the overlay when interacting with modal content.
Accessibility
All navigation components include proper accessibility labels:
accessibilityLabel="اختر سورة"
accessibilityHint="اضغط لفتح قائمة السور"
- BottomMenu: Complementary bottom navigation bar
- ThemedView/ThemedText: Provide consistent theming
- useQuranMetadata: Hook for Surah and Juz data