| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- 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.coordinates;
- // If answer is not correct display both solution, and answer
- if (!answer.korrekt) {
- points.incorrect = question.antworten[index].coordinates;
- }
- 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 = <QuestionScreen
- {...content}
- route={this.state.route}
- chartMode={this.state.chartMode}
- headerState={headerState}
- questions={this.state.questions[this.state.currentGroup]}
- currentQuestion={this.state.currentQuestion}
- currentAnswerIndex={this.state.currentAnswerIndex}
- currentAnswerCorrect={this.state.currentAnswerCorrect}
- toNextQuestion={this.toNextQuestion}
- currentGroup={this.state.currentGroup}
- getUserVotes={this.getUserVotes}
- voteRatios={this.state.voteRatios}
- points={this.state.points}
- setData={this.setData}
- setAnswer={this.state.currentAnswerIndex === null ? this.setAnswer : () => {} } />; // eslint-disable-line no-empty-function
- break; }
- case 'finalscreen':
- outputContent = <FinalScreen
- {...content}
- navigate={this.navigate}
- isFetching={this.state.isFetching} />;
- break;
- case 'titlescreen':
- default:
- outputContent = <TitleScreen
- {...content}
- navigate={ this.navigate }
- navigateTo='question'
- isFetching={this.state.isFetching} />;
- break;
- }
- return outputContent;
- }
- }
|