Practice and reinforce the concepts from Lesson 8
Build educational language games by:
Time Limit: 5 minutes
# Create language learning app
npx create-expo-app LanguageQuest --template blank
cd LanguageQuest
# Install dependencies
npx expo install expo-av
npx expo install expo-speech
npx expo install @react-native-async-storage/async-storage
npx expo install expo-haptics
npx expo install react-native-svg
Time Limit: 5 minutes
Prepare vocabulary data:
// Language data structure
const LANGUAGES = {
spanish: {
name: 'Spanish',
flag: '๐ช๐ธ',
code: 'es-ES',
},
french: {
name: 'French',
flag: '๐ซ๐ท',
code: 'fr-FR',
},
german: {
name: 'German',
flag: '๐ฉ๐ช',
code: 'de-DE',
},
};
// Vocabulary database
const VOCABULARY = {
spanish: [
{
id: 1,
word: 'hola',
translation: 'hello',
pronunciation: 'OH-lah',
category: 'greetings',
difficulty: 1,
image: '๐',
},
{
id: 2,
word: 'gato',
translation: 'cat',
pronunciation: 'GAH-toh',
category: 'animals',
difficulty: 1,
image: '๐ฑ',
},
{
id: 3,
word: 'casa',
translation: 'house',
pronunciation: 'KAH-sah',
category: 'places',
difficulty: 1,
image: '๐ ',
},
// Add more vocabulary...
],
french: [
{
id: 1,
word: 'bonjour',
translation: 'hello',
pronunciation: 'bon-ZHOOR',
category: 'greetings',
difficulty: 1,
image: '๐',
},
// Add more vocabulary...
],
};
// Game types
const GAME_TYPES = {
FLASHCARDS: 'flashcards',
MULTIPLE_CHOICE: 'multiple_choice',
PRONUNCIATION: 'pronunciation',
MEMORY_GAME: 'memory_game',
WORD_BUILDER: 'word_builder',
};
โ Checkpoint: Language data and structure ready!
Replace App.js
with flashcard functionality:
import React, { useState, useEffect, useRef } from 'react';
import {
StyleSheet,
Text,
View,
TouchableOpacity,
Dimensions,
Animated,
Alert,
} from 'react-native';
import * as Speech from 'expo-speech';
import * as Haptics from 'expo-haptics';
import AsyncStorage from '@react-native-async-storage/async-storage';
const { width } = Dimensions.get('window');
// Sample vocabulary data
const VOCABULARY = {
spanish: [
{ id: 1, word: 'hola', translation: 'hello', image: '๐', difficulty: 1 },
{ id: 2, word: 'gato', translation: 'cat', image: '๐ฑ', difficulty: 1 },
{ id: 3, word: 'casa', translation: 'house', image: '๐ ', difficulty: 1 },
{ id: 4, word: 'agua', translation: 'water', image: '๐ง', difficulty: 1 },
{ id: 5, word: 'comida', translation: 'food', image: '๐ฝ๏ธ', difficulty: 2 },
{ id: 6, word: 'familia', translation: 'family', image: '๐จโ๐ฉโ๐งโ๐ฆ', difficulty: 2 },
{ id: 7, word: 'escuela', translation: 'school', image: '๐ซ', difficulty: 2 },
{ id: 8, word: 'feliz', translation: 'happy', image: '๐', difficulty: 3 },
],
};
export default function App() {
// App state
const [currentScreen, setCurrentScreen] = useState('home');
const [selectedLanguage, setSelectedLanguage] = useState('spanish');
const [currentCardIndex, setCurrentCardIndex] = useState(0);
const [showTranslation, setShowTranslation] = useState(false);
const [score, setScore] = useState(0);
const [streak, setStreak] = useState(0);
const [userProgress, setUserProgress] = useState({});
// Animation
const flipAnimation = useRef(new Animated.Value(0)).current;
const cardAnimation = useRef(new Animated.Value(0)).current;
// Load progress on mount
useEffect(() => {
loadUserProgress();
}, []);
const loadUserProgress = async () => {
try {
const progress = await AsyncStorage.getItem('languageLearningProgress');
if (progress) {
setUserProgress(JSON.parse(progress));
}
} catch (error) {
console.error('Error loading progress:', error);
}
};
const saveUserProgress = async (progress) => {
try {
await AsyncStorage.setItem('languageLearningProgress', JSON.stringify(progress));
} catch (error) {
console.error('Error saving progress:', error);
}
};
const getCurrentVocabulary = () => {
return VOCABULARY[selectedLanguage] || [];
};
const getCurrentCard = () => {
const vocab = getCurrentVocabulary();
return vocab[currentCardIndex] || null;
};
const speakWord = (text, language = 'es-ES') => {
Speech.speak(text, {
language,
pitch: 1.0,
rate: 0.8,
});
};
const flipCard = () => {
if (showTranslation) {
// Flip to front (word)
Animated.timing(flipAnimation, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start();
} else {
// Flip to back (translation)
Animated.timing(flipAnimation, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
}
setShowTranslation(!showTranslation);
};
const nextCard = () => {
const vocab = getCurrentVocabulary();
if (currentCardIndex < vocab.length - 1) {
setCurrentCardIndex(currentCardIndex + 1);
} else {
// End of deck
Alert.alert(
'Deck Complete!',
`Great job! You completed the ${selectedLanguage} deck with a score of ${score}.`,
[
{ text: 'Restart', onPress: () => restartDeck() },
{ text: 'Home', onPress: () => setCurrentScreen('home') },
]
);
}
setShowTranslation(false);
flipAnimation.setValue(0);
};
const previousCard = () => {
if (currentCardIndex > 0) {
setCurrentCardIndex(currentCardIndex - 1);
}
setShowTranslation(false);
flipAnimation.setValue(0);
};
const markKnown = () => {
const card = getCurrentCard();
if (!card) return;
setScore(score + 10);
setStreak(streak + 1);
// Update progress
const updatedProgress = {
...userProgress,
[card.id]: {
...userProgress[card.id],
correct: (userProgress[card.id]?.correct || 0) + 1,
lastReview: Date.now(),
nextReview: Date.now() + (24 * 60 * 60 * 1000), // 24 hours
},
};
setUserProgress(updatedProgress);
saveUserProgress(updatedProgress);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
nextCard();
};
const markUnknown = () => {
const card = getCurrentCard();
if (!card) return;
setStreak(0);
// Update progress
const updatedProgress = {
...userProgress,
[card.id]: {
...userProgress[card.id],
incorrect: (userProgress[card.id]?.incorrect || 0) + 1,
lastReview: Date.now(),
nextReview: Date.now() + (10 * 60 * 1000), // 10 minutes
},
};
setUserProgress(updatedProgress);
saveUserProgress(updatedProgress);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
nextCard();
};
const restartDeck = () => {
setCurrentCardIndex(0);
setScore(0);
setStreak(0);
setShowTranslation(false);
flipAnimation.setValue(0);
};
const startFlashcards = () => {
setCurrentScreen('flashcards');
restartDeck();
};
// Render functions
const renderHomeScreen = () => (
<View style={styles.container}>
<Text style={styles.title}>Language Quest</Text>
<Text style={styles.subtitle}>Learn languages through games!</Text>
<View style={styles.statsContainer}>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{score}</Text>
<Text style={styles.statLabel}>Score</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{streak}</Text>
<Text style={styles.statLabel}>Streak</Text>
</View>
</View>
<View style={styles.gameSelection}>
<TouchableOpacity style={styles.gameButton} onPress={startFlashcards}>
<Text style={styles.gameEmoji}>๐</Text>
<Text style={styles.gameButtonText}>Flashcards</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.gameButton}>
<Text style={styles.gameEmoji}>๐งช</Text>
<Text style={styles.gameButtonText}>Quiz</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.gameButton}>
<Text style={styles.gameEmoji}>๐ค</Text>
<Text style={styles.gameButtonText}>Pronunciation</Text>
</TouchableOpacity>
</View>
</View>
);
const renderFlashcardScreen = () => {
const card = getCurrentCard();
if (!card) return null;
const frontInterpolate = flipAnimation.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '180deg'],
});
const backInterpolate = flipAnimation.interpolate({
inputRange: [0, 1],
outputRange: ['180deg', '360deg'],
});
return (
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => setCurrentScreen('home')}
>
<Text style={styles.backButtonText}>โ Back</Text>
</TouchableOpacity>
<View style={styles.progressContainer}>
<Text style={styles.progressText}>
{currentCardIndex + 1} / {getCurrentVocabulary().length}
</Text>
</View>
<View style={styles.scoreContainer}>
<Text style={styles.scoreText}>Score: {score}</Text>
<Text style={styles.streakText}>Streak: {streak} ๐ฅ</Text>
</View>
</View>
{/* Flashcard */}
<View style={styles.cardContainer}>
<TouchableOpacity onPress={flipCard} style={styles.cardTouchable}>
<Animated.View
style={[
styles.card,
styles.cardFront,
{ transform: [{ rotateY: frontInterpolate }] },
showTranslation && styles.cardHidden,
]}
>
<Text style={styles.cardEmoji}>{card.image}</Text>
<Text style={styles.cardWord}>{card.word}</Text>
<TouchableOpacity
style={styles.speakButton}
onPress={() => speakWord(card.word)}
>
<Text style={styles.speakButtonText}>๐ Speak</Text>
</TouchableOpacity>
</Animated.View>
<Animated.View
style={[
styles.card,
styles.cardBack,
{ transform: [{ rotateY: backInterpolate }] },
!showTranslation && styles.cardHidden,
]}
>
<Text style={styles.cardEmoji}>{card.image}</Text>
<Text style={styles.cardTranslation}>{card.translation}</Text>
<Text style={styles.cardWord}>{card.word}</Text>
</Animated.View>
</TouchableOpacity>
</View>
{/* Instructions */}
<Text style={styles.instruction}>
{showTranslation
? 'Did you know this word?'
: 'Tap card to see translation'}
</Text>
{/* Controls */}
<View style={styles.controls}>
{showTranslation ? (
<View style={styles.knowledgeButtons}>
<TouchableOpacity
style={[styles.knowledgeButton, styles.unknownButton]}
onPress={markUnknown}
>
<Text style={styles.knowledgeButtonText}>โ Don't Know</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.knowledgeButton, styles.knownButton]}
onPress={markKnown}
>
<Text style={styles.knowledgeButtonText}>โ
I Know It!</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.navigationButtons}>
<TouchableOpacity
style={styles.navButton}
onPress={previousCard}
disabled={currentCardIndex === 0}
>
<Text style={styles.navButtonText}>โช Previous</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navButton}
onPress={nextCard}
>
<Text style={styles.navButtonText}>Skip โฉ</Text>
</TouchableOpacity>
</View>
)}
</View>
</View>
);
};
return (
<View style={styles.container}>
{currentScreen === 'home' && renderHomeScreen()}
{currentScreen === 'flashcards' && renderFlashcardScreen()}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8f9fa',
paddingTop: 50,
},
title: {
fontSize: 32,
fontWeight: 'bold',
color: '#2c3e50',
textAlign: 'center',
marginBottom: 10,
},
subtitle: {
fontSize: 18,
color: '#7f8c8d',
textAlign: 'center',
marginBottom: 30,
},
statsContainer: {
flexDirection: 'row',
justifyContent: 'center',
marginBottom: 40,
},
statItem: {
alignItems: 'center',
marginHorizontal: 30,
},
statNumber: {
fontSize: 32,
fontWeight: 'bold',
color: '#3498db',
},
statLabel: {
fontSize: 16,
color: '#7f8c8d',
marginTop: 5,
},
gameSelection: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
paddingHorizontal: 20,
},
gameButton: {
backgroundColor: 'white',
borderRadius: 15,
padding: 20,
margin: 10,
alignItems: 'center',
minWidth: width * 0.25,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
gameEmoji: {
fontSize: 40,
marginBottom: 10,
},
gameButtonText: {
fontSize: 16,
fontWeight: 'bold',
color: '#2c3e50',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 10,
},
backButton: {
padding: 10,
},
backButtonText: {
fontSize: 16,
color: '#3498db',
fontWeight: 'bold',
},
progressContainer: {
alignItems: 'center',
},
progressText: {
fontSize: 16,
color: '#7f8c8d',
},
scoreContainer: {
alignItems: 'flex-end',
},
scoreText: {
fontSize: 14,
color: '#27ae60',
},
streakText: {
fontSize: 14,
color: '#e74c3c',
},
cardContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 20,
},
cardTouchable: {
width: width * 0.8,
height: 300,
},
card: {
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: 'white',
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 6,
backfaceVisibility: 'hidden',
},
cardFront: {
backgroundColor: '#3498db',
},
cardBack: {
backgroundColor: '#27ae60',
},
cardHidden: {
opacity: 0,
},
cardEmoji: {
fontSize: 60,
marginBottom: 20,
},
cardWord: {
fontSize: 28,
fontWeight: 'bold',
color: 'white',
marginBottom: 10,
},
cardTranslation: {
fontSize: 32,
fontWeight: 'bold',
color: 'white',
marginBottom: 10,
},
speakButton: {
backgroundColor: 'rgba(255,255,255,0.2)',
paddingHorizontal: 15,
paddingVertical: 8,
borderRadius: 20,
marginTop: 10,
},
speakButtonText: {
color: 'white',
fontSize: 16,
},
instruction: {
fontSize: 18,
textAlign: 'center',
color: '#7f8c8d',
marginBottom: 20,
},
controls: {
paddingHorizontal: 20,
paddingBottom: 30,
},
knowledgeButtons: {
flexDirection: 'row',
justifyContent: 'space-around',
},
knowledgeButton: {
paddingHorizontal: 25,
paddingVertical: 15,
borderRadius: 25,
minWidth: width * 0.35,
alignItems: 'center',
},
unknownButton: {
backgroundColor: '#e74c3c',
},
knownButton: {
backgroundColor: '#27ae60',
},
knowledgeButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
navigationButtons: {
flexDirection: 'row',
justifyContent: 'space-around',
},
navButton: {
backgroundColor: '#95a5a6',
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 20,
},
navButtonText: {
color: 'white',
fontSize: 16,
},
});
Add a multiple choice quiz game:
// Add to your component state
const [quizScore, setQuizScore] = useState(0);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [selectedAnswer, setSelectedAnswer] = useState(null);
const [showQuizResult, setShowQuizResult] = useState(false);
// Generate quiz questions
const generateQuizQuestions = (vocabulary, count = 10) => {
const questions = [];
const shuffledVocab = [...vocabulary].sort(() => Math.random() - 0.5);
for (let i = 0; i < Math.min(count, shuffledVocab.length); i++) {
const correctAnswer = shuffledVocab[i];
const wrongAnswers = vocabulary
.filter(word => word.id !== correctAnswer.id)
.sort(() => Math.random() - 0.5)
.slice(0, 3);
const allOptions = [correctAnswer, ...wrongAnswers]
.sort(() => Math.random() - 0.5);
questions.push({
id: i,
word: correctAnswer.word,
image: correctAnswer.image,
correctAnswer: correctAnswer.translation,
options: allOptions.map(option => option.translation),
});
}
return questions;
};
// Quiz functions
const startQuiz = () => {
const questions = generateQuizQuestions(getCurrentVocabulary());
setQuizQuestions(questions);
setCurrentQuestionIndex(0);
setQuizScore(0);
setSelectedAnswer(null);
setShowQuizResult(false);
setCurrentScreen('quiz');
};
const selectQuizAnswer = (answer) => {
setSelectedAnswer(answer);
const currentQuestion = quizQuestions[currentQuestionIndex];
if (answer === currentQuestion.correctAnswer) {
setQuizScore(quizScore + 10);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} else {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}
setShowQuizResult(true);
setTimeout(() => {
if (currentQuestionIndex < quizQuestions.length - 1) {
setCurrentQuestionIndex(currentQuestionIndex + 1);
setSelectedAnswer(null);
setShowQuizResult(false);
} else {
// Quiz complete
Alert.alert(
'Quiz Complete!',
`Final Score: ${quizScore + (answer === currentQuestion.correctAnswer ? 10 : 0)}`,
[{ text: 'OK', onPress: () => setCurrentScreen('home') }]
);
}
}, 1500);
};
// Quiz render function
const renderQuizScreen = () => {
const question = quizQuestions[currentQuestionIndex];
if (!question) return null;
return (
<View style={styles.container}>
{/* Quiz Header */}
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => setCurrentScreen('home')}
>
<Text style={styles.backButtonText}>โ Back</Text>
</TouchableOpacity>
<Text style={styles.progressText}>
{currentQuestionIndex + 1} / {quizQuestions.length}
</Text>
<Text style={styles.scoreText}>Score: {quizScore}</Text>
</View>
{/* Question */}
<View style={styles.questionContainer}>
<Text style={styles.questionEmoji}>{question.image}</Text>
<Text style={styles.questionWord}>{question.word}</Text>
<TouchableOpacity onPress={() => speakWord(question.word)}>
<Text style={styles.speakIcon}>๐</Text>
</TouchableOpacity>
</View>
{/* Answer Options */}
<View style={styles.optionsContainer}>
{question.options.map((option, index) => {
let buttonStyle = styles.optionButton;
if (showQuizResult && selectedAnswer === option) {
buttonStyle = option === question.correctAnswer
? styles.correctOption
: styles.incorrectOption;
} else if (showQuizResult && option === question.correctAnswer) {
buttonStyle = styles.correctOption;
}
return (
<TouchableOpacity
key={index}
style={buttonStyle}
onPress={() => !showQuizResult && selectQuizAnswer(option)}
disabled={showQuizResult}
>
<Text style={styles.optionText}>{option}</Text>
</TouchableOpacity>
);
})}
</View>
</View>
);
};
// Add quiz styles
questionContainer: {
alignItems: 'center',
paddingVertical: 40,
},
questionEmoji: {
fontSize: 80,
marginBottom: 20,
},
questionWord: {
fontSize: 36,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 10,
},
speakIcon: {
fontSize: 30,
},
optionsContainer: {
paddingHorizontal: 20,
},
optionButton: {
backgroundColor: 'white',
padding: 20,
borderRadius: 10,
marginVertical: 8,
elevation: 2,
},
correctOption: {
backgroundColor: '#27ae60',
},
incorrectOption: {
backgroundColor: '#e74c3c',
},
optionText: {
fontSize: 18,
textAlign: 'center',
color: '#2c3e50',
},
Build pronunciation practice:
// Add speech recognition (Note: This is a simplified version)
const [isListening, setIsListening] = useState(false);
const [pronunciationFeedback, setPronunciationFeedback] = useState('');
// Pronunciation game
const startPronunciationGame = () => {
setCurrentScreen('pronunciation');
};
const startListening = async () => {
try {
setIsListening(true);
setPronunciationFeedback('Listening...');
// Simplified pronunciation check
// In a real app, you'd use speech recognition APIs
setTimeout(() => {
const accuracy = Math.random(); // Simulate accuracy
if (accuracy > 0.7) {
setPronunciationFeedback('โ
Great pronunciation!');
setScore(score + 15);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} else if (accuracy > 0.4) {
setPronunciationFeedback('๐ก Good attempt! Try again.');
} else {
setPronunciationFeedback('โ Try listening more carefully.');
}
setIsListening(false);
}, 3000);
} catch (error) {
console.error('Speech recognition error:', error);
setIsListening(false);
setPronunciationFeedback('Error with speech recognition');
}
};
// Pronunciation screen render
const renderPronunciationScreen = () => {
const card = getCurrentCard();
if (!card) return null;
return (
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => setCurrentScreen('home')}
>
<Text style={styles.backButtonText}>โ Back</Text>
</TouchableOpacity>
</View>
<View style={styles.pronunciationContainer}>
<Text style={styles.pronunciationTitle}>Pronunciation Practice</Text>
<View style={styles.wordDisplay}>
<Text style={styles.cardEmoji}>{card.image}</Text>
<Text style={styles.pronunciationWord}>{card.word}</Text>
<TouchableOpacity
style={styles.playButton}
onPress={() => speakWord(card.word)}
>
<Text style={styles.playButtonText}>โถ๏ธ Listen</Text>
</TouchableOpacity>
</View>
<View style={styles.recordingSection}>
<TouchableOpacity
style={[
styles.recordButton,
isListening && styles.recordButtonActive
]}
onPress={startListening}
disabled={isListening}
>
<Text style={styles.recordButtonText}>
{isListening ? '๐ค Listening...' : '๐ค Record'}
</Text>
</TouchableOpacity>
{pronunciationFeedback ? (
<Text style={styles.feedback}>{pronunciationFeedback}</Text>
) : (
<Text style={styles.instruction}>Tap record and say the word</Text>
)}
</View>
<View style={styles.pronunciationControls}>
<TouchableOpacity style={styles.nextWordButton} onPress={nextCard}>
<Text style={styles.nextWordButtonText}>Next Word</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
};
// Pronunciation styles
pronunciationContainer: {
flex: 1,
paddingHorizontal: 20,
justifyContent: 'center',
},
pronunciationTitle: {
fontSize: 24,
fontWeight: 'bold',
color: '#2c3e50',
textAlign: 'center',
marginBottom: 40,
},
wordDisplay: {
alignItems: 'center',
marginBottom: 60,
},
pronunciationWord: {
fontSize: 42,
fontWeight: 'bold',
color: '#3498db',
marginVertical: 20,
},
playButton: {
backgroundColor: '#3498db',
paddingHorizontal: 30,
paddingVertical: 15,
borderRadius: 25,
},
playButtonText: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
recordingSection: {
alignItems: 'center',
marginBottom: 40,
},
recordButton: {
backgroundColor: '#e74c3c',
width: 120,
height: 120,
borderRadius: 60,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 20,
},
recordButtonActive: {
backgroundColor: '#c0392b',
transform: [{ scale: 1.1 }],
},
recordButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
},
feedback: {
fontSize: 18,
textAlign: 'center',
marginTop: 10,
},
instruction: {
fontSize: 16,
color: '#7f8c8d',
textAlign: 'center',
},
pronunciationControls: {
alignItems: 'center',
},
nextWordButton: {
backgroundColor: '#27ae60',
paddingHorizontal: 25,
paddingVertical: 12,
borderRadius: 20,
},
nextWordButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
Create a memory card matching game:
// Memory game state
const [memoryCards, setMemoryCards] = useState([]);
const [flippedCards, setFlippedCards] = useState([]);
const [matchedPairs, setMatchedPairs] = useState([]);
const [memoryScore, setMemoryScore] = useState(0);
// Initialize memory game
const startMemoryGame = () => {
const vocabulary = getCurrentVocabulary().slice(0, 6); // Use 6 pairs
const gameCards = [];
// Create word cards and translation cards
vocabulary.forEach((word, index) => {
gameCards.push({
id: `word-${word.id}`,
content: word.word,
type: 'word',
pairId: word.id,
image: word.image,
});
gameCards.push({
id: `translation-${word.id}`,
content: word.translation,
type: 'translation',
pairId: word.id,
image: word.image,
});
});
// Shuffle cards
const shuffledCards = gameCards.sort(() => Math.random() - 0.5);
setMemoryCards(shuffledCards);
setFlippedCards([]);
setMatchedPairs([]);
setMemoryScore(0);
setCurrentScreen('memory');
};
// Handle card flip
const flipMemoryCard = (cardId) => {
if (flippedCards.length >= 2 || flippedCards.includes(cardId)) {
return;
}
const newFlippedCards = [...flippedCards, cardId];
setFlippedCards(newFlippedCards);
if (newFlippedCards.length === 2) {
// Check for match
const card1 = memoryCards.find(card => card.id === newFlippedCards[0]);
const card2 = memoryCards.find(card => card.id === newFlippedCards[1]);
if (card1.pairId === card2.pairId) {
// Match found!
setMatchedPairs([...matchedPairs, card1.pairId]);
setMemoryScore(memoryScore + 20);
setFlippedCards([]);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
// Check if game is complete
if (matchedPairs.length + 1 === getCurrentVocabulary().slice(0, 6).length) {
setTimeout(() => {
Alert.alert('Congratulations!', `You completed the memory game with ${memoryScore + 20} points!`);
}, 500);
}
} else {
// No match
setTimeout(() => {
setFlippedCards([]);
}, 1000);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
}
}
};
// Memory game render
const renderMemoryGame = () => {
return (
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => setCurrentScreen('home')}
>
<Text style={styles.backButtonText}>โ Back</Text>
</TouchableOpacity>
<Text style={styles.memoryTitle}>Memory Game</Text>
<Text style={styles.scoreText}>Score: {memoryScore}</Text>
</View>
<View style={styles.memoryGrid}>
{memoryCards.map((card) => {
const isFlipped = flippedCards.includes(card.id) || matchedPairs.includes(card.pairId);
const isMatched = matchedPairs.includes(card.pairId);
return (
<TouchableOpacity
key={card.id}
style={[
styles.memoryCard,
isMatched && styles.matchedCard,
]}
onPress={() => flipMemoryCard(card.id)}
disabled={isFlipped}
>
{isFlipped ? (
<View style={styles.cardContent}>
<Text style={styles.memoryCardEmoji}>{card.image}</Text>
<Text style={styles.memoryCardText}>{card.content}</Text>
</View>
) : (
<View style={styles.cardBack}>
<Text style={styles.cardBackText}>?</Text>
</View>
)}
</TouchableOpacity>
);
})}
</View>
<Text style={styles.memoryInstructions}>
Match words with their translations!
</Text>
</View>
);
};
// Memory game styles
memoryTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#2c3e50',
},
memoryGrid: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
paddingHorizontal: 10,
paddingVertical: 20,
},
memoryCard: {
width: width * 0.28,
height: width * 0.28,
backgroundColor: '#3498db',
borderRadius: 10,
margin: 5,
justifyContent: 'center',
alignItems: 'center',
elevation: 3,
},
matchedCard: {
backgroundColor: '#27ae60',
},
cardContent: {
alignItems: 'center',
},
memoryCardEmoji: {
fontSize: 24,
marginBottom: 5,
},
memoryCardText: {
color: 'white',
fontSize: 14,
fontWeight: 'bold',
textAlign: 'center',
},
cardBack: {
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%',
},
cardBackText: {
fontSize: 32,
color: 'white',
fontWeight: 'bold',
},
memoryInstructions: {
fontSize: 16,
textAlign: 'center',
color: '#7f8c8d',
paddingBottom: 20,
},
Implement spaced repetition algorithm:
// Spaced repetition intervals (in milliseconds)
const SPACED_REPETITION = {
INTERVALS: [
10 * 60 * 1000, // 10 minutes
60 * 60 * 1000, // 1 hour
24 * 60 * 60 * 1000, // 1 day
3 * 24 * 60 * 60 * 1000, // 3 days
7 * 24 * 60 * 60 * 1000, // 1 week
30 * 24 * 60 * 60 * 1000, // 1 month
],
};
// Calculate next review time
const calculateNextReview = (wordProgress) => {
const { correct = 0, incorrect = 0 } = wordProgress;
const totalAttempts = correct + incorrect;
if (totalAttempts === 0) {
return Date.now(); // Review immediately for new words
}
const accuracy = correct / totalAttempts;
let intervalIndex = Math.min(correct - 1, SPACED_REPETITION.INTERVALS.length - 1);
// Adjust interval based on accuracy
if (accuracy < 0.6) {
intervalIndex = Math.max(0, intervalIndex - 1); // Review sooner if struggling
}
const interval = SPACED_REPETITION.INTERVALS[intervalIndex] || SPACED_REPETITION.INTERVALS[0];
return Date.now() + interval;
};
// Get words due for review
const getWordsForReview = () => {
const vocabulary = getCurrentVocabulary();
const now = Date.now();
return vocabulary.filter(word => {
const progress = userProgress[word.id];
if (!progress) return true; // New words
return progress.nextReview <= now;
});
};
// Progress dashboard
const renderProgressDashboard = () => {
const vocabulary = getCurrentVocabulary();
const wordsForReview = getWordsForReview();
const totalWords = vocabulary.length;
const masteredWords = vocabulary.filter(word => {
const progress = userProgress[word.id];
return progress && progress.correct >= 5 && (progress.correct / (progress.correct + progress.incorrect)) >= 0.8;
}).length;
return (
<View style={styles.progressDashboard}>
<Text style={styles.progressTitle}>Your Progress</Text>
<View style={styles.progressStats}>
<View style={styles.progressStat}>
<Text style={styles.progressNumber}>{masteredWords}</Text>
<Text style={styles.progressLabel}>Mastered</Text>
</View>
<View style={styles.progressStat}>
<Text style={styles.progressNumber}>{wordsForReview.length}</Text>
<Text style={styles.progressLabel}>Due for Review</Text>
</View>
<View style={styles.progressStat}>
<Text style={styles.progressNumber}>{totalWords}</Text>
<Text style={styles.progressLabel}>Total Words</Text>
</View>
</View>
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{ width: `${(masteredWords / totalWords) * 100}%` }
]}
/>
</View>
<Text style={styles.progressPercentage}>
{Math.round((masteredWords / totalWords) * 100)}% Complete
</Text>
</View>
);
};
// Progress styles
progressDashboard: {
backgroundColor: 'white',
margin: 20,
borderRadius: 15,
padding: 20,
elevation: 3,
},
progressTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#2c3e50',
textAlign: 'center',
marginBottom: 20,
},
progressStats: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 20,
},
progressStat: {
alignItems: 'center',
},
progressNumber: {
fontSize: 24,
fontWeight: 'bold',
color: '#3498db',
},
progressLabel: {
fontSize: 14,
color: '#7f8c8d',
marginTop: 5,
},
progressBar: {
height: 8,
backgroundColor: '#ecf0f1',
borderRadius: 4,
marginVertical: 10,
},
progressFill: {
height: '100%',
backgroundColor: '#27ae60',
borderRadius: 4,
},
progressPercentage: {
textAlign: 'center',
fontSize: 16,
color: '#2c3e50',
fontWeight: 'bold',
},
Language Learning App Successfully Built If:
Time Investment: 60 minutes total
Difficulty Level: Intermediate to Advanced
Prerequisites: React Native basics, understanding of educational app design
Tools Needed: Physical device for speech features