import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars // content and config: import content from '../content/module.json'; import offlineQuestions from '../content/offline'; // two-dim. array questions[group][a=1|b=2|c=3] import config from '../config'; // services and utilities: import api from '../utilities/api'; import { fixedDigits } from '../utilities/formatter'; // screens and items: import TitleScreen from './TitleScreen.jsx'; import FinalScreen from './FinalScreen.jsx'; import QuestionScreen from './QuestionScreen.jsx'; /** * titlescreen -> questionscreen (*n) -> finalscreen */ export default class App extends Component { // construct and initialize functions constructor (props) { super(props); this.state = { route: 'titlescreen', // current screen chartMode: 'question', // current chart mode: question || score currentGroup: 0, // current group currentQuestion: 0, // current question in group currentAnswerIndex: null, // index in question pair of the currently set answer currentAnswerCorrect: false, // is the currently set question the correct one? isFetching: false, // currently performing XHR? userId: null, // current user id for posting token: null, // current access token voteRatios: null, // get votes from other users points: {} // get correct and incorrect points }; // context binding this.navigate = this.navigate.bind(this); this.toNextQuestion = this.toNextQuestion.bind(this); this.setAnswer = this.setAnswer.bind(this); this.setData = this.setData.bind(this); this.drawQuestions = this.drawQuestions.bind(this); this.getUserVotes = this.getUserVotes.bind(this); this.jumpTo = this.jumpTo.bind(this); this.setInitialState = this.setInitialState.bind(this); this.getFeedbackCoordinates = this.getFeedbackCoordinates.bind(this); } // initial setup, get accesToken and userId componentDidMount () { // register hashchange handler. // Needs more work: reset state, otherwise artefacts will appear. // Without event handler the site has to be reloaded after hash change // onhashchange = this.jumpTo; if (this.props.isOffline) { this.setState({ questions: offlineQuestions }); this.jumpTo(); } else { this.setState({ isFetching: true }); api.getToken().then(accessToken => { this.setState({ token: accessToken }); // create user api.createUser(accessToken) .then(user => { this.setState({ userId: user.userId, isFetching: false }); // draw first question this.drawQuestions(); }); }); } } /** * Navigation using window.location.hash, else go to title screen * * @route: The state's route (string). See switch statement in render function * @currentGroup: 1-based index of current 'question group' * @currentQuestion: 1-based index of current question * * example: .../#question_2_1 */ jumpTo () { if (window.location.hash) { const hashInfo = window.location.hash.replace('#', '').split('_'); this.setState({ route: hashInfo[0], currentGroup: parseInt(hashInfo[1]) - 1 || 0, currentQuestion: parseInt(hashInfo[2]) - 1 || 0 }); } } // navigate to specific route navigate (route) { if (route === 'titlescreen') this.setInitialState(); this.setState({ route }); } // set data setData (isCorrect) { // handle continue button visibility this.setState({ currentAnswerIndex: isCorrect ? 1 : null }); } // get number of questions getNumberQuestions () { let sum = 0; for (let i = 0; i < this.state.questions.length; i += 1) { sum += this.state.questions[i].length; } return sum; } // get question set from api drawQuestions () { const questionSet = []; api.get(config.api.list) .then(json => { [ ...json ].forEach(item => { if (questionSet.length < item.gruppe) { questionSet.push([]); } questionSet[item.gruppe - 1].push(item); }); this.setState({ questions: questionSet }); this.jumpTo(); }); } // update state depending on chart mode and current question toNextQuestion () { // Show "score screen" after each question of first and second question groups in online mode if (this.state.chartMode === 'question' && this.state.currentGroup < 2 && !this.props.isOffline) { this.setState({ chartMode: 'score', points: {} }); // Else, if exists, display next question in group } else if (this.state.currentQuestion < this.state.questions[this.state.currentGroup].length - 1) { this.setState({ route: 'question', chartMode: 'question', currentQuestion: this.state.currentQuestion + 1, currentAnswerIndex: null, points: {}, voteRatios: null }); // Else, if exists, jump to fist question in next group } else if (this.state.currentGroup < this.state.questions.length - 1) { this.setState({ route: 'question', chartMode: 'question', currentGroup: this.state.currentGroup + 1, currentQuestion: 0, currentAnswerIndex: null, voteRatios: null }); // Else show "final screen" } else { // go to finalscreen this.setState({ route: 'finalscreen' }); } } // set initial state (for start and restart) setInitialState () { this.setState({ route: 'titlescreen', chartMode: 'question', currentGroup: 0, currentQuestion: 0, currentAnswerIndex: null, voteRatios: null }); } /** * Get coordinates for solution and, if applicable, incorrect answer. * * @param question: Data structure defining the question * @param answer: Answer selected by user * @return: Coordinates of solution, and answer, if incorrect */ getFeedbackCoordinates (question, answer) { const points = { correct: null, incorrect: null }; const index = question.antworten.indexOf(answer); let correctAnswer; if (!answer.korrekt) { correctAnswer = question.antworten.find(a => a.korrekt === true); } else { correctAnswer = answer; } points.correct = correctAnswer.koordinate; // If answer is not correct display both solution, and answer if (!answer.korrekt) { points.incorrect = question.antworten[index].koordinate; } return points; } /** * Submit answer to API, get percentage of correct answers by other users (multiple choice questions) * By convention (react / preact), the render function is automatically invoked after this.setState(...) has been called * * @param answer: Answer selected by user */ setAnswer (answer) { const question = this.state.questions[this.state.currentGroup][this.state.currentQuestion]; const cAnswerIndex = question.antworten.indexOf(answer); let prepareState = { currentAnswerIndex: cAnswerIndex, currentAnswerCorrect: answer.korrekt }; if (this.state.currentGroup === 0) { // only search for result coordinates for first question group. prepareState.points = this.getFeedbackCoordinates(question, answer); } if (!this.props.isOffline) { const payload = { userId: this.state.userId, aufgabeId: question.id, antwort: question.antworten[cAnswerIndex].antwort, korrekt: answer.korrekt }; api.post(config.api.create, payload, this.state.token) .then(() => { this.getUserVotes(question.id).then( json => { prepareState.voteRatios = [ json.auswertung.korrekt, json.auswertung.inkorrekt ]; this.setState(prepareState); } ); }); } else { this.setState(prepareState); } } // get user votes by question id getUserVotes (aufgabeId) { return api.get(config.api.proportions, { aufgabeId }); } // RENDER render () { let outputContent; switch (this.state.route) { case 'question': { // calculate number of questions from nested questions array const total = this.state.questions.map(e => e.length).reduce((s, e) => (s + e)); // calculate current question 'index' (positive number) let index = 0; for (let i = 0; i < this.state.currentGroup; i += 1) { index += this.state.questions[i].length; } index += this.state.currentQuestion + 1; const headerState = `${fixedDigits(index, 2)}/${total}`; outputContent = {} } />; // eslint-disable-line no-empty-function break; } case 'finalscreen': outputContent = ; break; case 'titlescreen': default: outputContent = ; break; } return outputContent; } }