From 279cd6bda4a8461760f9a8e28bc519b8420223f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Silv=C3=A9rio?= Date: Sun, 28 Jan 2018 20:45:25 -0200 Subject: [PATCH] FEATURE: Quiz Quiz feature working for deck lists --- components/flashcards/DeckDetails.js | 2 +- components/flashcards/Quiz.js | 354 +++++++++++++++++++ components/navigators/FlashStackNavigator.js | 4 + services/index.js | 28 +- utils/colors.js | 1 + 5 files changed, 381 insertions(+), 8 deletions(-) create mode 100644 components/flashcards/Quiz.js diff --git a/components/flashcards/DeckDetails.js b/components/flashcards/DeckDetails.js index f83bb6d..a1e9e53 100644 --- a/components/flashcards/DeckDetails.js +++ b/components/flashcards/DeckDetails.js @@ -97,7 +97,7 @@ class DeckDetails extends Component { - + this.props.navigation.navigate('Quiz', { deckKey: deck.name })}> Start Quiz diff --git a/components/flashcards/Quiz.js b/components/flashcards/Quiz.js new file mode 100644 index 0000000..1193d32 --- /dev/null +++ b/components/flashcards/Quiz.js @@ -0,0 +1,354 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Alert } from 'react-native'; + +import { MaterialCommunityIcons } from '@expo/vector-icons'; + +import { saveScore } from '../../services'; + +import { COLOR_WHITE, COLOR_B_4, COLOR_A_1, COLOR_B_6, COLOR_SUCCESS, COLOR_BLACK, COLOR_FAILURE } from '../../utils/colors'; + +const ANSWER = 'A'; +const QUESTION = 'Q'; +const FORWARD = 'F'; +const REWIND = 'RE'; +const RIGHT = true; +const WRONG = false; +const WON = 'W'; +const LOST = 'L'; +const TIE = 'T'; + +class Quiz extends Component { + + static navigationOptions = ({ navigation }) => { + const { deckKey } = navigation.state.params; + + return { + title: `'${deckKey}' Quiz`, + } + } + + state = { + questionIndex: 0, + answerQuestion: ANSWER, + questions: [] + } + + componentDidMount() { + if (this.props.cards.length >= 0) { + this.setState((prevState) => { + return { + questions: this.props.cards.map(card => { + const c = card; + c.correct = null; + + return c; + }) + } + }) + } + } + + navigateQuestions(step) { + + const { questionIndex } = this.state; + const size = this.state.questions.length; + + this.setState((prevState) => { + return { + answerQuestion: ANSWER + } + }) + + switch (step) { + case FORWARD: { + + if (questionIndex >= size - 1) { + return; + } + + this.setState((prevState) => { + return { + questionIndex: questionIndex + 1 + } + }); + return; + } + case REWIND: { + + if (questionIndex <= 0) { + return; + } + + this.setState((prevState) => { + return { + questionIndex: questionIndex - 1 + } + }); + return; + } + } + + } + + showAnswer() { + this.setState((prevState) => { + return { + answerQuestion: QUESTION + } + }) + } + + answer(status) { + const { questionIndex } = this.state; + + let refreshedQuestions = this.state.questions; + refreshedQuestions[questionIndex].correct = status; + + this.setState((prevState) => { + return { + questions: refreshedQuestions, + answerQuestion: ANSWER + } + }); + } + + done() { + + Alert.alert( + 'Finish quiz?', + 'Do you want do finish this quiz?', + [ + { text: 'Yes', onPress: this.finishQuiz.bind(this) }, + { text: 'No', onPress: () => { } } + ] + ) + } + + async finishQuiz() { + const { r, w } = this.state.questions.reduce( + (currentScore, question) => { + if (question.correct) { + return { + r: currentScore.r + 1, + w: currentScore.w + } + } else { + return { + r: currentScore.r, + w: currentScore.w + 1 + } + } + }, + { r: 0, w: 0 } + ); + + let title = ""; + let message = ""; + let status = null; + + if (r > w) { + title = "Congratulations!"; + message = "You won!"; + status = WON; + } else if (r < w) { + title = "Keep studying!"; + message = "You lost :("; + status = LOST; + } else { + title = "Almost there"; + message = "It's a tie =|"; + status = TIE; + } + + try { + const { navigation } = this.props; + const { deckKey } = navigation.state.params; + + await saveScore(deckKey, status); + Alert.alert( + title, + message, + [{ text: 'OK', onPress: () => navigation.goBack() }], + { cancelable: false } + ); + } catch (e) { + console.log(e); + } + + } + + + render() { + + const size = this.state.questions.length; + + if (size === 0) { + return ( + + + + ); + } else { + + const { questionIndex, answerQuestion } = this.state; + const question = this.state.questions[questionIndex]; + + return ( + + + { this.navigateQuestions(REWIND) }}> + + + + {questionIndex + 1}/{size} + + { this.navigateQuestions(FORWARD) }}> + + + + { + this.state.answerQuestion === ANSWER + ? + ( + + + "{question.question}" + + + + Answer + + + Done + + + + ) + : + ( + + + "{question.answer}" + + + this.answer(RIGHT)}> + Right =) + + this.answer(WRONG)}> + Wrong =( + + + + ) + } + + + ); + + } + } +} + +const styles = StyleSheet.create({ + mainContainer: { + flex: 1 + }, + topContainer: { + flex: 2, + flexDirection: 'row', + justifyContent: 'space-between' + }, + questionContainer: { + flex: 7, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderRadius: 2, + borderColor: COLOR_B_6, + margin: 5 + }, + btnContainer: { + flex: 3 + }, + qaWrapper: { + flex: 10 + }, + btnAnswerContainer: { + flex: 3, + flexDirection: 'row' + }, + btnArrow: { + flex: 2, + backgroundColor: COLOR_B_4, + alignItems: 'center', + justifyContent: 'center' + }, + numQuestionContainer: { + flex: 8, + alignItems: 'center', + justifyContent: 'center' + }, + numQuestionText: { + fontSize: 35, + fontWeight: 'bold' + }, + questionText: { + fontSize: 25, + fontStyle: 'italic' + }, + btn: { + flex: 1, + flexDirection: 'row', + backgroundColor: COLOR_B_4, + borderRadius: 2, + margin: 2 + }, + btnDone: { + flex: 1, + flexDirection: 'row', + backgroundColor: COLOR_SUCCESS, + borderRadius: 2, + margin: 2 + }, + btnAnswerText: { + flex: 1, + fontSize: 20, + alignSelf: 'center', + color: COLOR_A_1, + textAlign: 'center' + }, + btnDoneText: { + flex: 1, + fontSize: 20, + alignSelf: 'center', + color: COLOR_BLACK, + textAlign: 'center' + }, + btnCorrect: { + flex: 1, + flexDirection: 'row', + backgroundColor: 'green', + borderRadius: 2, + margin: 2 + }, + btnWrong: { + flex: 1, + flexDirection: 'row', + backgroundColor: COLOR_FAILURE, + borderRadius: 2, + margin: 2 + } +}); + +function mapStateToProps({ decks }, props) { + const { deckKey } = props.navigation.state.params; + const deck = decks[deckKey]; + const cards = deck.cards; + + return { + cards + }; +} + +export default connect(mapStateToProps)(Quiz); \ No newline at end of file diff --git a/components/navigators/FlashStackNavigator.js b/components/navigators/FlashStackNavigator.js index 9c97a97..f2761ed 100644 --- a/components/navigators/FlashStackNavigator.js +++ b/components/navigators/FlashStackNavigator.js @@ -9,6 +9,7 @@ import DeckDetails from '../flashcards/DeckDetails'; import { COLOR_A_1, COLOR_B_5 } from '../../utils/colors'; import CardList from '../flashcards/CardList'; +import Quiz from '../flashcards/Quiz'; export default class FlashStackNavigator extends Component { @@ -25,6 +26,9 @@ export default class FlashStackNavigator extends Component { }, Cards: { screen: CardList + }, + Quiz:{ + screen: Quiz } }, { navigationOptions: { diff --git a/services/index.js b/services/index.js index 98d28b6..5336258 100644 --- a/services/index.js +++ b/services/index.js @@ -1,14 +1,15 @@ import { AsyncStorage } from 'react-native'; -const FLASHCARDS_STORAGE_KEY = "Flashcards:decks" +const FLASHCARDS_STORAGE_KEY = "Flashcards:decks"; +const SCORES_STORAGE_KEY = "Flashcards:scores"; -export async function persistDeck(deck){ +export async function persistDeck(deck) { await AsyncStorage.mergeItem(FLASHCARDS_STORAGE_KEY, JSON.stringify({ [deck.name]: deck })) } -export async function deleteDeck(deckKey){ +export async function deleteDeck(deckKey) { let decks = await loadDecks(); delete decks[deckKey]; @@ -20,16 +21,16 @@ export async function loadDecks() { return decks !== null ? decks : []; } -export async function getDeck(deckKey){ +export async function getDeck(deckKey) { const decks = await loadDecks(); const deck = decks[deckKey]; return deck; } -export async function addCardToDeck(deckKey, card){ +export async function addCardToDeck(deckKey, card) { const deck = await getDeck(deckKey); - if(typeof deck === 'undefined'){ + if (typeof deck === 'undefined') { console.warn(`Trying to fetch invalid deck: ${deckKey}`); return; } @@ -38,9 +39,22 @@ export async function addCardToDeck(deckKey, card){ await persistDeck(deck); } -export async function deleteCardFromDeck(deckKey, cardName){ +export async function deleteCardFromDeck(deckKey, cardName) { const deck = await getDeck(deckKey); deck.cards = deck.cards.filter(card => card.question !== cardName); await persistDeck(deck); +} + +export async function getScores() { + const scores = JSON.parse(await AsyncStorage.getItem(SCORES_STORAGE_KEY)); + return scores !== null ? scores : []; +} + +export async function saveScore(deck, status) { + const scores = await getScores(); + const date = Date.now(); + scores.push({ deck, status, date }); + + await AsyncStorage.setItem(SCORES_STORAGE_KEY, JSON.stringify(scores)); } \ No newline at end of file diff --git a/utils/colors.js b/utils/colors.js index 6b7d5a3..d6b2c26 100644 --- a/utils/colors.js +++ b/utils/colors.js @@ -15,6 +15,7 @@ export const COLOR_B_6 = "#2728ff"; export const COLOR_B_7 = "#0000fe"; export const COLOR_WHITE = "#fff"; +export const COLOR_BLACK = "#000"; export const COLOR_SUCCESS = "#7ee821"; export const COLOR_FAILURE = "#ff2424";