Index.jsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import { h, render, Component } from 'preact'; // eslint-disable-line no-unused-vars
  2. // content and config:
  3. import content from '../content/module.json';
  4. import offlineQuestions from '../content/offline'; // two-dim. array questions[group][a=1|b=2|c=3]
  5. import config from '../config';
  6. // services and utilities:
  7. import api from '../utilities/api';
  8. import { fixedDigits } from '../utilities/formatter';
  9. // screens and items:
  10. import TitleScreen from './TitleScreen.jsx';
  11. import FinalScreen from './FinalScreen.jsx';
  12. import QuestionScreen from './QuestionScreen.jsx';
  13. /**
  14. * titlescreen -> questionscreen (*n) -> finalscreen
  15. */
  16. export default class App extends Component {
  17. // construct and initialize functions
  18. constructor (props) {
  19. super(props);
  20. this.state = {
  21. route: 'titlescreen', // current screen
  22. chartMode: 'question', // current chart mode: question || score
  23. currentGroup: 0, // current group
  24. currentQuestion: 0, // current question in group
  25. currentAnswerIndex: null, // index in question pair of the currently set answer
  26. currentAnswerCorrect: false, // is the currently set question the correct one?
  27. isFetching: false, // currently performing XHR?
  28. userId: null, // current user id for posting
  29. token: null, // current access token
  30. voteRatios: null, // get votes from other users
  31. points: {} // get correct and incorrect points
  32. };
  33. // context binding
  34. this.navigate = this.navigate.bind(this);
  35. this.toNextQuestion = this.toNextQuestion.bind(this);
  36. this.setAnswer = this.setAnswer.bind(this);
  37. this.setData = this.setData.bind(this);
  38. this.drawQuestions = this.drawQuestions.bind(this);
  39. this.getUserVotes = this.getUserVotes.bind(this);
  40. this.jumpTo = this.jumpTo.bind(this);
  41. this.setInitialState = this.setInitialState.bind(this);
  42. this.getFeedbackCoordinates = this.getFeedbackCoordinates.bind(this);
  43. }
  44. // initial setup, get accesToken and userId
  45. componentDidMount () {
  46. // register hashchange handler.
  47. // Needs more work: reset state, otherwise artefacts will appear.
  48. // Without event handler the site has to be reloaded after hash change
  49. // onhashchange = this.jumpTo;
  50. if (this.props.isOffline) {
  51. this.setState({ questions: offlineQuestions });
  52. this.jumpTo();
  53. } else {
  54. this.setState({ isFetching: true });
  55. api.getToken().then(accessToken => {
  56. this.setState({ token: accessToken });
  57. // create user
  58. api.createUser(accessToken)
  59. .then(user => {
  60. this.setState({
  61. userId: user.userId,
  62. isFetching: false
  63. });
  64. // draw first question
  65. this.drawQuestions();
  66. });
  67. });
  68. }
  69. }
  70. /**
  71. * Navigation using window.location.hash, else go to title screen
  72. *
  73. * @route: The state's route (string). See switch statement in render function
  74. * @currentGroup: 1-based index of current 'question group'
  75. * @currentQuestion: 1-based index of current question
  76. *
  77. * example: .../#question_2_1
  78. */
  79. jumpTo () {
  80. if (window.location.hash) {
  81. const hashInfo = window.location.hash.replace('#', '').split('_');
  82. this.setState({
  83. route: hashInfo[0],
  84. currentGroup: parseInt(hashInfo[1]) - 1 || 0,
  85. currentQuestion: parseInt(hashInfo[2]) - 1 || 0
  86. });
  87. }
  88. }
  89. // navigate to specific route
  90. navigate (route) {
  91. if (route === 'titlescreen') this.setInitialState();
  92. this.setState({ route });
  93. }
  94. // set data
  95. setData (isCorrect) {
  96. // handle continue button visibility
  97. this.setState({ currentAnswerIndex: isCorrect ? 1 : null });
  98. }
  99. // get number of questions
  100. getNumberQuestions () {
  101. let sum = 0;
  102. for (let i = 0; i < this.state.questions.length; i += 1) {
  103. sum += this.state.questions[i].length;
  104. }
  105. return sum;
  106. }
  107. // get question set from api
  108. drawQuestions () {
  109. const questionSet = [];
  110. api.get(config.api.list)
  111. .then(json => {
  112. [ ...json ].forEach(item => {
  113. if (questionSet.length < item.gruppe) {
  114. questionSet.push([]);
  115. }
  116. questionSet[item.gruppe - 1].push(item);
  117. });
  118. this.setState({ questions: questionSet });
  119. this.jumpTo();
  120. });
  121. }
  122. // update state depending on chart mode and current question
  123. toNextQuestion () {
  124. // Show "score screen" after each question of first and second question groups in online mode
  125. if (this.state.chartMode === 'question' && this.state.currentGroup < 2 && !this.props.isOffline) {
  126. this.setState({ chartMode: 'score', points: {} });
  127. // Else, if exists, display next question in group
  128. } else if (this.state.currentQuestion < this.state.questions[this.state.currentGroup].length - 1) {
  129. this.setState({
  130. route: 'question',
  131. chartMode: 'question',
  132. currentQuestion: this.state.currentQuestion + 1,
  133. currentAnswerIndex: null,
  134. points: {},
  135. voteRatios: null
  136. });
  137. // Else, if exists, jump to fist question in next group
  138. } else if (this.state.currentGroup < this.state.questions.length - 1) {
  139. this.setState({
  140. route: 'question',
  141. chartMode: 'question',
  142. currentGroup: this.state.currentGroup + 1,
  143. currentQuestion: 0,
  144. currentAnswerIndex: null,
  145. voteRatios: null
  146. });
  147. // Else show "final screen"
  148. } else {
  149. // go to finalscreen
  150. this.setState({ route: 'finalscreen' });
  151. }
  152. }
  153. // set initial state (for start and restart)
  154. setInitialState () {
  155. this.setState({
  156. route: 'titlescreen',
  157. chartMode: 'question',
  158. currentGroup: 0,
  159. currentQuestion: 0,
  160. currentAnswerIndex: null,
  161. voteRatios: null
  162. });
  163. }
  164. /**
  165. * Get coordinates for solution and, if applicable, incorrect answer.
  166. *
  167. * @param question: Data structure defining the question
  168. * @param answer: Answer selected by user
  169. * @return: Coordinates of solution, and answer, if incorrect
  170. */
  171. getFeedbackCoordinates (question, answer) {
  172. const points = { correct: null, incorrect: null };
  173. const index = question.antworten.indexOf(answer);
  174. let correctAnswer;
  175. if (!answer.korrekt) {
  176. correctAnswer = question.antworten.find(a => a.korrekt === true);
  177. } else {
  178. correctAnswer = answer;
  179. }
  180. points.correct = correctAnswer.coordinates;
  181. // If answer is not correct display both solution, and answer
  182. if (!answer.korrekt) {
  183. points.incorrect = question.antworten[index].coordinates;
  184. }
  185. return points;
  186. }
  187. /**
  188. * Submit answer to API, get percentage of correct answers by other users (multiple choice questions)
  189. * By convention (react / preact), the render function is automatically invoked after this.setState(...) has been called
  190. *
  191. * @param answer: Answer selected by user
  192. */
  193. setAnswer (answer) {
  194. const question = this.state.questions[this.state.currentGroup][this.state.currentQuestion];
  195. const cAnswerIndex = question.antworten.indexOf(answer);
  196. let prepareState = {
  197. currentAnswerIndex: cAnswerIndex,
  198. currentAnswerCorrect: answer.korrekt
  199. };
  200. if (this.state.currentGroup === 0) { // only search for result coordinates for first question group.
  201. prepareState.points = this.getFeedbackCoordinates(question, answer);
  202. }
  203. if (!this.props.isOffline) {
  204. const payload = {
  205. userId: this.state.userId,
  206. aufgabeId: question.id,
  207. antwort: question.antworten[cAnswerIndex].antwort,
  208. korrekt: answer.korrekt
  209. };
  210. api.post(config.api.create, payload, this.state.token)
  211. .then(() => {
  212. this.getUserVotes(question.id).then(
  213. json => {
  214. prepareState.voteRatios = [ json.auswertung.korrekt, json.auswertung.inkorrekt ];
  215. this.setState(prepareState);
  216. }
  217. );
  218. });
  219. } else {
  220. this.setState(prepareState);
  221. }
  222. }
  223. // get user votes by question id
  224. getUserVotes (aufgabeId) {
  225. return api.get(config.api.proportions, { aufgabeId });
  226. }
  227. // RENDER
  228. render () {
  229. let outputContent;
  230. switch (this.state.route) {
  231. case 'question': {
  232. // calculate number of questions from nested questions array
  233. const total = this.state.questions.map(e => e.length).reduce((s, e) => (s + e));
  234. // calculate current question 'index' (positive number)
  235. let index = 0;
  236. for (let i = 0; i < this.state.currentGroup; i += 1) {
  237. index += this.state.questions[i].length;
  238. }
  239. index += this.state.currentQuestion + 1;
  240. const headerState = `${fixedDigits(index, 2)}/${total}`;
  241. outputContent = <QuestionScreen
  242. {...content}
  243. route={this.state.route}
  244. chartMode={this.state.chartMode}
  245. headerState={headerState}
  246. questions={this.state.questions[this.state.currentGroup]}
  247. currentQuestion={this.state.currentQuestion}
  248. currentAnswerIndex={this.state.currentAnswerIndex}
  249. currentAnswerCorrect={this.state.currentAnswerCorrect}
  250. toNextQuestion={this.toNextQuestion}
  251. currentGroup={this.state.currentGroup}
  252. getUserVotes={this.getUserVotes}
  253. voteRatios={this.state.voteRatios}
  254. points={this.state.points}
  255. setData={this.setData}
  256. setAnswer={this.state.currentAnswerIndex === null ? this.setAnswer : () => {} } />; // eslint-disable-line no-empty-function
  257. break; }
  258. case 'finalscreen':
  259. outputContent = <FinalScreen
  260. {...content}
  261. navigate={this.navigate}
  262. isFetching={this.state.isFetching} />;
  263. break;
  264. case 'titlescreen':
  265. default:
  266. outputContent = <TitleScreen
  267. {...content}
  268. navigate={ this.navigate }
  269. navigateTo='question'
  270. isFetching={this.state.isFetching} />;
  271. break;
  272. }
  273. return outputContent;
  274. }
  275. }