USER
bu koddaki bazı fonsiyonları farklı sayfalara taşıyıp dışarıdan çekelim:
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { View, StyleSheet, Text, FlatList, Image, TouchableOpacity, ActivityIndicator, BackHandler } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import WordExplanationBox from './WordExplanationBox';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Font from 'expo-font';
import { useThemeAndLanguage } from '../utils/ThemeAndLanguageContext';
import { BottomSheetModal, BottomSheetModalProvider, BottomSheetScrollView } from '@gorhom/bottom-sheet';
import QuizScreen from './QuizScreen';
import { doc, getDoc } from 'firebase/firestore';
import { db } from '../utils/firebase';
import { useFocusEffect } from '@react-navigation/native';
import { useVocabulary } from './VocabularyContext';
import PropTypes from 'prop-types';
import ToggleHeartButton from './heart';
const ARABIC_FONTS = {
'Scheherazade': require('../../assets/fonts/scheherazade.ttf'),
'Adobe Arabic': require('../../assets/fonts/adobearabic.ttf'),
'Amiri': require('../../assets/fonts/amiri.ttf'),
'Noto Naskh Arabic': require('../../assets/fonts/noto.ttf'),
};
const STORAGE_KEY = '@user_words';
const Word = React.memo(({ word, onPress, style, isKnown }) => (
<TouchableOpacity onPress={() => onPress(word)}>
<Text style={[style, isKnown && styles.knownWord]}>{word}</Text>
</TouchableOpacity>
));
Word.propTypes = {
word: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
style: PropTypes.object,
isKnown: PropTypes.bool,
};
const Sentence = React.memo(({ sentenceObj, selectedSentence, handleSentenceButtonPress, handleWordPress, fontSize, theme, arabicFont, knownWords }) => {
if (sentenceObj.type === 'image') {
return (
<Image
source={{ uri: sentenceObj.content }}
style={[styles.imageStyle]}
/>
);
}
const isHeaderOrSubHeader = sentenceObj.type === 'header' || sentenceObj.type === 'subheader';
return (
<View style={[
styles.sentenceContainer,
selectedSentence === sentenceObj.content && styles.selectedSentence,
isHeaderOrSubHeader && (sentenceObj.type === 'header' ? styles.headerContainer : styles.subHeaderContainer),
{
borderRightColor: sentenceObj.type === 'header' ? theme.primaryColor : theme.secondaryColor
} ]}>
<TouchableOpacity style={styles.explanationButton} onPress={() => handleSentenceButtonPress(sentenceObj.content)}>
<Ionicons name="open" size={24} color={theme.primaryColor} />
</TouchableOpacity>
<View style={styles.arabicTextWrapper}>
{sentenceObj.content.split(' ').map((word, wordIndex) => (
<Word
key={`${sentenceObj.id}-${wordIndex}`}
word={word}
onPress={handleWordPress}
style={[
styles.descriptionArabic,
{
fontSize: isHeaderOrSubHeader ? fontSize * 1.6 : fontSize * 1.4,
color: theme.textColor,
fontFamily: arabicFont,
}
]}
isKnown={knownWords.has(word)}
/>
))}
</View>
</View>
);
});
Sentence.propTypes = {
sentenceObj: PropTypes.shape({
type: PropTypes.string,
content: PropTypes.string,
id: PropTypes.string,
}).isRequired,
selectedSentence: PropTypes.string,
handleSentenceButtonPress: PropTypes.func.isRequired,
handleWordPress: PropTypes.func.isRequired,
fontSize: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired,
arabicFont: PropTypes.string.isRequired,
knownWords: PropTypes.instanceOf(Set).isRequired,
};
export default function ReadStoryScreen({ route, navigation }) {
const { story, storyId } = route.params;
const [fontSize, setFontSize] = useState(18);
const [selectedWord, setSelectedWord] = useState(null);
const [selectedSentence, setSelectedSentence] = useState(null);
const [loading, setLoading] = useState(true);
const [cachedStory, setCachedStory] = useState(null);
const [hasQuiz, setHasQuiz] = useState(false);
const bottomSheetModalRef = useRef(null);
const settingsBottomSheetModalRef = useRef(null);
const quizBottomSheetModalRef = useRef(null);
const { locale, theme, arabicFont, setFontFamily, t } = useThemeAndLanguage();
const [activeModal, setActiveModal] = useState(null);
const [knownWords, setKnownWords] = useState(new Set());
const { vocabulary, isLoggedIn, userId} = useVocabulary();
const increaseFontSize = useCallback(() => setFontSize(prevSize => prevSize + 2), []);
const decreaseFontSize = useCallback(() => setFontSize(prevSize => prevSize - 2), []);
const closeAllModals = useCallback(() => {
bottomSheetModalRef.current?.dismiss();
settingsBottomSheetModalRef.current?.dismiss();
quizBottomSheetModalRef.current?.dismiss();
setActiveModal(null);
}, []);
const openSettingsModal = useCallback(() => {
settingsBottomSheetModalRef.current?.present();
setActiveModal('settings');
}, []);
const openQuizModal = useCallback(() => {
quizBottomSheetModalRef.current?.present();
setActiveModal('quiz');
}, []);
const handleWordPress = useCallback((word) => {
setSelectedWord(word);
setSelectedSentence(null);
bottomSheetModalRef.current?.present();
setActiveModal('word');
}, []);
const handleSentenceButtonPress = useCallback((sentence) => {
setSelectedSentence(sentence);
setSelectedWord(null);
bottomSheetModalRef.current?.present();
setActiveModal('sentence');
}, []);
const loadFonts = useCallback(async () => {
await Font.loadAsync(ARABIC_FONTS);
}, []);
const fetchStoryFromFirestore = useCallback(async (id) => {
const storyDocRef = doc(db, 'stories', id);
const storyDoc = await getDoc(storyDocRef);
return storyDoc.exists() ? { id: storyDoc.id, ...storyDoc.data() } : null;
}, []);
const fetchStory = useCallback(async () => {
try {
let storyData;
if (story) {
storyData = story;
} else if (storyId) {
const cachedData = await AsyncStorage.getItem(`story-${storyId}`);
if (cachedData) {
storyData = JSON.parse(cachedData);
} else {
storyData = await fetchStoryFromFirestore(storyId);
if (storyData) {
await AsyncStorage.setItem(`story-${storyId}`, JSON.stringify(storyData));
}
}
}
if (storyData) {
setCachedStory(storyData);
setHasQuiz(storyData.test_questions && storyData.test_questions.length > 0);
}
} catch (error) {
console.error('Error fetching story:', error);
} finally {
setLoading(false);
}
}, [story, storyId, fetchStoryFromFirestore]);
const fetchKnownWords = useCallback(async () => {
if (!isLoggedIn) {
setKnownWords(new Set());
return;
}
try {
const cachedWords = await AsyncStorage.getItem(STORAGE_KEY);
if (cachedWords) {
const wordsArray = JSON.parse(cachedWords);
setKnownWords(new Set(wordsArray.map(word => word.word)));
}
} catch (error) {
console.error('Error fetching known words:', error);
}
}, [isLoggedIn]);
useEffect(() => {
const initialize = async () => {
await loadFonts();
await fetchStory();
await fetchKnownWords();
};
initialize();
}, [loadFonts, fetchStory, fetchKnownWords]);
useEffect(() => {
if (cachedStory) {
const storyTitle = locale === 'tr' ? cachedStory.storytitleturkish : cachedStory.storytitleenglish;
const truncatedTitle = storyTitle.length > 23 ? `${storyTitle.substring(0, 23)}...` : storyTitle;
navigation.setOptions({
title: truncatedTitle,
headerStyle: { backgroundColor: theme.lighterCardBackgroundColor },
headerTintColor: theme.textColor,
headerTitleStyle: { color: theme.textColor, fontWeight: 'bold' },
headerRight: () => (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<ToggleHeartButton storyId={cachedStory.id} initialHeartCount={cachedStory.hearts} />
<TouchableOpacity onPress={openSettingsModal} style={styles.fontSizeControl}>
<Ionicons name="settings-outline" size={24} color={theme.textColor} />
</TouchableOpacity>
</View>
),
});
}
}, [navigation, cachedStory, theme, locale, openSettingsModal]);
useFocusEffect(
useCallback(() => {
const onBackPress = () => {
if (activeModal) {
closeAllModals();
return true;
}
return false;
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress);
}, [activeModal, closeAllModals])
);
const renderSentence = useCallback(({ item: sentenceObj }) => (
<Sentence
sentenceObj={sentenceObj}
selectedSentence={selectedSentence}
handleSentenceButtonPress={handleSentenceButtonPress}
handleWordPress={handleWordPress}
fontSize={fontSize}
theme={theme}
arabicFont={arabicFont}
knownWords={knownWords}
/>
), [selectedSentence, handleSentenceButtonPress, handleWordPress, fontSize, theme, arabicFont, knownWords]);
const memoizedData = useMemo(() => cachedStory?.sentences || [], [cachedStory]);
const keyExtractor = useCallback((item) => item.id.toString(), []);
useEffect(() => {
const updatedKnownWords = new Set(vocabulary.map(word => word.word));
setKnownWords(updatedKnownWords);
}, [vocabulary]);
if (loading) {
return (
<View style={[styles.loadingContainer, { backgroundColor: theme.backgroundColorForWordBox }]}>
<ActivityIndicator size="large" color={theme.primaryColor} />
</View>
);
}
if (!cachedStory) {
return (
<View style={[styles.loadingContainer, { backgroundColor: theme.backgroundColorForWordBox }]}>
<Text style={{ color: theme.textColor }}>{t('storyNotFound')}</Text>
</View>
);
}
return (
<BottomSheetModalProvider>
<View style={[styles.container, { backgroundColor: theme.backgroundColorForWordBox }]}>
<FlatList
data={memoizedData}
renderItem={renderSentence}
keyExtractor={keyExtractor}
ListHeaderComponent={() => (
<>
<Image source={{ uri: cachedStory.thumbnail }} style={styles.thumbnail} />
<View style={styles.content}>
<View style={styles.header}>
<Text style={[styles.titleArabic, { color: theme.textColor, fontFamily: arabicFont }]}>{cachedStory.storytitlearabic}</Text>
<Text style={[styles.titleTurkish, { color: theme.secondaryColor }]}>
{locale === 'tr' ? cachedStory.storytitleturkish : cachedStory.storytitleenglish}
</Text>
</View>
</View>
</>
)}
initialNumToRender={10}
maxToRenderPerBatch={5}
windowSize={10}
/>
{hasQuiz && (
<TouchableOpacity
style={[styles.quizButton, { backgroundColor: theme.primaryColor }]}
onPress={openQuizModal}
>
<Text style={styles.quizButtonText}>{t('startQuiz')}</Text>
</TouchableOpacity>
)}
<BottomSheetModal
ref={bottomSheetModalRef}
index={0}
snapPoints={['45%', '90%']}
handleStyle={{ backgroundColor: theme.backgroundColorForWordBox }}
onDismiss={() => setActiveModal(null)}
>
<BottomSheetScrollView contentContainerStyle={styles.bottomSheetContent}>
<WordExplanationBox
word={selectedWord || selectedSentence}
storyId={cachedStory.id}
onClose={() => bottomSheetModalRef.current?.dismiss()}
/>
</BottomSheetScrollView>
</BottomSheetModal>
<BottomSheetModal
ref={settingsBottomSheetModalRef}
index={0}
snapPoints={['85%']}
handleStyle={{ backgroundColor: theme.backgroundColorForWordBox }}
onDismiss={() => setActiveModal(null)}
>
<BottomSheetScrollView
contentContainerStyle={[
styles.settingsContent,
{ backgroundColor: theme.backgroundColorForWordBox }
]}
>
<View style={styles.exampleCard}>
<Text style={[styles.exampleText, { fontSize: fontSize * 1.4, fontFamily: arabicFont }]}>
(هذا مثال على الكتابة بالعربية)
</Text>
</View>
<View style={styles.fontSizeControls}>
<TouchableOpacity onPress={increaseFontSize} style={styles.fontSizeButton}>
<Ionicons name="add-circle-outline" size={40} color={theme.primaryColor} />
<Text style={styles.fontSizeButtonText}>{t('increaseFontSize')}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={decreaseFontSize} style={styles.fontSizeButton}>
<Ionicons name="remove-circle-outline" size={40} color={theme.primaryColor} />
<Text style={styles.fontSizeButtonText}>{t('decreaseFontSize')}</Text>
</TouchableOpacity>
</View>
<View style={styles.fontOptionsContainer}>
{Object.keys(ARABIC_FONTS).map((font) => (
<TouchableOpacity
key={font}
style={styles.fontOption}
onPress={() => setFontFamily(font)}
>
<Text style={styles.fontOptionText}>{font}</Text>
<Text style={[styles.fontOptionSample, { fontFamily: font }]}>
(اقرأ العربية)
</Text>
</TouchableOpacity>
))}
</View>
</BottomSheetScrollView>
</BottomSheetModal>
{hasQuiz && (
<BottomSheetModal
ref={quizBottomSheetModalRef}
index={0}
snapPoints={['90%']}
handleStyle={{ backgroundColor: theme.backgroundColorForWordBox }}
onDismiss={() => setActiveModal(null)}
>
<QuizScreen
story={cachedStory}
onClose={() => {
quizBottomSheetModalRef.current?.dismiss();
setActiveModal(null);
}}
/>
</BottomSheetModal>
)}
</View>
</BottomSheetModalProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
knownWord: {
backgroundColor: 'rgba(0, 255, 0, 0.1)',
borderRadius: 10,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
thumbnail: {
width: '100%',
aspectRatio:1/1,
},
content: {
padding: 16,
},
header: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},
titleArabic: {
fontSize: 26,
},
titleTurkish: {
fontSize: 20,
marginTop: 4,
textAlign: 'center',
},
headerRight: {
flexDirection: 'row',
alignItems: 'center',
marginRight: 10,
},
fontSizeControl: {
marginHorizontal: 5,
},
sentenceContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
paddingLeft: 10,
},
explanationButton: {
marginRight: 2,
},
arabicTextWrapper: {
flex: 1,
flexDirection: 'row-reverse',
flexWrap: 'wrap',
paddingRight: 15,
},
descriptionArabic: {
marginLeft: 2,
padding: 2,
},
selectedSentence: {
backgroundColor: 'rgba(255, 215, 0, 0.3)',
},
settingsContent: {
flexGrow: 1,
paddingBottom: 20,
},
fontExampleContainer: {
alignItems: 'center',
marginBottom: 20,
},
exampleLabel: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
},
exampleBox: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 10,
padding: 10,
width: '80%',
alignItems: 'center',
backgroundColor: 'white',
elevation: 4,
},
fontSizeControls: {
flexDirection: 'row',
justifyContent: 'space-between',
marginVertical: 25,
paddingHorizontal: 25,
width: '80%',
alignSelf: 'center',
},
fontSizeControl: {
alignItems: 'center',
padding: 10,
},
fontSizeControlText: {
marginTop: 5,
fontSize: 14,
color: 'gray',
},
selectFontLabel: {
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 10,
},
fontGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-around',
marginTop: 20,
width: '80%',
alignSelf: 'center',
},
fontOptionGridItem: {
width: '45%',
padding: 5,
marginBottom: 20,
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 10,
alignItems: 'center',
backgroundColor: 'white',
elevation: 4,
},
fontOptionText: {
fontSize: 16,
marginBottom: 5,
textAlign: 'center',
},
quizButton: {
display: 'flex', // Flexbox ile genişliği içeriğe göre yap
justifyContent: 'center', // İçeriği ortala
alignItems: 'center', // İçeriği dikeyde ortala
padding: 10,
},
quizButtonText: {
color: 'white',
fontWeight: 'bold',
},
bottomSheetContent: {
flexGrow: 1,
},
settingsContent: {
flexGrow: 1,
paddingHorizontal: 20,
paddingBottom: 20,
alignItems: 'center',
},
exampleCard: {
backgroundColor: 'white',
borderRadius: 10,
padding: 15,
marginVertical: 20,
width: '90%',
alignItems: 'center',
elevation: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
exampleText: {
textAlign: 'center',
},
fontSizeControls: {
flexDirection: 'row',
justifyContent: 'space-around',
width: '100%',
marginBottom: 20,
},
fontSizeButton: {
alignItems: 'center',
},
fontSizeButtonText: {
marginTop: 5,
fontSize: 12,
color: 'gray',
textAlign: 'center',
},
fontOptionsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
width: '100%',
},
fontOption: {
width: '48%',
backgroundColor: 'white',
borderRadius: 10,
padding: 10,
marginBottom: 15,
alignItems: 'center',
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.22,
shadowRadius: 2.22,
},
fontOptionText: {
fontSize: 16,
marginBottom: 5,
},
fontOptionSample: {
fontSize: 18,
},
headerContainer: {
marginVertical: 10,
backgroundColor: 'rgba(0, 0, 0, 0.03)',
borderRightWidth: 5,
width: '97%', // Genişliği biraz küçülttük
},
subHeaderContainer: {
marginVertical: 10,
backgroundColor: 'rgba(0, 0, 0, 0.03)',
borderRightWidth: 3,
width: '97%',
},
imageStyle: {
width: "90%",
alignSelf: "center", // Ortalamak için bunu ekledik
marginBottom: 10,
aspectRatio: 1 / 1, // Oranı sabit tutmak için ekledik
elevation:1,
borderRadius:15,
},
});
ASSISTANT
Elbette! Kodunuzun daha modüler ve okunabilir olması için bazı bileşenleri ve fonksiyonları ayrı dosyalara taşıyabiliriz. Aşağıda adım adım nasıl yapabileceğinizi ve gerekli dosya yapısını bulabilirsiniz.
## 1. Genel Yaklaşım
- **Bileşenleri Ayırma:** `Word` ve `Sentence` bileşenlerini ayrı dosyalara taşıyabiliriz.
- **Yardımcı Fonksiyonları Ayırma:** `fetchStoryFromFirestore`, `loadFonts` ve benzeri yardımcı fonksiyonları ayrı bir dosyada toplayabiliriz.
- **Stilleri Ayırma:** Stilleri de ayrı bir dosyada tanımlayarak daha temiz bir yapı oluşturabiliriz.
## 2. Dosya Yapısı Önerisi
```
/components
├── ReadStoryScreen.js
├── Word.js
├── Sentence.js
├── WordExplanationBox.js
├── QuizScreen.js
├── ToggleHeartButton.js
└── heart.js
/utils
├── fontUtils.js
├── storyUtils.js
└── ...
/styles
└── ReadStoryStyles.js
```
## 3. Bileşenleri Ayrı Dosyalara Taşıma
### a. `Word.js`
```javascript
// components/Word.js
import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
const Word = React.memo(({ word, onPress, style, isKnown }) => (
<TouchableOpacity onPress={() => onPress(word)}>
<Text style={[style, isKnown && styles.knownWord]}>{word}</Text>
</TouchableOpacity>
));
Word.propTypes = {
word: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
style: PropTypes.object,
isKnown: PropTypes.bool,
};
const styles = StyleSheet.create({
knownWord: {
backgroundColor: 'rgba(0, 255, 0, 0.1)',
borderRadius: 10,
},
});
export default Word;
```
### b. `Sentence.js`
```javascript
// components/Sentence.js
import React from 'react';
import { View, StyleSheet, Text, Image, TouchableOpacity } from 'react-native';
import PropTypes from 'prop-types';
import { Ionicons } from '@expo/vector-icons';
import Word from './Word';
const Sentence = React.memo(({
sentenceObj,
selectedSentence,
handleSentenceButtonPress,
handleWordPress,
fontSize,
theme,
arabicFont,
knownWords,
}) => {
if (sentenceObj.type === 'image') {
return (
<Image
source={{ uri: sentenceObj.content }}
style={styles.imageStyle}
/>
);
}
const isHeaderOrSubHeader = sentenceObj.type === 'header' || sentenceObj.type === 'subheader';
return (
<View style={[
styles.sentenceContainer,
selectedSentence === sentenceObj.content && styles.selectedSentence,
isHeaderOrSubHeader && (sentenceObj.type === 'header' ? styles.headerContainer : styles.subHeaderContainer),
{
borderRightColor: sentenceObj.type === 'header' ? theme.primaryColor : theme.secondaryColor
}
]}>
<TouchableOpacity style={styles.explanationButton} onPress={() => handleSentenceButtonPress(sentenceObj.content)}>
<Ionicons name="open" size={24} color={theme.primaryColor} />
</TouchableOpacity>
<View style={styles.arabicTextWrapper}>
{sentenceObj.content.split(' ').map((word, wordIndex) => (
<Word
key={`${sentenceObj.id}-${wordIndex}`}
word={word}
onPress={handleWordPress}
style={[
styles.descriptionArabic,
{
fontSize: isHeaderOrSubHeader ? fontSize * 1.6 : fontSize * 1.4,
color: theme.textColor,
fontFamily: arabicFont,
}
]}
isKnown={knownWords.has(word)}
/>
))}
</View>
</View>
);
});
Sentence.propTypes = {
sentenceObj: PropTypes.shape({
type: PropTypes.string,
content: PropTypes.string,
id: PropTypes.string,
}).isRequired,
selectedSentence: PropTypes.string,
handleSentenceButtonPress: PropTypes.func.isRequired,
handleWordPress: PropTypes.func.isRequired,
fontSize: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired,
arabicFont: PropTypes.string.isRequired,
knownWords: PropTypes.instanceOf(Set).isRequired,
};
const styles = StyleSheet.create({
sentenceContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
paddingLeft: 10,
},
explanationButton: {
marginRight: 2,
},
arabicTextWrapper: {
flex: 1,
flexDirection: 'row-reverse',
flexWrap: 'wrap',
paddingRight: 15,
},
descriptionArabic: {
marginLeft: 2,
padding: 2,
},
selectedSentence: {
backgroundColor: 'rgba(255, 215, 0, 0.3)',
},
headerContainer: {
marginVertical: 10,
backgroundColor: 'rgba(0, 0, 0, 0.03)',
borderRightWidth: 5,
width: '97%',
},
subHeaderContainer: {
marginVertical: 10,
backgroundColor: 'rgba(0, 0, 0, 0.03)',
borderRightWidth: 3,
width: '97%',
},
imageStyle: {
width: "90%",
alignSelf: "center",
marginBottom: 10,
aspectRatio: 1 / 1,
elevation:1,
borderRadius:15,
},
});
export default Sentence;
```
### c. `fontUtils.js` (Yardımcı Fonksiyonlar)
```javascript
// utils/fontUtils.js
import * as Font from 'expo-font';
export const ARABIC_FONTS = {
'Scheherazade': require('../../assets/fonts/scheherazade.ttf'),
'Adobe Arabic': require('../../assets/fonts/adobearabic.ttf'),
'Amiri': require('../../assets/fonts/amiri.ttf'),
'Noto Naskh Arabic': require('../../assets/fonts/noto.ttf'),
};
export const loadFonts = async () => {
await Font.loadAsync(ARABIC_FONTS);
};
```
### d. `storyUtils.js` (Firestore İşlemleri)
```javascript
// utils/storyUtils.js
import { doc, getDoc } from 'firebase/firestore';
import { db } from './firebase';
export const fetchStoryFromFirestore = async (id) => {
const storyDocRef = doc(db, 'stories', id);
const storyDoc = await getDoc(storyDocRef);
return storyDoc.exists() ? { id: storyDoc.id, ...storyDoc.data() } : null;
};
```
### e. `ReadStoryStyles.js` (Stilleri Ayırma)
```javascript
// styles/ReadStoryStyles.js
import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({
container: {
flex: 1,
},
knownWord: {
backgroundColor: 'rgba(0, 255, 0, 0.1)',
borderRadius: 10,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
thumbnail: {
width: '100%',
aspectRatio:1/1,
},
content: {
padding: 16,
},
header: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},
titleArabic: {
fontSize: 26,
},
titleTurkish: {
fontSize: 20,
marginTop: 4,
textAlign: 'center',
},
headerRight: {
flexDirection: 'row',
alignItems: 'center',
marginRight: 10,
},
fontSizeControl: {
marginHorizontal: 5,
},
sentenceContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
paddingLeft: 10,
},
explanationButton: {
marginRight: 2,
},
arabicTextWrapper: {
flex: 1,
flexDirection: 'row-reverse',
flexWrap: 'wrap',
paddingRight: 15,
},
descriptionArabic: {
marginLeft: 2,
padding: 2,
},
selectedSentence: {
backgroundColor: 'rgba(255, 215, 0, 0.3)',
},
settingsContent: {
flexGrow: 1,
paddingBottom: 20,
},
fontExampleContainer: {
alignItems: 'center',
marginBottom: 20,
},
exampleLabel: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
},
exampleBox: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 10,
padding: 10,
width: '80%',
alignItems: 'center',
backgroundColor: 'white',
elevation: 4,
},
fontSizeControls: {
flexDirection: 'row',
justifyContent: 'space-around',
marginVertical: 25,
paddingHorizontal: 25,
width: '80%',
alignSelf: 'center',
},
fontSizeControl: {
alignItems: 'center',
padding: 10,
},
fontSizeControlText: {
marginTop: 5,
fontSize: 14,
color: 'gray',
},
selectFontLabel: {
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 10,
},
fontGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-around',
marginTop: 20,
width: '80%',
alignSelf: 'center',
},
fontOptionGridItem: {
width: '45%',
padding: 5,
marginBottom: 20,
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 10,
alignItems: 'center',
backgroundColor: 'white',
elevation: 4,
},
fontOptionText: {
fontSize: 16,
marginBottom: 5,
textAlign: 'center',
},
quizButton: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
padding: 10,
},
quizButtonText: {
color: 'white',
fontWeight: 'bold',
},
bottomSheetContent: {
flexGrow: 1,
},
exampleCard: {
backgroundColor: 'white',
borderRadius: 10,
padding: 15,
marginVertical: 20,
width: '90%',
alignItems: 'center',
elevation: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
exampleText: {
textAlign: 'center',
},
fontSizeControls: {
flexDirection: 'row',
justifyContent: 'space-around',
width: '100%',
marginBottom: 20,
},
fontSizeButton: {
alignItems: 'center',
},
fontSizeButtonText: {
marginTop: 5,
fontSize: 12,
color: 'gray',
textAlign: 'center',
},
fontOptionsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
width: '100%',
},
fontOption: {
width: '48%',
backgroundColor: 'white',
borderRadius: 10,
padding: 10,
marginBottom: 15,
alignItems: 'center',
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.22,
shadowRadius: 2.22,
},
fontOptionText: {
fontSize: 16,
marginBottom: 5,
},
fontOptionSample: {
fontSize: 18,
},
});
```
## 4. `ReadStoryScreen.js` Dosyasını Güncelleme
Artık `Word` ve `Sentence` bileşenlerini ayrı dosyalarda tanımladığımıza göre, ana bileşen `ReadStoryScreen.js` dosyanızı aşağıdaki şekilde güncelleyebilirsiniz:
```javascript
// components/ReadStoryScreen.js
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { View, StyleSheet, Text, FlatList, Image, TouchableOpacity, ActivityIndicator, BackHandler } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import WordExplanationBox from './WordExplanationBox';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useThemeAndLanguage } from '../utils/ThemeAndLanguageContext';
import { BottomSheetModal, BottomSheetModalProvider, BottomSheetScrollView } from '@gorhom/bottom-sheet';
import QuizScreen from './QuizScreen';
import { useFocusEffect } from '@react-navigation/native';
import { useVocabulary } from './VocabularyContext';
import PropTypes from 'prop-types';
import ToggleHeartButton from './ToggleHeartButton'; // Dosya adı lowercase yapılabilir, uyumlu hale getirilmeli
import { loadFonts, ARABIC_FONTS } from '../utils/fontUtils';
import { fetchStoryFromFirestore } from '../utils/storyUtils';
import Sentence from './Sentence';
import { styles } from '../styles/ReadStoryStyles'; // Stil dosyasını ithal ediyoruz
const STORAGE_KEY = '@user_words';
export default function ReadStoryScreen({ route, navigation }) {
const { story, storyId } = route.params;
const [fontSize, setFontSize] = useState(18);
const [selectedWord, setSelectedWord] = useState(null);
const [selectedSentence, setSelectedSentence] = useState(null);
const [loading, setLoading] = useState(true);
const [cachedStory, setCachedStory] = useState(null);
const [hasQuiz, setHasQuiz] = useState(false);
const bottomSheetModalRef = useRef(null);
const settingsBottomSheetModalRef = useRef(null);
const quizBottomSheetModalRef = useRef(null);
const { locale, theme, arabicFont, setFontFamily, t } = useThemeAndLanguage();
const [activeModal, setActiveModal] = useState(null);
const [knownWords, setKnownWords] = useState(new Set());
const { vocabulary, isLoggedIn, userId } = useVocabulary();
const increaseFontSize = useCallback(() => setFontSize(prevSize => prevSize + 2), []);
const decreaseFontSize = useCallback(() => setFontSize(prevSize => prevSize - 2), []);
const closeAllModals = useCallback(() => {
bottomSheetModalRef.current?.dismiss();
settingsBottomSheetModalRef.current?.dismiss();
quizBottomSheetModalRef.current?.dismiss();
setActiveModal(null);
}, []);
const openSettingsModal = useCallback(() => {
settingsBottomSheetModalRef.current?.present();
setActiveModal('settings');
}, []);
const openQuizModal = useCallback(() => {
quizBottomSheetModalRef.current?.present();
setActiveModal('quiz');
}, []);
const handleWordPress = useCallback((word) => {
setSelectedWord(word);
setSelectedSentence(null);
bottomSheetModalRef.current?.present();
setActiveModal('word');
}, []);
const handleSentenceButtonPress = useCallback((sentence) => {
setSelectedSentence(sentence);
setSelectedWord(null);
bottomSheetModalRef.current?.present();
setActiveModal('sentence');
}, []);
const fetchStory = useCallback(async () => {
try {
let storyData;
if (story) {
storyData = story;
} else if (storyId) {
const cachedData = await AsyncStorage.getItem(`story-${storyId}`);
if (cachedData) {
storyData = JSON.parse(cachedData);
} else {
storyData = await fetchStoryFromFirestore(storyId);
if (storyData) {
await AsyncStorage.setItem(`story-${storyId}`, JSON.stringify(storyData));
}
}
}
if (storyData) {
setCachedStory(storyData);
setHasQuiz(storyData.test_questions && storyData.test_questions.length > 0);
}
} catch (error) {
console.error('Error fetching story:', error);
} finally {
setLoading(false);
}
}, [story, storyId]);
const fetchKnownWords = useCallback(async () => {
if (!isLoggedIn) {
setKnownWords(new Set());
return;
}
try {
const cachedWords = await AsyncStorage.getItem(STORAGE_KEY);
if (cachedWords) {
const wordsArray = JSON.parse(cachedWords);
setKnownWords(new Set(wordsArray.map(word => word.word)));
}
} catch (error) {
console.error('Error fetching known words:', error);
}
}, [isLoggedIn]);
useEffect(() => {
const initialize = async () => {
await loadFonts();
await fetchStory();
await fetchKnownWords();
};
initialize();
}, [fetchStory, fetchKnownWords]);
useEffect(() => {
if (cachedStory) {
const storyTitle = locale === 'tr' ? cachedStory.storytitleturkish : cachedStory.storytitleenglish;
const truncatedTitle = storyTitle.length > 23 ? `${storyTitle.substring(0, 23)}...` : storyTitle;
navigation.setOptions({
title: truncatedTitle,
headerStyle: { backgroundColor: theme.lighterCardBackgroundColor },
headerTintColor: theme.textColor,
headerTitleStyle: { color: theme.textColor, fontWeight: 'bold' },
headerRight: () => (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<ToggleHeartButton storyId={cachedStory.id} initialHeartCount={cachedStory.hearts} />
<TouchableOpacity onPress={openSettingsModal} style={styles.fontSizeControl}>
<Ionicons name="settings-outline" size={24} color={theme.textColor} />
</TouchableOpacity>
</View>
),
});
}
}, [navigation, cachedStory, theme, locale, openSettingsModal]);
useFocusEffect(
useCallback(() => {
const onBackPress = () => {
if (activeModal) {
closeAllModals();
return true;
}
return false;
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => BackHandler.removeEventListener('hardwareBackPress', onBackPress);
}, [activeModal, closeAllModals])
);
const renderSentence = useCallback(({ item: sentenceObj }) => (
<Sentence
sentenceObj={sentenceObj}
selectedSentence={selectedSentence}
handleSentenceButtonPress={handleSentenceButtonPress}
handleWordPress={handleWordPress}
fontSize={fontSize}
theme={theme}
arabicFont={arabicFont}
knownWords={knownWords}
/>
), [selectedSentence, handleSentenceButtonPress, handleWordPress, fontSize, theme, arabicFont, knownWords]);
const memoizedData = useMemo(() => cachedStory?.sentences || [], [cachedStory]);
const keyExtractor = useCallback((item) => item.id.toString(), []);
useEffect(() => {
const updatedKnownWords = new Set(vocabulary.map(word => word.word));
setKnownWords(updatedKnownWords);
}, [vocabulary]);
if (loading) {
return (
<View style={[styles.loadingContainer, { backgroundColor: theme.backgroundColorForWordBox }]}>
<ActivityIndicator size="large" color={theme.primaryColor} />
</View>
);
}
if (!cachedStory) {
return (
<View style={[styles.loadingContainer, { backgroundColor: theme.backgroundColorForWordBox }]}>
<Text style={{ color: theme.textColor }}>{t('storyNotFound')}</Text>
</View>
);
}
return (
<BottomSheetModalProvider>
<View style={[styles.container, { backgroundColor: theme.backgroundColorForWordBox }]}>
<FlatList
data={memoizedData}
renderItem={renderSentence}
keyExtractor={keyExtractor}
ListHeaderComponent={() => (
<>
<Image source={{ uri: cachedStory.thumbnail }} style={styles.thumbnail} />
<View style={styles.content}>
<View style={styles.header}>
<Text style={[styles.titleArabic, { color: theme.textColor, fontFamily: arabicFont }]}>
{cachedStory.storytitlearabic}
</Text>
<Text style={[styles.titleTurkish, { color: theme.secondaryColor }]}>
{locale === 'tr' ? cachedStory.storytitleturkish : cachedStory.storytitleenglish}
</Text>
</View>
</View>
</>
)}
initialNumToRender={10}
maxToRenderPerBatch={5}
windowSize={10}
/>
{hasQuiz && (
<TouchableOpacity
style={[styles.quizButton, { backgroundColor: theme.primaryColor }]}
onPress={openQuizModal}
>
<Text style={styles.quizButtonText}>{t('startQuiz')}</Text>
</TouchableOpacity>
)}
<BottomSheetModal
ref={bottomSheetModalRef}
index={0}
snapPoints={['45%', '90%']}
handleStyle={{ backgroundColor: theme.backgroundColorForWordBox }}
onDismiss={() => setActiveModal(null)}
>
<BottomSheetScrollView contentContainerStyle={styles.bottomSheetContent}>
<WordExplanationBox
word={selectedWord || selectedSentence}
storyId={cachedStory.id}
onClose={() => bottomSheetModalRef.current?.dismiss()}
/>
</BottomSheetScrollView>
</BottomSheetModal>
<BottomSheetModal
ref={settingsBottomSheetModalRef}
index={0}
snapPoints={['85%']}
handleStyle={{ backgroundColor: theme.backgroundColorForWordBox }}
onDismiss={() => setActiveModal(null)}
>
<BottomSheetScrollView
contentContainerStyle={[
styles.settingsContent,
{ backgroundColor: theme.backgroundColorForWordBox }
]}
>
<View style={styles.exampleCard}>
<Text style={[styles.exampleText, { fontSize: fontSize * 1.4, fontFamily: arabicFont }]}>
(هذا مثال على الكتابة بالعربية)
</Text>
</View>
<View style={styles.fontSizeControls}>
<TouchableOpacity onPress={increaseFontSize} style={styles.fontSizeButton}>
<Ionicons name="add-circle-outline" size={40} color={theme.primaryColor} />
<Text style={styles.fontSizeButtonText}>{t('increaseFontSize')}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={decreaseFontSize} style={styles.fontSizeButton}>
<Ionicons name="remove-circle-outline" size={40} color={theme.primaryColor} />
<Text style={styles.fontSizeButtonText}>{t('decreaseFontSize')}</Text>
</TouchableOpacity>
</View>
<View style={styles.fontOptionsContainer}>
{Object.keys(ARABIC_FONTS).map((font) => (
<TouchableOpacity
key={font}
style={styles.fontOption}
onPress={() => setFontFamily(font)}
>
<Text style={styles.fontOptionText}>{font}</Text>
<Text style={[styles.fontOptionSample, { fontFamily: font }]}>
(اقرأ العربية)
</Text>
</TouchableOpacity>
))}
</View>
</BottomSheetScrollView>
</BottomSheetModal>
{hasQuiz && (
<BottomSheetModal
ref={quizBottomSheetModalRef}
index={0}
snapPoints={['90%']}
handleStyle={{ backgroundColor: theme.backgroundColorForWordBox }}
onDismiss={() => setActiveModal(null)}
>
<QuizScreen
story={cachedStory}
onClose={() => {
quizBottomSheetModalRef.current?.dismiss();
setActiveModal(null);
}}
/>
</BottomSheetModal>
)}
</View>
</BottomSheetModalProvider>
);
}
ReadStoryScreen.propTypes = {
route: PropTypes.shape({
params: PropTypes.shape({
story: PropTypes.object,
storyId: PropTypes.string,
}).isRequired,
}).isRequired,
navigation: PropTypes.object.isRequired,
};
// Eğer stiller hala dosyada yer alıyorsa, aşağıdaki satırı kaldırın ve `styles` dosyasını kullandığınızdan emin olun
// const styles = StyleSheet.create({ ... });
```
## 5. Diğer Bileşenleri Kontrol Etme
Yukarıda `WordExplanationBox`, `QuizScreen`, `ToggleHeartButton` gibi bileşenlerin de ayrı dosyalarda tanımlandığından emin olun. Örneğin, `ToggleHeartButton.js`:
```javascript
// components/ToggleHeartButton.js
import React, { useState } from 'react';
import { TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import PropTypes from 'prop-types';
import AsyncStorage from '@react-native-async-storage/async-storage';
const ToggleHeartButton = ({ storyId, initialHeartCount }) => {
const [hearts, setHearts] = useState(initialHeartCount || 0);
const [isHearted, setIsHearted] = useState(false);
const toggleHeart = async () => {
try {
const newHeartStatus = !isHearted;
setIsHearted(newHeartStatus);
setHearts(prev => newHeartStatus ? prev + 1 : prev - 1);
// Burada hearts sayısını backend'de güncellemek için gerekli işlemleri yapabilirsiniz
// Örneğin: updateHeartCountInFirestore(storyId, newHeartStatus)
} catch (error) {
console.error('Error toggling heart:', error);
}
};
return (
<TouchableOpacity onPress={toggleHeart} style={{ marginRight: 10 }}>
<Ionicons name={isHearted ? "heart" : "heart-outline"} size={24} color="red" />
<Text>{hearts}</Text>
</TouchableOpacity>
);
};
ToggleHeartButton.propTypes = {
storyId: PropTypes.string.isRequired,
initialHeartCount: PropTypes.number,
};
export default ToggleHeartButton;
```
## 6. Stilleri Kullanma
`ReadStoryStyles.js` dosyasını oluşturduktan sonra, diğer bileşenlerde de bu stilleri kullanabilirsiniz. Örneğin, `Sentence.js` bileşeninizde:
```javascript
// components/Sentence.js
import { styles } from '../styles/ReadStoryStyles';
```
Bu şekilde hem stil hem de bileşenlerin daha temiz ve yönetilebilir olması sağlanır.
## 7. Yardımcı Notlar
- **Dosya İsimlendirmesi:** Dosya adlarınızın tutarlı olduğundan emin olun. Örneğin, `ToggleHeartButton` ile `heart.js` gibi dosya adlarında tutarlılık sağlayın.
- **PropTypes Kullanımı:** PropTypes ile bileşenlerinizin beklediği prop tiplerini tanımlamak, hata ayıklamayı kolaylaştırır.
- **Performans İyileştirmeleri:** `React.memo`, `useCallback` ve `useMemo` kullanarak performansı optimize etmeye devam edebilirsiniz.
- **Daha Fazla Ayırma:** Eğer proje büyürse, context'ler, yardımcı fonksiyonlar ve stil dosyalarını da ilgili klasörlerde daha detaylı ayırabilirsiniz.
Bu yapılandırma, kodunuzu daha modüler hale getirerek bakım ve geliştirme süreçlerini kolaylaştıracaktır. Her bileşenin kendi dosyasında bulunması, anlaşılabilirliği artırır ve ekip içinde çalışmayı kolaylaştırır.