Skip to main content

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

Props Interface

PageNavigator.tsx (lines 18-24)
interface PageNavigatorProps {
  currentPage: number;
  totalPages: number;
  onPageChange: (page: number) => void;
  primaryColor: string;
  iconColor: string;
}
currentPage
number
required
The currently active page number
totalPages
number
required
Total number of pages (typically 604 for the Quran)
onPageChange
(page: number) => void
required
Callback function when a page is selected
primaryColor
string
required
Primary theme color for active states
iconColor
string
required
Color for icons in the navigator

Pagination Logic

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 logic
PageNavigator.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>

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);
};

Input UI

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;
}
currentSurah
number
required
Currently selected Surah number (1-114)
currentAya
number
required
Currently selected Aya number
ayaCount
number[]
required
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}
/>

TopMenu Component

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,
);

Header Information

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>
Displays the current Surah with Tashkeel removed for cleaner display

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

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

Build docs developers (and LLMs) love