Overview
The Tafseer system consists of two main components:
- TafseerPopup: A bottom sheet modal container
- Tafseer: The content component displaying tafseer text with tab navigation
Props
Controls the visibility of the bottom sheet
setShow
React.Dispatch<React.SetStateAction<boolean>>
required
Function to update the visibility state
The verse (Aya) number to display tafseer for
The chapter (Surah) number
Implementation
TafseerPopup.tsx (lines 25-48)
export default function TafseerPopup({ show, setShow, aya, surah }: Props) {
const colorScheme = useColorScheme();
const tintColor = Colors[colorScheme ?? 'light'].tint;
const backgroundColor = Colors[colorScheme ?? 'light'].background;
const bottomSheetRef = useRef<BottomSheet>(null);
const [opacity, setOpacity] = useState(1);
const animatedPosition = useSharedValue(0);
useAnimatedReaction(
() => animatedPosition.value,
(currentValue) => {
const isResizing = currentValue % 1 !== 0;
runOnJS(setOpacity)(isResizing ? 0.8 : 1);
},
);
const snapPoints = useMemo(() => ['40%', '70%', '90%'], []);
const handleSheetChange = useCallback(
(index: number) => {
if (index === -1) setShow(false);
},
[setShow],
);
}
Snap Points
The bottom sheet has three snap points:
TafseerPopup.tsx (line 41)
const snapPoints = useMemo(() => ['40%', '70%', '90%'], []);
Initial view - Good for quick reference
Default position - Comfortable reading size
Expanded view - For detailed study
Resize Animation
The component provides visual feedback during resize:
TafseerPopup.tsx (lines 33-39)
useAnimatedReaction(
() => animatedPosition.value,
(currentValue) => {
const isResizing = currentValue % 1 !== 0;
runOnJS(setOpacity)(isResizing ? 0.8 : 1);
},
);
During resize (when position is not a whole number), opacity is reduced to 0.8 for better UX.
Backdrop Configuration
TafseerPopup.tsx (lines 50-61)
const renderBackdrop = useCallback(
(props: any) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
opacity={0.6}
onPress={() => bottomSheetRef.current?.close()}
/>
),
[],
);
Custom Handle
TafseerPopup.tsx (lines 63-72)
const renderHandle = useCallback(
() => (
<ThemedView style={styles.resizer}>
<ThemedView
style={[styles.resizerIcon, { backgroundColor: tintColor }]}
/>
</ThemedView>
),
[tintColor],
);
BottomSheet Render
TafseerPopup.tsx (lines 76-100)
<BottomSheet
ref={bottomSheetRef}
index={1}
snapPoints={snapPoints}
onChange={handleSheetChange}
backdropComponent={renderBackdrop}
handleComponent={renderHandle}
enablePanDownToClose
backgroundStyle={{ backgroundColor }}
animatedPosition={animatedPosition}
activeOffsetY={[-1, 1]}
>
<BottomSheetScrollView
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
>
<Suspense
fallback={<ActivityIndicator size="large" color={tintColor} />}
>
<Tafseer aya={aya} surah={surah} opacity={opacity} />
</Suspense>
</BottomSheetScrollView>
</BottomSheet>
Tafseer Component
Props
The verse number to display tafseer for
The surah (chapter) number
Optional opacity value for the content (default: undefined)
Tafseer Sources
The component supports 11 different tafseer sources:
Tafseer.tsx (lines 19-31)
const tabLabels: Partial<Record<TafseerTabs, string>> = {
katheer: 'إبن كثير',
maany: 'معاني القرآن',
earab: 'إعراب القرآن',
baghawy: 'البغوي',
muyassar: 'الميسر',
qortoby: 'القرطبي',
tabary: 'الطبري',
saady: 'السعدي',
wahidy: 'أسباب النزول',
tanweer: 'التحرير و التنوير',
waseet: 'الوسيط',
};
Ibn Katheer
Al-Tabari
Al-Qurtubi
Al-Saady
Others
Classical tafseer by Ismail ibn Katheer - One of the most respected commentaries
Early comprehensive tafseer by Ibn Jarir al-Tabari
Comprehensive tafseer focusing on legal rulings
Modern, concise tafseer by Abdur-Rahman al-Sa’di
Additional sources including grammar (I’rab), meanings, and reasons of revelation
State Management
Tafseer.tsx (lines 39-54)
export default function Tafseer({ aya, surah, opacity = undefined }: Props) {
const { tintColor, textColor } = useColors();
const { surahData } = useQuranMetadata();
const [surahName, setSurahName] = useState<string>('');
const [selectedTabValue, setSelectedTab] = useAtom(tafseerTab);
const [tafseerData, setTafseerData] = useState<TafseerAya[] | null>(null);
const [tabsWithContent, setTabsWithContent] = useState<
Record<TafseerTabs, boolean>
>({} as Record<TafseerTabs, boolean>);
const { specsData } = useQuranMetadata();
const { countBesmalAya } = specsData;
const formattedTafseerHtml = useTafseerContent({ tafseerData, surah, aya });
const isCurrentTabEmpty = hasNoTafseerContent({ tafseerData, surah, aya });
}
Dynamic Content Loading
Tafseer content is loaded dynamically based on the selected tab:
Tafseer.tsx (lines 151-200)
const loadTafseerData = useCallback(async () => {
let tafseerJSON;
try {
switch (selectedTabValue) {
case 'baghawy':
tafseerJSON = await import('@/assets/tafaseer/baghawy.json');
break;
case 'earab':
tafseerJSON = await import('@/assets/tafaseer/earab.json');
break;
case 'katheer':
tafseerJSON = await import('@/assets/tafaseer/katheer.json');
break;
case 'maany':
tafseerJSON = await import('@/assets/tafaseer/maany.json');
break;
case 'muyassar':
tafseerJSON = await import('@/assets/tafaseer/muyassar.json');
break;
case 'qortoby':
tafseerJSON = await import('@/assets/tafaseer/qortoby.json');
break;
case 'saady':
tafseerJSON = await import('@/assets/tafaseer/saady.json');
break;
case 'tabary':
tafseerJSON = await import('@/assets/tafaseer/tabary.json');
break;
case 'wahidy':
tafseerJSON = await import('@/assets/tafaseer/nozool-wahidy.json');
break;
case 'tanweer':
tafseerJSON = await import('@/assets/tafaseer/tanweer.json');
break;
case 'waseet':
tafseerJSON = await import('@/assets/tafaseer/waseet.json');
break;
default:
tafseerJSON = await import('@/assets/tafaseer/katheer.json');
}
setTafseerData(
(tafseerJSON.default as TafseerAya[]) || (tafseerJSON as TafseerAya[]),
);
} catch {
setTafseerData(null);
}
}, [selectedTabValue]);
Dynamic imports reduce initial bundle size and improve performance by loading only the selected tafseer.
Content Availability Detection
The component preloads all tafseer sources to detect which ones have content for the current verse:
Tafseer.tsx (lines 71-149)
useEffect(() => {
if (!allTafseersLoaded) {
const loadAllTafseers = async () => {
const tabsToCheck = Object.keys(tabLabels) as TafseerTabs[];
for (const tab of tabsToCheck) {
try {
let tafseerJSON;
// Import logic for each tafseer source...
const data =
(tafseerJSON?.default as TafseerAya[]) ||
(tafseerJSON as TafseerAya[]);
const hasContent = !hasNoTafseerContent({
tafseerData: data,
surah,
aya,
});
setTabsWithContent((prev) => ({
...prev,
[tab]: hasContent,
}));
} catch {
setTabsWithContent((prev) => ({
...prev,
[tab]: false,
}));
}
}
setAllTafseersLoaded(true);
};
loadAllTafseers();
}
}, [surah, aya, allTafseersLoaded]);
Tab Navigation
Tafseer.tsx (lines 213-253)
<ThemedView style={[styles.tabs, { backgroundColor: 'transparent' }]}>
{Object.keys(tabLabels).map((key) => {
const tabKey = key as TafseerTabs;
const isCurrentTab = tabKey === selectedTabValue;
const hasNoContent =
tabsWithContent[tabKey] === false ||
(isCurrentTab &&
isCurrentTabEmpty &&
tabsWithContent[tabKey] === undefined);
return (
<Pressable
key={tabKey}
style={[
styles.tabButton,
selectedTabValue === tabKey && styles.activeTab,
selectedTabValue === tabKey && {
borderColor: tintColor,
},
hasNoContent && styles.disabledTab,
{ backgroundColor: 'transparent' },
]}
onPress={() => setSelectedTab(tabKey)}
accessibilityLabel={`${tabLabels[tabKey]} tab for Surah ${surahName}, Aya ${aya}`}
accessibilityHint={`Tap to see the tafseer for Surah ${surahName}, Aya ${aya} from ${tabLabels[tabKey]}`}
>
<ThemedText
style={[
{ color: tintColor, backgroundColor: 'transparent' },
hasNoContent && styles.disabledTabText,
]}
>
{tabLabels[tabKey]}
</ThemedText>
</Pressable>
);
})}
</ThemedView>
Tabs without content for the current verse are displayed with reduced opacity and strikethrough text.
HTML Rendering
Tafseer content is rendered as HTML with custom styling:
Tafseer.tsx (lines 255-284)
{tafseerData ? (
<ThemedView style={{ flex: 1 }}>
{/* @ts-ignore - Ignoring type error for HTMLView component */}
<HTMLView
value={formattedTafseerHtml}
style={{
paddingHorizontal: 10,
paddingVertical: 5,
backgroundColor: 'transparent',
}}
stylesheet={{
div: {
color: textColor,
fontFamily: 'Tajawal_400Regular',
fontSize: 16,
lineHeight: 24,
backgroundColor: 'transparent',
},
p: {
color: textColor,
fontFamily: 'Tajawal_400Regular',
fontSize: 16,
lineHeight: 24,
flexDirection: 'row',
backgroundColor: 'transparent',
},
}}
addLineBreaks={false}
/>
</ThemedView>
) : (
<ActivityIndicator size="large" color={tintColor} />
)}
TypeScript Interfaces
TafseerPopup.tsx (lines 18-23)
type Props = {
show: boolean;
setShow: React.Dispatch<React.SetStateAction<boolean>>;
aya: number;
surah: number;
};
Tafseer Props
Tafseer.tsx (lines 33-37)
type Props = {
aya: number;
surah: number;
opacity?: number | undefined;
};
Styling
TafseerPopup.tsx (lines 103-119)
const styles = StyleSheet.create({
content: {
flexGrow: 1,
paddingHorizontal: 2,
paddingBottom: 2,
},
resizer: {
alignSelf: 'center',
paddingVertical: 10,
},
resizerIcon: {
width: 80,
height: 3,
borderRadius: 3,
alignSelf: 'center',
},
});
Tafseer Styles
Tafseer.tsx (lines 292-326)
const styles = StyleSheet.create({
container: {
flex: 1,
height: '100%',
backgroundColor: 'transparent',
padding: 5,
},
title: {
fontSize: 18,
marginBottom: 10,
padding: 8,
fontFamily: 'Amiri_400Regular',
},
tabs: {
flexDirection: 'row',
alignItems: 'center',
flexWrap: 'wrap',
gap: 2,
justifyContent: 'flex-start',
marginBottom: 10,
},
tabButton: {
paddingVertical: 10,
paddingHorizontal: 15,
},
activeTab: {
borderBottomWidth: 2,
},
disabledTab: {
opacity: 0.5,
},
disabledTabText: {
textDecorationLine: 'line-through',
},
});
- PageOverlay: Triggers the TafseerPopup when verses are tapped
- ThemedView/ThemedText: Provide theme-aware styling