├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ ├── bug.md │ ├── code-refactor.md │ └── feature-request.md ├── .gitignore ├── .gitpod.yml ├── .postcssrc.js ├── LICENSE ├── MvpFeatures.md ├── README.md ├── UserJourney.md ├── animations ├── clap.json └── correct-check.json ├── babel.config.js ├── credits.md ├── logo.png ├── package.json ├── public └── icons │ ├── apple-touch-icon.png │ ├── favicon-128x128.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── github-28x28.png │ ├── opencollective-28x28.png │ └── paypal-28x28.png ├── quasar.conf.js ├── src ├── App.vue ├── assets │ ├── animations │ │ ├── clap.json │ │ └── correct-check.json │ ├── basic.svg │ ├── calculate.svg │ ├── difficulty-icons │ │ ├── advanced.svg │ │ ├── basic.svg │ │ └── normal.svg │ ├── logo.svg │ ├── quasar-logo-full.svg │ └── sad.svg ├── boot │ ├── .gitkeep │ ├── composition-api.ts │ └── i18n.ts ├── components │ ├── ActionButton.vue │ ├── ChallengeHeader.vue │ ├── ChallengeStreak.vue │ ├── ClassicChallenge.vue │ ├── ClassicInput.vue │ ├── ClassicQuestion.vue │ ├── ConceptPicker.vue │ ├── ConceptPickerButton.vue │ ├── CustomizePracticeCard.vue │ ├── DifficultyPicker.vue │ ├── DonationDialog.vue │ ├── MaterialMathIcon.vue │ ├── ModePicker.vue │ ├── PracticeSessionProgress.vue │ ├── ValuePicker.vue │ └── models.ts ├── css │ ├── app.sass │ └── quasar.variables.sass ├── engine │ ├── math_questions │ │ ├── expression.ts │ │ ├── expression │ │ │ ├── addition.ts │ │ │ ├── generator.ts │ │ │ ├── models.ts │ │ │ ├── multiplication.ts │ │ │ ├── subtraction.ts │ │ │ └── utils.ts │ │ └── factorization.ts │ ├── models │ │ └── math_question.ts │ └── utils.ts ├── env.d.ts ├── i18n │ ├── en-us │ │ └── index.ts │ └── index.ts ├── index.template.html ├── layouts │ └── MainLayout.vue ├── pages │ ├── CustomizePracticePage.vue │ ├── Error404.vue │ ├── HomePage.vue │ └── PracticePage.vue ├── router │ ├── index.ts │ └── routes.ts ├── shims-vue.d.ts ├── store │ ├── index.ts │ ├── module-example │ │ ├── actions.ts │ │ ├── getters.ts │ │ ├── index.ts │ │ ├── mutations.ts │ │ └── state.ts │ ├── practice │ │ └── practice.ts │ └── store-flag.d.ts ├── types │ └── svg.d.ts └── vue-katex.d.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Please use this template to file a bug you find on our website! 4 | title: "\U0001F41B Bug: {{Enter Bug Title}}" 5 | labels: 'Type: Bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ### Expected Behavior 🧭 13 | 14 | 15 | (Please write your answer here.) 16 | 17 | 18 | ### Current Behavior 🔍 19 | 20 | 21 | (Please write your answer here.) 22 | 23 | ### Steps to Reproduce 🔢 24 | 25 | 26 | (Please write your answer here.) 27 | 28 | 1. 29 | 2. 30 | 3. 31 | 4. 32 | 33 | 34 | **Acceptence Criteria for Fix ✅** 35 | 36 | 37 | 38 | (Please write your answer here.) 39 | 40 | 41 | - [ ] 42 | - [ ] 43 | - [ ] 44 | 45 | ### Possible Solution 🛠️ 46 | 47 | 48 | (Please write your answer here.) 49 | 50 | 51 | **Implementation Details 🧰** 52 | 53 | 54 | (Please write your answer here.) 55 | 56 | 57 | - 58 | - 59 | - 60 | 61 | ### Additional details ℹ️ 62 | 63 | 67 | 68 | (Please write your answer here.) 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/code-refactor.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Code Refactor 3 | about: Use this template to propose enhancements to the codebase 4 | title: "\U0001F6E0️ Refactor: {{Enter Title}}" 5 | labels: 'Domain: Dev Experience, Role: Software Engineer' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Motivation 🏁 11 | 12 | 17 | 18 | (Please write your motivation here.) 19 | 20 | ### Describe your refactoring solution 🛠️ 21 | 22 | 25 | 26 | (Please describe your proposed solution here.) 27 | 28 | ### Additional details ℹ️ 29 | 30 | 34 | 35 | (Please provide additional details here.) 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Please use this template to request a new feature 4 | title: "\U0001F680 Feature Request: {{Feature Request Title}}" 5 | labels: 'Type: Enhancement' 6 | assignees: '' 7 | --- 8 | 9 | ### Problem Overview 👁️‍🗨️ 10 | 11 | 12 | 13 | (Please write your answer here.) 14 | 15 | ### What would you like? 🧰 16 | 17 | 18 | 19 | (Please describe your proposed solution here.) 20 | 21 | ### What alternatives have you considered? 🔍 22 | 23 | 26 | 27 | (Please write your answer here.) 28 | 29 | ### Additional details ℹ️ 30 | 31 | 35 | 36 | (Please write your answer here.) 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | .history 23 | 24 | .DS_Store 25 | .thumbs.db 26 | node_modules 27 | 28 | # Quasar core related directories 29 | .quasar 30 | /dist 31 | 32 | # Cordova related directories and files 33 | /src-cordova/node_modules 34 | /src-cordova/platforms 35 | /src-cordova/plugins 36 | /src-cordova/www 37 | 38 | # Capacitor related directories and files 39 | /src-capacitor/www 40 | /src-capacitor/node_modules 41 | 42 | # BEX related directories and files 43 | /src-bex/www 44 | /src-bex/js/core 45 | 46 | # Log files 47 | npm-debug.log* 48 | yarn-debug.log* 49 | yarn-error.log* 50 | 51 | # Editor directories and files 52 | .idea 53 | *.suo 54 | *.ntvs* 55 | *.njsproj 56 | *.sln -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: yarn run setup 3 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | plugins: [ 5 | // to edit target browsers: use "browserslist" field in package.json 6 | require('autoprefixer') 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Grey Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MvpFeatures.md: -------------------------------------------------------------------------------- 1 | # MVP Features 2 | 3 | ## Beautiful Splash Screen -P2 4 | 5 | A beautiful splash screen will give a pleasant feeling to users as they enter the app. 6 | 7 | ## Customize Practice Session on Home Screen - P1 8 | 9 | ### Time or # of Questions Picker - P1 10 | Users need to define when their session can end. 11 | 12 | ### Difficulty Picker - P1 13 | All different levels of users need to be able to practice. 14 | 15 | ### Concepts Picker - P1 16 | Users need to be able to choose which concepts they want to practice. 17 | 18 | Next to each concept, you’ll have a level indicating your strength in that concept. 19 | 20 | I see a button on the right side of each concept that takes me to the stats for that concept. My recent high-scores and my recent progress. 21 | 22 | ## Stats Page - P2 23 | 24 | Stats will be counted across all concepts, and we’ll present aggregated as well as individual concept stats. 25 | 26 | ### Percentage Of Correct Questions -P2 27 | 28 | # of Questions answered -P2 29 | 30 | ## Time spent practicing -P2 31 | 32 | ## Profile Page -P1 33 | 34 | The profile page will display all the user settings. It will give them the ability to change any of their information. 35 | 36 | ## User Gamification -P3 37 | 38 | ###Day Streaks -P3 39 | \# of consecutive days that the user has practiced. 40 | 41 | ### XP Levels -P3 42 | The user’s level increases as they gain more XP. 43 | 44 | ## Practice Session -P1 45 | 46 | Displays one questions at a time with the specified features below: 47 | 48 | ### Current Streak Count -P2 49 | Displays the current streak count. 50 | 51 | ### Timer or Progress Bar -P1 52 | Shows the time left or the questions that are left. 53 | 54 | ### Skip Button -P2 55 | This allows you to skip a question. 56 | 57 | ### Animated Feedback for Correct/Incorrect Answers - P2 58 | 59 | ## Post Practice Session -P1 60 | 61 | ### XP Points Gained -P3 62 | 63 | ### Report -P1 64 | Gives detailed information on how your session was. The percentage of questions you got right, time taken, concepts practiced. 65 | 66 | You could forward this report to someone keeping you accountable. (This can be automated). 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | Material Math Icon 5 | 6 |
7 | 8 | # Material Math 9 | 10 | 11 | [![Netlify Status](https://api.netlify.com/api/v1/badges/ec96054f-9705-4ecb-bdce-f12b42b3e7fc/deploy-status)](https://app.netlify.com/sites/material-math/deploys) 12 | 13 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://gitlab.com/grey-software/Material-Math) 14 | 15 | Material Math brings unlimited mental math practice in a fun, beautiful interface to the web! 16 | 17 | ## Status 18 | 19 | This project is currently on standby. 20 | 21 | 22 | ## Development setup 23 | 24 | This script installs the necessary dependencies for the app and docs packages. 25 | 26 | ```sh 27 | yarn setup 28 | ``` 29 | 30 | ## Running Material Math 31 | 32 | ```sh 33 | yarn dev 34 | ``` 35 | 36 | ### The spiritual successor to Boltz 37 | 38 | Material Math is the spiritual successor to Boltz, a mental math app for Android. Grey software is building off of Boltz's great work, and bringing Boltz's principles of unlimited fun math practice, interleaved concepts, and spaced repition to the web! 39 | 40 | 41 | [![Boltz Promo Video](https://i.ytimg.com/vi/ceACiAdXSDc/hq720.jpg)](https://www.youtube.com/watch?v=ceACiAdXSDc) 42 | 43 | -------------------------------------------------------------------------------- /UserJourney.md: -------------------------------------------------------------------------------- 1 | # Personas 2 | 3 | **As a math enthusiast, I want to be able to practice mental math in a fun interface.** 4 | 5 | **As a student who isn't allowed to use a calculator in tests and exams, I'd like to improve my mental math to spend less time on computation.** 6 | 7 | I am busy with other school work and I would like to be reminded in a systematic manner to practice daily/at an interval of my choosing. 8 | 9 | **As a parent of a young learner, I would like to equip my child with mental math ability in an encouraging, fun environment.** 10 | 11 | **As a competitive person, I'd like to be able to compete in the arena of mental math.** 12 | 13 | # User Journey 14 | 15 | I enter the app, and I’m presented with a beautiful splash screen. On the home page, I can select the concepts I want to practice, the time I’d like to practice for, and the difficulty I want to set for each concept. 16 | 17 | On my Homepage I see a button on the right side of each concept that takes me to the stats for that concept. My recent high-scores and my recent progress. 18 | 19 | I can navigate to the profile, stats, and leaderboards pages from the home page. 20 | 21 | ## Free Mode 22 | I can select the math concepts I want to practice, the difficulty, and the number of questions, or time that I want to spend practicing. 23 | 24 | ### Practice Session 25 | I start the practice session, and see one question at a time. I can see my highest streak count, and either a timer or a progress bar. I can skip a question if it’s too hard for me. 26 | 27 | ### Post Practice Session 28 | 29 | After the session, I get a summary of the concepts I practiced, xp I gained, etc. 30 | 31 | I get the ability to generate a report, and share my practice session details. I go back to the home page with the previous session’s config already selected. 32 | 33 | 34 | ## Arcade Mode 35 | I can see a set number of configurations for arcade mode, like ‘Basic Arithmetic’, ‘Trigonometry’, or ‘Fundamental Algebra’, and I can see my high scores for each mode. I can also see a Leaderboard that shows the high scores of other users for each config. 36 | 37 | ### Practice Session 38 | I start the practice session, and see one question at a time. I can see my highest streak count, a timer and a progress bar displaying my previous high score and the bar increases as you reach closer. I see a pass button that passes the question, which comes again. 39 | 40 | ### Post-Practice Session 41 | After the game ends, I see a screen that compares my current score with the high score, and presents an animation accordingly. 42 | 43 | If I’m online, I see my score’s rank in the leaderboards, and I can restart with one click. 44 | 45 | There’s a button to send my config to my friend and challenge them to beat my high score. 46 | 47 | # Questions to Address 48 | 49 | ## How do we ensure this remains fun and engaging? 50 | 51 | - Add streaks during the practice session 52 | - Add XP popups like Boltz 53 | - Add fun Lottie animations when there are correct answers 54 | -------------------------------------------------------------------------------- /animations/correct-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "v": "4.10.1", 3 | "fr": 30, 4 | "ip": 0, 5 | "op": 25, 6 | "w": 194, 7 | "h": 195, 8 | "nm": "コンポ 1", 9 | "ddd": 0, 10 | "assets": [], 11 | "layers": [ 12 | { 13 | "ddd": 0, 14 | "ind": 1, 15 | "ty": 4, 16 | "nm": "シェイプレイヤー 2", 17 | "sr": 1, 18 | "ks": { 19 | "o": { 20 | "a": 0, 21 | "k": 100, 22 | "ix": 11 23 | }, 24 | "r": { 25 | "a": 0, 26 | "k": 0, 27 | "ix": 10 28 | }, 29 | "p": { 30 | "a": 0, 31 | "k": [ 32 | 80.5, 33 | 103, 34 | 0 35 | ], 36 | "ix": 2 37 | }, 38 | "a": { 39 | "a": 0, 40 | "k": [ 41 | 0, 42 | 0, 43 | 0 44 | ], 45 | "ix": 1 46 | }, 47 | "s": { 48 | "a": 0, 49 | "k": [ 50 | 104, 51 | 104, 52 | 100 53 | ], 54 | "ix": 6 55 | } 56 | }, 57 | "ao": 0, 58 | "shapes": [ 59 | { 60 | "ty": "gr", 61 | "it": [ 62 | { 63 | "ind": 0, 64 | "ty": "sh", 65 | "ix": 1, 66 | "ks": { 67 | "a": 0, 68 | "k": { 69 | "i": [ 70 | [ 71 | 0, 72 | 0 73 | ], 74 | [ 75 | 0, 76 | 0 77 | ], 78 | [ 79 | 0, 80 | 0 81 | ] 82 | ], 83 | "o": [ 84 | [ 85 | 0, 86 | 0 87 | ], 88 | [ 89 | 0, 90 | 0 91 | ], 92 | [ 93 | 0, 94 | 0 95 | ] 96 | ], 97 | "v": [ 98 | [ 99 | -38.5, 100 | 3 101 | ], 102 | [ 103 | -7.5, 104 | 32.5 105 | ], 106 | [ 107 | 66, 108 | -38 109 | ] 110 | ], 111 | "c": false 112 | }, 113 | "ix": 2 114 | }, 115 | "nm": "パス 1", 116 | "mn": "ADBE Vector Shape - Group", 117 | "hd": false 118 | }, 119 | { 120 | "ty": "st", 121 | "c": { 122 | "a": 0, 123 | "k": [ 124 | 0.149019607843, 125 | 0.717647058824, 126 | 0.301960784314, 127 | 1 128 | ], 129 | "ix": 3 130 | }, 131 | "o": { 132 | "a": 0, 133 | "k": 100, 134 | "ix": 4 135 | }, 136 | "w": { 137 | "a": 0, 138 | "k": 12, 139 | "ix": 5 140 | }, 141 | "lc": 1, 142 | "lj": 1, 143 | "ml": 4, 144 | "nm": "線 1", 145 | "mn": "ADBE Vector Graphic - Stroke", 146 | "hd": false 147 | }, 148 | { 149 | "ty": "tr", 150 | "p": { 151 | "a": 0, 152 | "k": [ 153 | 0, 154 | 0 155 | ], 156 | "ix": 2 157 | }, 158 | "a": { 159 | "a": 0, 160 | "k": [ 161 | 0, 162 | 0 163 | ], 164 | "ix": 1 165 | }, 166 | "s": { 167 | "a": 0, 168 | "k": [ 169 | 100, 170 | 100 171 | ], 172 | "ix": 3 173 | }, 174 | "r": { 175 | "a": 0, 176 | "k": 0, 177 | "ix": 6 178 | }, 179 | "o": { 180 | "a": 0, 181 | "k": 100, 182 | "ix": 7 183 | }, 184 | "sk": { 185 | "a": 0, 186 | "k": 0, 187 | "ix": 4 188 | }, 189 | "sa": { 190 | "a": 0, 191 | "k": 0, 192 | "ix": 5 193 | }, 194 | "nm": "トランスフォーム" 195 | } 196 | ], 197 | "nm": "シェイプ 1", 198 | "np": 3, 199 | "cix": 2, 200 | "ix": 1, 201 | "mn": "ADBE Vector Group", 202 | "hd": false 203 | }, 204 | { 205 | "ty": "tm", 206 | "s": { 207 | "a": 0, 208 | "k": 0, 209 | "ix": 1 210 | }, 211 | "e": { 212 | "a": 1, 213 | "k": [ 214 | { 215 | "i": { 216 | "x": [ 217 | 0.667 218 | ], 219 | "y": [ 220 | 1 221 | ] 222 | }, 223 | "o": { 224 | "x": [ 225 | 0.721 226 | ], 227 | "y": [ 228 | -0.003 229 | ] 230 | }, 231 | "n": [ 232 | "0p667_1_0p721_-0p003" 233 | ], 234 | "t": 0, 235 | "s": [ 236 | 0 237 | ], 238 | "e": [ 239 | 100 240 | ] 241 | }, 242 | { 243 | "t": 14 244 | } 245 | ], 246 | "ix": 2 247 | }, 248 | "o": { 249 | "a": 0, 250 | "k": 0, 251 | "ix": 3 252 | }, 253 | "m": 1, 254 | "ix": 2, 255 | "nm": "パスのトリミング 1", 256 | "mn": "ADBE Vector Filter - Trim", 257 | "hd": false 258 | } 259 | ], 260 | "ip": 0, 261 | "op": 25, 262 | "st": 0, 263 | "bm": 0 264 | }, 265 | { 266 | "ddd": 0, 267 | "ind": 2, 268 | "ty": 4, 269 | "nm": "シェイプレイヤー 1", 270 | "sr": 1, 271 | "ks": { 272 | "o": { 273 | "a": 0, 274 | "k": 100, 275 | "ix": 11 276 | }, 277 | "r": { 278 | "a": 0, 279 | "k": 0, 280 | "ix": 10 281 | }, 282 | "p": { 283 | "a": 0, 284 | "k": [ 285 | 88.5, 286 | 102, 287 | 0 288 | ], 289 | "ix": 2 290 | }, 291 | "a": { 292 | "a": 0, 293 | "k": [ 294 | 0, 295 | 0, 296 | 0 297 | ], 298 | "ix": 1 299 | }, 300 | "s": { 301 | "a": 0, 302 | "k": [ 303 | 105, 304 | 105, 305 | 100 306 | ], 307 | "ix": 6 308 | } 309 | }, 310 | "ao": 0, 311 | "shapes": [ 312 | { 313 | "ty": "gr", 314 | "it": [ 315 | { 316 | "d": 1, 317 | "ty": "el", 318 | "s": { 319 | "a": 0, 320 | "k": [ 321 | 245.992, 322 | 245.992 323 | ], 324 | "ix": 2 325 | }, 326 | "p": { 327 | "a": 0, 328 | "k": [ 329 | 0, 330 | 0 331 | ], 332 | "ix": 3 333 | }, 334 | "nm": "楕円形パス 1", 335 | "mn": "ADBE Vector Shape - Ellipse", 336 | "hd": false 337 | }, 338 | { 339 | "ty": "st", 340 | "c": { 341 | "a": 0, 342 | "k": [ 343 | 0.149019607843, 344 | 0.717647058824, 345 | 0.301960784314, 346 | 1 347 | ], 348 | "ix": 3 349 | }, 350 | "o": { 351 | "a": 0, 352 | "k": 100, 353 | "ix": 4 354 | }, 355 | "w": { 356 | "a": 0, 357 | "k": 12, 358 | "ix": 5 359 | }, 360 | "lc": 1, 361 | "lj": 1, 362 | "ml": 4, 363 | "nm": "線 1", 364 | "mn": "ADBE Vector Graphic - Stroke", 365 | "hd": false 366 | }, 367 | { 368 | "ty": "tr", 369 | "p": { 370 | "a": 0, 371 | "k": [ 372 | 7.992, 373 | -4.004 374 | ], 375 | "ix": 2 376 | }, 377 | "a": { 378 | "a": 0, 379 | "k": [ 380 | 0, 381 | 0 382 | ], 383 | "ix": 1 384 | }, 385 | "s": { 386 | "a": 0, 387 | "k": [ 388 | 70, 389 | 70 390 | ], 391 | "ix": 3 392 | }, 393 | "r": { 394 | "a": 0, 395 | "k": 0, 396 | "ix": 6 397 | }, 398 | "o": { 399 | "a": 0, 400 | "k": 100, 401 | "ix": 7 402 | }, 403 | "sk": { 404 | "a": 0, 405 | "k": 0, 406 | "ix": 4 407 | }, 408 | "sa": { 409 | "a": 0, 410 | "k": 0, 411 | "ix": 5 412 | }, 413 | "nm": "トランスフォーム" 414 | } 415 | ], 416 | "nm": "楕円形 1", 417 | "np": 3, 418 | "cix": 2, 419 | "ix": 1, 420 | "mn": "ADBE Vector Group", 421 | "hd": false 422 | }, 423 | { 424 | "ty": "tm", 425 | "s": { 426 | "a": 0, 427 | "k": 0, 428 | "ix": 1 429 | }, 430 | "e": { 431 | "a": 1, 432 | "k": [ 433 | { 434 | "i": { 435 | "x": [ 436 | 0.667 437 | ], 438 | "y": [ 439 | 1 440 | ] 441 | }, 442 | "o": { 443 | "x": [ 444 | 1 445 | ], 446 | "y": [ 447 | 0 448 | ] 449 | }, 450 | "n": [ 451 | "0p667_1_1_0" 452 | ], 453 | "t": 11, 454 | "s": [ 455 | 0 456 | ], 457 | "e": [ 458 | 100 459 | ] 460 | }, 461 | { 462 | "t": 24 463 | } 464 | ], 465 | "ix": 2 466 | }, 467 | "o": { 468 | "a": 0, 469 | "k": 0, 470 | "ix": 3 471 | }, 472 | "m": 1, 473 | "ix": 2, 474 | "nm": "パスのトリミング 1", 475 | "mn": "ADBE Vector Filter - Trim", 476 | "hd": false 477 | } 478 | ], 479 | "ip": 0, 480 | "op": 25, 481 | "st": 0, 482 | "bm": 0 483 | } 484 | ] 485 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | presets: [ 4 | '@quasar/babel-preset-app' 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /credits.md: -------------------------------------------------------------------------------- 1 | Icons made by Pixel perfect from www.flaticon.com 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grey-software/Material-Math/ade04f6e1e38b9cf5a0c9085bea3bbe84b254fca/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-math", 3 | "version": "0.5.0", 4 | "description": "Practice Math problems in a fun, beautiful, material experience!", 5 | "productName": "Material Math", 6 | "cordovaId": "software.grey.materialmath", 7 | "capacitorId": "", 8 | "author": "org@grey.software", 9 | "private": true, 10 | "scripts": { 11 | "setup": "yarn", 12 | "dev": "quasar dev", 13 | "build": "quasar build", 14 | "test": "echo \"No test specified\" && exit 0" 15 | }, 16 | "dependencies": { 17 | "@quasar/extras": "^1.9.10", 18 | "@types/mathjs": "^6.0.7", 19 | "@vue/composition-api": "1.0.0-beta.18", 20 | "katex": "^0.12.0", 21 | "mathjs": "^7.5.1", 22 | "quasar": "^1.14.3", 23 | "typescript-collections": "^1.3.3", 24 | "vue-i18n": "^8.22.1", 25 | "vue-katex": "^0.5.0" 26 | }, 27 | "devDependencies": { 28 | "@quasar/app": "^2.1.6", 29 | "@types/node": "^14.14.6", 30 | "vue-svg-loader": "^0.17.0-beta.2" 31 | }, 32 | "engines": { 33 | "node": ">= 10.18.1", 34 | "npm": ">= 6.13.4", 35 | "yarn": ">= 1.21.1" 36 | }, 37 | "browserslist": [ 38 | "last 1 version, not dead, ie >= 11" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /public/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grey-software/Material-Math/ade04f6e1e38b9cf5a0c9085bea3bbe84b254fca/public/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grey-software/Material-Math/ade04f6e1e38b9cf5a0c9085bea3bbe84b254fca/public/icons/favicon-128x128.png -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grey-software/Material-Math/ade04f6e1e38b9cf5a0c9085bea3bbe84b254fca/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grey-software/Material-Math/ade04f6e1e38b9cf5a0c9085bea3bbe84b254fca/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grey-software/Material-Math/ade04f6e1e38b9cf5a0c9085bea3bbe84b254fca/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grey-software/Material-Math/ade04f6e1e38b9cf5a0c9085bea3bbe84b254fca/public/icons/favicon.ico -------------------------------------------------------------------------------- /public/icons/github-28x28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grey-software/Material-Math/ade04f6e1e38b9cf5a0c9085bea3bbe84b254fca/public/icons/github-28x28.png -------------------------------------------------------------------------------- /public/icons/opencollective-28x28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grey-software/Material-Math/ade04f6e1e38b9cf5a0c9085bea3bbe84b254fca/public/icons/opencollective-28x28.png -------------------------------------------------------------------------------- /public/icons/paypal-28x28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grey-software/Material-Math/ade04f6e1e38b9cf5a0c9085bea3bbe84b254fca/public/icons/paypal-28x28.png -------------------------------------------------------------------------------- /quasar.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file runs in a Node context (it's NOT transpiled by Babel), so use only 3 | * the ES6 features that are supported by your Node version. https://node.green/ 4 | */ 5 | 6 | // Configuration for your app 7 | // https://quasar.dev/quasar-cli/quasar-conf-js 8 | /* eslint-disable @typescript-eslint/no-var-requires */ 9 | /* eslint-disable @typescript-eslint/camelcase */ 10 | const { configure } = require('quasar/wrappers'); 11 | 12 | module.exports = configure(function (/* ctx */) { 13 | return { 14 | // app boot file (/src/boot) 15 | // --> boot files are part of "main.js" 16 | // https://quasar.dev/quasar-cli/cli-documentation/boot-files 17 | boot: [ 18 | 'composition-api', 19 | 'i18n', 20 | ], 21 | 22 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css 23 | css: [ 24 | 'app.sass' 25 | ], 26 | 27 | // https://github.com/quasarframework/quasar/tree/dev/extras 28 | extras: [ 29 | // 'ionicons-v4', 30 | 'mdi-v5', 31 | // 'fontawesome-v5', 32 | // 'eva-icons', 33 | // 'themify', 34 | // 'line-awesome', 35 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 36 | 37 | 'roboto-font', // optional, you are not bound to it 38 | 'material-icons', // optional, you are not bound to it 39 | ], 40 | 41 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework 42 | framework: { 43 | iconSet: 'mdi-v5', // Quasar icon set 44 | lang: 'en-us', // Quasar language pack 45 | 46 | // Possible values for "all": 47 | // * 'auto' - Auto-import needed Quasar components & directives 48 | // (slightly higher compile time; next to minimum bundle size; most convenient) 49 | // * false - Manually specify what to import 50 | // (fastest compile time; minimum bundle size; most tedious) 51 | // * true - Import everything from Quasar 52 | // (not treeshaking Quasar; biggest bundle size; convenient) 53 | all: 'auto', 54 | 55 | components: [], 56 | directives: [], 57 | 58 | // Quasar plugins 59 | plugins: [] 60 | }, 61 | 62 | // https://quasar.dev/quasar-cli/cli-documentation/supporting-ie 63 | supportIE: false, 64 | 65 | // https://quasar.dev/quasar-cli/cli-documentation/supporting-ts 66 | supportTS: true, 67 | 68 | // https://quasar.dev/quasar-cli/cli-documentation/prefetch-feature 69 | // preFetch: true 70 | 71 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build 72 | build: { 73 | vueRouterMode: 'hash', // available values: 'hash', 'history' 74 | 75 | // rtl: false, // https://quasar.dev/options/rtl-support 76 | // preloadChunks: true, 77 | // showProgress: false, 78 | // gzip: true, 79 | // analyze: true, 80 | 81 | // Options below are automatically set depending on the env, set them if you want to override 82 | // extractCSS: false, 83 | 84 | // https://quasar.dev/quasar-cli/cli-documentation/handling-webpack 85 | extendWebpack (cfg) { 86 | /* 87 | * deletes 'svg' from old url-loader regex rule 88 | * old: /\.(png|jpe?g|gif|svg|webp|avif)(\?.*)?$/ 89 | * new: /\.(png|jpe?g|gif|webp|avif)(\?.*)?$/ 90 | */ 91 | cfg.module.rules = cfg.module.rules.map(rule => { 92 | const regstr = rule.test.toString() 93 | if (regstr.includes('svg')) { 94 | const newReg = new RegExp(regstr 95 | .replace('svg', '') // delete all svg mentions 96 | .replace('||', '|') // delete extra '|' operators 97 | .replace('(|', '(') 98 | .replace('|)', ')') 99 | .slice(1, -1) // remove surrounding '/' signs 100 | ) 101 | return { ...rule, test: newReg } 102 | } else { 103 | return rule 104 | } 105 | }) 106 | cfg.module.rules.push({ 107 | test: /\.svg$/, 108 | use: ['vue-loader', 'vue-svg-loader'] 109 | }) 110 | }, 111 | }, 112 | 113 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer 114 | devServer: { 115 | https: false, 116 | port: 8080, 117 | open: true // opens browser window automatically 118 | }, 119 | 120 | // animations: 'all', // --- includes all animations 121 | // https://quasar.dev/options/animations 122 | animations: [], 123 | 124 | // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr 125 | ssr: { 126 | pwa: false 127 | }, 128 | 129 | // https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa 130 | pwa: { 131 | workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest' 132 | workboxOptions: {}, // only for GenerateSW 133 | manifest: { 134 | name: 'Material Math', 135 | short_name: 'Material Math', 136 | description: 'Practice Math problems in a fun, beautiful, material experience!', 137 | display: 'standalone', 138 | orientation: 'portrait', 139 | background_color: '#ffffff', 140 | theme_color: '#027be3', 141 | icons: [ 142 | { 143 | src: '/icons/icon-128x128.png', 144 | sizes: '128x128', 145 | type: 'image/png' 146 | }, 147 | { 148 | src: '/icons/icon-192x192.png', 149 | sizes: '192x192', 150 | type: 'image/png' 151 | }, 152 | { 153 | src: '/icons/icon-256x256.png', 154 | sizes: '256x256', 155 | type: 'image/png' 156 | }, 157 | { 158 | src: '/icons/icon-384x384.png', 159 | sizes: '384x384', 160 | type: 'image/png' 161 | }, 162 | { 163 | src: '/icons/icon-512x512.png', 164 | sizes: '512x512', 165 | type: 'image/png' 166 | } 167 | ] 168 | } 169 | }, 170 | 171 | // Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova 172 | cordova: { 173 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 174 | id: 'software.grey.materialmath' 175 | }, 176 | 177 | // Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor 178 | capacitor: { 179 | hideSplashscreen: true 180 | }, 181 | 182 | // Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron 183 | electron: { 184 | bundler: 'packager', // 'packager' or 'builder' 185 | 186 | packager: { 187 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 188 | 189 | // OS X / Mac App Store 190 | // appBundleId: '', 191 | // appCategoryType: '', 192 | // osxSign: '', 193 | // protocol: 'myapp://path', 194 | 195 | // Windows only 196 | // win32metadata: { ... } 197 | }, 198 | 199 | builder: { 200 | // https://www.electron.build/configuration/configuration 201 | 202 | appId: 'material-math' 203 | }, 204 | 205 | // More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration 206 | nodeIntegration: true, 207 | 208 | extendWebpack (/* cfg */) { 209 | // do something with Electron main process Webpack cfg 210 | // chainWebpack also available besides this extendWebpack 211 | } 212 | } 213 | } 214 | }); 215 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /src/assets/animations/correct-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "v": "4.10.1", 3 | "fr": 30, 4 | "ip": 0, 5 | "op": 25, 6 | "w": 194, 7 | "h": 195, 8 | "nm": "コンポ 1", 9 | "ddd": 0, 10 | "assets": [], 11 | "layers": [ 12 | { 13 | "ddd": 0, 14 | "ind": 1, 15 | "ty": 4, 16 | "nm": "シェイプレイヤー 2", 17 | "sr": 1, 18 | "ks": { 19 | "o": { 20 | "a": 0, 21 | "k": 100, 22 | "ix": 11 23 | }, 24 | "r": { 25 | "a": 0, 26 | "k": 0, 27 | "ix": 10 28 | }, 29 | "p": { 30 | "a": 0, 31 | "k": [ 32 | 80.5, 33 | 103, 34 | 0 35 | ], 36 | "ix": 2 37 | }, 38 | "a": { 39 | "a": 0, 40 | "k": [ 41 | 0, 42 | 0, 43 | 0 44 | ], 45 | "ix": 1 46 | }, 47 | "s": { 48 | "a": 0, 49 | "k": [ 50 | 104, 51 | 104, 52 | 100 53 | ], 54 | "ix": 6 55 | } 56 | }, 57 | "ao": 0, 58 | "shapes": [ 59 | { 60 | "ty": "gr", 61 | "it": [ 62 | { 63 | "ind": 0, 64 | "ty": "sh", 65 | "ix": 1, 66 | "ks": { 67 | "a": 0, 68 | "k": { 69 | "i": [ 70 | [ 71 | 0, 72 | 0 73 | ], 74 | [ 75 | 0, 76 | 0 77 | ], 78 | [ 79 | 0, 80 | 0 81 | ] 82 | ], 83 | "o": [ 84 | [ 85 | 0, 86 | 0 87 | ], 88 | [ 89 | 0, 90 | 0 91 | ], 92 | [ 93 | 0, 94 | 0 95 | ] 96 | ], 97 | "v": [ 98 | [ 99 | -38.5, 100 | 3 101 | ], 102 | [ 103 | -7.5, 104 | 32.5 105 | ], 106 | [ 107 | 66, 108 | -38 109 | ] 110 | ], 111 | "c": false 112 | }, 113 | "ix": 2 114 | }, 115 | "nm": "パス 1", 116 | "mn": "ADBE Vector Shape - Group", 117 | "hd": false 118 | }, 119 | { 120 | "ty": "st", 121 | "c": { 122 | "a": 0, 123 | "k": [ 124 | 0.149019607843, 125 | 0.717647058824, 126 | 0.301960784314, 127 | 1 128 | ], 129 | "ix": 3 130 | }, 131 | "o": { 132 | "a": 0, 133 | "k": 100, 134 | "ix": 4 135 | }, 136 | "w": { 137 | "a": 0, 138 | "k": 12, 139 | "ix": 5 140 | }, 141 | "lc": 1, 142 | "lj": 1, 143 | "ml": 4, 144 | "nm": "線 1", 145 | "mn": "ADBE Vector Graphic - Stroke", 146 | "hd": false 147 | }, 148 | { 149 | "ty": "tr", 150 | "p": { 151 | "a": 0, 152 | "k": [ 153 | 0, 154 | 0 155 | ], 156 | "ix": 2 157 | }, 158 | "a": { 159 | "a": 0, 160 | "k": [ 161 | 0, 162 | 0 163 | ], 164 | "ix": 1 165 | }, 166 | "s": { 167 | "a": 0, 168 | "k": [ 169 | 100, 170 | 100 171 | ], 172 | "ix": 3 173 | }, 174 | "r": { 175 | "a": 0, 176 | "k": 0, 177 | "ix": 6 178 | }, 179 | "o": { 180 | "a": 0, 181 | "k": 100, 182 | "ix": 7 183 | }, 184 | "sk": { 185 | "a": 0, 186 | "k": 0, 187 | "ix": 4 188 | }, 189 | "sa": { 190 | "a": 0, 191 | "k": 0, 192 | "ix": 5 193 | }, 194 | "nm": "トランスフォーム" 195 | } 196 | ], 197 | "nm": "シェイプ 1", 198 | "np": 3, 199 | "cix": 2, 200 | "ix": 1, 201 | "mn": "ADBE Vector Group", 202 | "hd": false 203 | }, 204 | { 205 | "ty": "tm", 206 | "s": { 207 | "a": 0, 208 | "k": 0, 209 | "ix": 1 210 | }, 211 | "e": { 212 | "a": 1, 213 | "k": [ 214 | { 215 | "i": { 216 | "x": [ 217 | 0.667 218 | ], 219 | "y": [ 220 | 1 221 | ] 222 | }, 223 | "o": { 224 | "x": [ 225 | 0.721 226 | ], 227 | "y": [ 228 | -0.003 229 | ] 230 | }, 231 | "n": [ 232 | "0p667_1_0p721_-0p003" 233 | ], 234 | "t": 0, 235 | "s": [ 236 | 0 237 | ], 238 | "e": [ 239 | 100 240 | ] 241 | }, 242 | { 243 | "t": 14 244 | } 245 | ], 246 | "ix": 2 247 | }, 248 | "o": { 249 | "a": 0, 250 | "k": 0, 251 | "ix": 3 252 | }, 253 | "m": 1, 254 | "ix": 2, 255 | "nm": "パスのトリミング 1", 256 | "mn": "ADBE Vector Filter - Trim", 257 | "hd": false 258 | } 259 | ], 260 | "ip": 0, 261 | "op": 25, 262 | "st": 0, 263 | "bm": 0 264 | }, 265 | { 266 | "ddd": 0, 267 | "ind": 2, 268 | "ty": 4, 269 | "nm": "シェイプレイヤー 1", 270 | "sr": 1, 271 | "ks": { 272 | "o": { 273 | "a": 0, 274 | "k": 100, 275 | "ix": 11 276 | }, 277 | "r": { 278 | "a": 0, 279 | "k": 0, 280 | "ix": 10 281 | }, 282 | "p": { 283 | "a": 0, 284 | "k": [ 285 | 88.5, 286 | 102, 287 | 0 288 | ], 289 | "ix": 2 290 | }, 291 | "a": { 292 | "a": 0, 293 | "k": [ 294 | 0, 295 | 0, 296 | 0 297 | ], 298 | "ix": 1 299 | }, 300 | "s": { 301 | "a": 0, 302 | "k": [ 303 | 105, 304 | 105, 305 | 100 306 | ], 307 | "ix": 6 308 | } 309 | }, 310 | "ao": 0, 311 | "shapes": [ 312 | { 313 | "ty": "gr", 314 | "it": [ 315 | { 316 | "d": 1, 317 | "ty": "el", 318 | "s": { 319 | "a": 0, 320 | "k": [ 321 | 245.992, 322 | 245.992 323 | ], 324 | "ix": 2 325 | }, 326 | "p": { 327 | "a": 0, 328 | "k": [ 329 | 0, 330 | 0 331 | ], 332 | "ix": 3 333 | }, 334 | "nm": "楕円形パス 1", 335 | "mn": "ADBE Vector Shape - Ellipse", 336 | "hd": false 337 | }, 338 | { 339 | "ty": "st", 340 | "c": { 341 | "a": 0, 342 | "k": [ 343 | 0.149019607843, 344 | 0.717647058824, 345 | 0.301960784314, 346 | 1 347 | ], 348 | "ix": 3 349 | }, 350 | "o": { 351 | "a": 0, 352 | "k": 100, 353 | "ix": 4 354 | }, 355 | "w": { 356 | "a": 0, 357 | "k": 12, 358 | "ix": 5 359 | }, 360 | "lc": 1, 361 | "lj": 1, 362 | "ml": 4, 363 | "nm": "線 1", 364 | "mn": "ADBE Vector Graphic - Stroke", 365 | "hd": false 366 | }, 367 | { 368 | "ty": "tr", 369 | "p": { 370 | "a": 0, 371 | "k": [ 372 | 7.992, 373 | -4.004 374 | ], 375 | "ix": 2 376 | }, 377 | "a": { 378 | "a": 0, 379 | "k": [ 380 | 0, 381 | 0 382 | ], 383 | "ix": 1 384 | }, 385 | "s": { 386 | "a": 0, 387 | "k": [ 388 | 70, 389 | 70 390 | ], 391 | "ix": 3 392 | }, 393 | "r": { 394 | "a": 0, 395 | "k": 0, 396 | "ix": 6 397 | }, 398 | "o": { 399 | "a": 0, 400 | "k": 100, 401 | "ix": 7 402 | }, 403 | "sk": { 404 | "a": 0, 405 | "k": 0, 406 | "ix": 4 407 | }, 408 | "sa": { 409 | "a": 0, 410 | "k": 0, 411 | "ix": 5 412 | }, 413 | "nm": "トランスフォーム" 414 | } 415 | ], 416 | "nm": "楕円形 1", 417 | "np": 3, 418 | "cix": 2, 419 | "ix": 1, 420 | "mn": "ADBE Vector Group", 421 | "hd": false 422 | }, 423 | { 424 | "ty": "tm", 425 | "s": { 426 | "a": 0, 427 | "k": 0, 428 | "ix": 1 429 | }, 430 | "e": { 431 | "a": 1, 432 | "k": [ 433 | { 434 | "i": { 435 | "x": [ 436 | 0.667 437 | ], 438 | "y": [ 439 | 1 440 | ] 441 | }, 442 | "o": { 443 | "x": [ 444 | 1 445 | ], 446 | "y": [ 447 | 0 448 | ] 449 | }, 450 | "n": [ 451 | "0p667_1_1_0" 452 | ], 453 | "t": 11, 454 | "s": [ 455 | 0 456 | ], 457 | "e": [ 458 | 100 459 | ] 460 | }, 461 | { 462 | "t": 24 463 | } 464 | ], 465 | "ix": 2 466 | }, 467 | "o": { 468 | "a": 0, 469 | "k": 0, 470 | "ix": 3 471 | }, 472 | "m": 1, 473 | "ix": 2, 474 | "nm": "パスのトリミング 1", 475 | "mn": "ADBE Vector Filter - Trim", 476 | "hd": false 477 | } 478 | ], 479 | "ip": 0, 480 | "op": 25, 481 | "st": 0, 482 | "bm": 0 483 | } 484 | ] 485 | } -------------------------------------------------------------------------------- /src/assets/basic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/calculate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/difficulty-icons/advanced.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/difficulty-icons/basic.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/difficulty-icons/normal.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grey-software/Material-Math/ade04f6e1e38b9cf5a0c9085bea3bbe84b254fca/src/assets/logo.svg -------------------------------------------------------------------------------- /src/assets/quasar-logo-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 63 | 66 | 69 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | 113 | 118 | 126 | 133 | 142 | 151 | 160 | 169 | 178 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /src/assets/sad.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/boot/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grey-software/Material-Math/ade04f6e1e38b9cf5a0c9085bea3bbe84b254fca/src/boot/.gitkeep -------------------------------------------------------------------------------- /src/boot/composition-api.ts: -------------------------------------------------------------------------------- 1 | import VueCompositionApi from '@vue/composition-api'; 2 | import { boot } from 'quasar/wrappers'; 3 | 4 | export default boot(({ Vue }) => { 5 | Vue.use(VueCompositionApi); 6 | }); 7 | -------------------------------------------------------------------------------- /src/boot/i18n.ts: -------------------------------------------------------------------------------- 1 | import { boot } from 'quasar/wrappers'; 2 | import messages from 'src/i18n'; 3 | import Vue from 'vue'; 4 | import VueI18n from 'vue-i18n'; 5 | 6 | declare module 'vue/types/vue' { 7 | interface Vue { 8 | i18n: VueI18n; 9 | } 10 | } 11 | 12 | Vue.use(VueI18n); 13 | 14 | export const i18n = new VueI18n({ 15 | locale: "en-us", 16 | fallbackLocale: "en-us", 17 | messages 18 | }); 19 | 20 | export default boot(({ app }) => { 21 | // Set i18n instance on app 22 | app.i18n = i18n; 23 | }); 24 | -------------------------------------------------------------------------------- /src/components/ActionButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 27 | -------------------------------------------------------------------------------- /src/components/ChallengeHeader.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/ChallengeStreak.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /src/components/ClassicChallenge.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 69 | 70 | 96 | -------------------------------------------------------------------------------- /src/components/ClassicInput.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 134 | 135 | -------------------------------------------------------------------------------- /src/components/ClassicQuestion.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 35 | -------------------------------------------------------------------------------- /src/components/ConceptPicker.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 51 | 52 | -------------------------------------------------------------------------------- /src/components/ConceptPickerButton.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 66 | 67 | -------------------------------------------------------------------------------- /src/components/CustomizePracticeCard.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 110 | 111 | 165 | -------------------------------------------------------------------------------- /src/components/DifficultyPicker.vue: -------------------------------------------------------------------------------- 1 | 155 | 156 | 182 | 183 | -------------------------------------------------------------------------------- /src/components/DonationDialog.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 76 | 77 | 87 | -------------------------------------------------------------------------------- /src/components/MaterialMathIcon.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/components/ModePicker.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 83 | 84 | -------------------------------------------------------------------------------- /src/components/PracticeSessionProgress.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/ValuePicker.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 80 | 81 | -------------------------------------------------------------------------------- /src/components/models.ts: -------------------------------------------------------------------------------- 1 | export interface Todo { 2 | id: number; 3 | content: string; 4 | } 5 | 6 | export interface Meta { 7 | totalCount: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/css/app.sass: -------------------------------------------------------------------------------- 1 | // app global css in Sass form 2 | -------------------------------------------------------------------------------- /src/css/quasar.variables.sass: -------------------------------------------------------------------------------- 1 | // Quasar Sass (& SCSS) Variables 2 | // -------------------------------------------------- 3 | // To customize the look and feel of this app, you can override 4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files. 5 | 6 | // Check documentation for full list of Quasar variables 7 | 8 | // Your own variables (that are declared here) and Quasar's own 9 | // ones will be available out of the box in your .vue/.scss/.sass files 10 | 11 | // It's highly recommended to change the default colors 12 | // to match your app's branding. 13 | // Tip: Use the "Theme Builder" on Quasar's documentation website. 14 | 15 | $primary : #1976D2 16 | $secondary : #26A69A 17 | $accent : #9C27B0 18 | 19 | $dark : #1D1D1D 20 | 21 | $positive : #21BA45 22 | $negative : #C10015 23 | $info : #31CCEC 24 | $warning : #F2C037 25 | -------------------------------------------------------------------------------- /src/engine/math_questions/expression.ts: -------------------------------------------------------------------------------- 1 | import { Stack } from 'typescript-collections' 2 | import { 3 | ChallengeParams, 4 | GenerateChallenge, 5 | ChallengeType 6 | } from '../models/math_question' 7 | import { getChallengeTokens } from './expression/generator' 8 | import { 9 | ExprToken, 10 | MathOperations, 11 | Operator, 12 | TokenType 13 | } from './expression/models' 14 | 15 | const getInfixTemplate = (op: Operator, left: string, right: string): string => { 16 | switch (op) { 17 | case Operator.Addition: { 18 | return `(${left}+${right})` 19 | } 20 | case Operator.Subtraction: { 21 | return `(${left}-${right})` 22 | } 23 | case Operator.Multiplication: { 24 | return `(${left}*${right})` 25 | } 26 | } 27 | return '' 28 | } 29 | 30 | const getLatexTemplate = ( 31 | op: Operator, 32 | left: string, 33 | right: string 34 | ): string => { 35 | switch (op) { 36 | case Operator.Addition: { 37 | return `(${left}+${parenthesizeNegative(right)})` 38 | } 39 | case Operator.Subtraction: { 40 | return `(${left}-${parenthesizeNegative(right)})` 41 | } 42 | case Operator.Multiplication: { 43 | return `(${left}\\times${parenthesizeNegative(right)})` 44 | } 45 | } 46 | return '' 47 | } 48 | 49 | const parenthesizeNegative = (num: string): string => { 50 | return num.startsWith('-') ? `${num}` : num 51 | } 52 | 53 | const expressionLatex = (tokens: ExprToken[]): string => { 54 | const exprStack: Stack = new Stack() 55 | for (let i = tokens.length - 1; i >= 0; i--) { 56 | if (tokens[i].type === TokenType.Operator) { 57 | const currentOp = tokens[i].value as Operator 58 | exprStack.push( 59 | getLatexTemplate(currentOp, exprStack.pop()!, exprStack.pop()!) 60 | ) 61 | } else { 62 | exprStack.push(tokens[i].value.toString()) 63 | } 64 | } 65 | return exprStack.pop()! 66 | } 67 | 68 | const expressionInfix = (tokens: ExprToken[]): string => { 69 | const exprStack: Stack = new Stack() 70 | for (let i = tokens.length - 1; i >= 0; i--) { 71 | if (tokens[i].type === TokenType.Operator) { 72 | const currentOp = tokens[i].value as Operator 73 | exprStack.push( 74 | getInfixTemplate(currentOp, exprStack.pop()!, exprStack.pop()!) 75 | ) 76 | } else { 77 | exprStack.push(tokens[i].value.toString()) 78 | } 79 | } 80 | return exprStack.pop()! 81 | } 82 | 83 | export const generateExpressionChallenge: GenerateChallenge = ( 84 | params: ChallengeParams 85 | ) => { 86 | const tokens: ExprToken[] = getChallengeTokens( 87 | params.operators, 88 | params.difficulty 89 | ) 90 | const difficulty = params.difficulty 91 | const exprLatex = expressionLatex(tokens) 92 | return { 93 | difficulty, 94 | infix: expressionInfix(tokens), 95 | latex: exprLatex.substring(1, exprLatex.length - 1), 96 | type: ChallengeType.Expression 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/engine/math_questions/expression/addition.ts: -------------------------------------------------------------------------------- 1 | import { randomNum } from '../../utils' 2 | import { 3 | BoundedComplementTokens, 4 | BoundedExpr, 5 | DifficultyBounds, 6 | Expr, 7 | ExprBound, 8 | ExprToken, 9 | Operator, 10 | SubExprLocation, 11 | SubExprToken, 12 | TokenType 13 | } from './models' 14 | import { isBounded } from './utils' 15 | 16 | const DifficultyBoundsModel: DifficultyBounds = { 17 | basic: { min: 1, max: 10 }, 18 | normal: { min: 5, max: 50 }, 19 | // tslint:disable-next-line:object-literal-sort-keys 20 | advanced: { min: 20, max: 100 } 21 | } 22 | 23 | const getSubExprToken = (expr: Expr): SubExprToken => { 24 | const bounds: ExprBound = DifficultyBoundsModel[expr.diff] 25 | return { 26 | bounded: false, 27 | type: TokenType.SubExpr, 28 | value: randomNum(bounds.min, bounds.max) 29 | } 30 | } 31 | 32 | const _getZeroBoundTokens = (expr: BoundedExpr): ExprToken[] => { 33 | const tokens: ExprToken[] = [] 34 | const zeroToken = { type: TokenType.Number, value: 0 } 35 | const zeroBoundedToken: SubExprToken = { type: TokenType.SubExpr, value: 0, bounded: true } 36 | switch (expr.subExprLocation) { 37 | case SubExprLocation.NEITHER: { 38 | return tokens.concat([zeroToken, zeroToken]) 39 | } 40 | case SubExprLocation.BOTH: { 41 | return tokens.concat([zeroBoundedToken, zeroBoundedToken]) 42 | } 43 | case SubExprLocation.LEFT: { 44 | return tokens.concat([zeroBoundedToken, zeroToken]) 45 | } 46 | case SubExprLocation.RIGHT: { 47 | return tokens.concat([zeroToken, zeroBoundedToken]) 48 | } 49 | } 50 | } 51 | 52 | const _getBoundedComplementTokens = (expr: BoundedExpr): BoundedComplementTokens => { 53 | const bound = expr.bound 54 | const boundedRandom = randomNum(DifficultyBoundsModel[expr.diff].min, bound) 55 | return { 56 | boundedToken: { type: TokenType.Number, value: boundedRandom }, 57 | complementToken: { type: TokenType.Number, value: bound - boundedRandom } 58 | } 59 | } 60 | 61 | const _getRandomToken = (expr: Expr): ExprToken => { 62 | const randomValue = randomNum(DifficultyBoundsModel[expr.diff].min, DifficultyBoundsModel[expr.diff].max) 63 | return { type: TokenType.Number, value: randomValue } 64 | } 65 | 66 | const _getNoSubExprTokens = (expr: Expr | BoundedExpr): ExprToken[] => { 67 | const tokens: ExprToken[] = [] 68 | if (expr.isBounded) { 69 | const bcTokens: BoundedComplementTokens = _getBoundedComplementTokens(expr as BoundedExpr) 70 | return tokens.concat([bcTokens.boundedToken, bcTokens.complementToken]) 71 | } else { 72 | const randomToken = _getRandomToken(expr) 73 | return tokens.concat([randomToken, randomToken]) 74 | } 75 | } 76 | 77 | const _getBothSubExprTokens = (expr: Expr | BoundedExpr): ExprToken[] => { 78 | const tokens: ExprToken[] = [] 79 | if (expr.isBounded) { 80 | const bcTokens: BoundedComplementTokens = _getBoundedComplementTokens(expr as BoundedExpr) 81 | const boundedSubExprToken: SubExprToken = { ...bcTokens.boundedToken, type: TokenType.SubExpr, bounded: true } 82 | return tokens.concat([boundedSubExprToken, bcTokens.complementToken]) 83 | } else { 84 | const subExprToken: SubExprToken = getSubExprToken(expr) 85 | return tokens.concat([subExprToken, subExprToken]) 86 | } 87 | } 88 | 89 | const _getLeftSubExprTokens = (expr: Expr | BoundedExpr): ExprToken[] => { 90 | const tokens: ExprToken[] = [] 91 | if (expr.isBounded) { 92 | const bcTokens: BoundedComplementTokens = _getBoundedComplementTokens(expr as BoundedExpr) 93 | const boundedSubExprToken: SubExprToken = { ...bcTokens.boundedToken, type: TokenType.SubExpr, bounded: true } 94 | return tokens.concat([boundedSubExprToken, bcTokens.complementToken]) 95 | } else { 96 | const subExprToken: SubExprToken = getSubExprToken(expr) 97 | const randomToken: ExprToken = _getRandomToken(expr) 98 | return tokens.concat([subExprToken, randomToken]) 99 | } 100 | } 101 | 102 | const _getRightSubExprTokens = (expr: Expr | BoundedExpr): ExprToken[] => { 103 | const tokens: ExprToken[] = [] 104 | if (expr.isBounded) { 105 | const bcTokens: BoundedComplementTokens = _getBoundedComplementTokens(expr as BoundedExpr) 106 | const boundedSubExprToken: SubExprToken = { ...bcTokens.boundedToken, type: TokenType.SubExpr, bounded: true } 107 | return tokens.concat([boundedSubExprToken, bcTokens.complementToken]) 108 | } else { 109 | const subExprToken: SubExprToken = getSubExprToken(expr) 110 | const randomToken: ExprToken = _getRandomToken(expr) 111 | return tokens.concat([randomToken, subExprToken]) 112 | } 113 | } 114 | 115 | export const getAdditionExprTokens = (expr: Expr): ExprToken[] => { 116 | const tokens: ExprToken[] = [] 117 | tokens.push({ 118 | type: TokenType.Operator, 119 | value: Operator.Addition 120 | }) 121 | if (isBounded(expr) && expr.bound === 0) { 122 | return tokens.concat(_getZeroBoundTokens(expr)) 123 | } 124 | switch (expr.subExprLocation) { 125 | case SubExprLocation.NEITHER: { 126 | return tokens.concat(_getNoSubExprTokens(expr)) 127 | } 128 | case SubExprLocation.BOTH: { 129 | return tokens.concat(_getBothSubExprTokens(expr)) 130 | } 131 | case SubExprLocation.LEFT: { 132 | return tokens.concat(_getLeftSubExprTokens(expr)) 133 | } 134 | case SubExprLocation.RIGHT: { 135 | return tokens.concat(_getRightSubExprTokens(expr)) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/engine/math_questions/expression/generator.ts: -------------------------------------------------------------------------------- 1 | import { Difficulty } from '../../models/math_question' 2 | import { randomNum } from '../../utils' 3 | import { getAdditionExprTokens } from './addition' 4 | import { 5 | BoundedExpr, 6 | Expr, 7 | ExprToken, 8 | Operator, 9 | SubExprLocation, 10 | SubExprToken, 11 | TokenType 12 | } from './models' 13 | import { getMultiplicationExprTokens } from './multiplication' 14 | import { getSubtractionExprTokens } from './subtraction' 15 | 16 | const getSubExprLocations = (opsLeft: number): SubExprLocation => { 17 | switch (opsLeft) { 18 | case 0: 19 | case 1: { 20 | return SubExprLocation.NEITHER 21 | } 22 | case 2: { 23 | return Math.random() > 0.5 ? SubExprLocation.LEFT : SubExprLocation.RIGHT 24 | } 25 | default: { 26 | const random = Math.random() 27 | if (random < 0.33) { 28 | return SubExprLocation.LEFT 29 | } else if (random < 0.66) { 30 | return SubExprLocation.RIGHT 31 | } else { 32 | return SubExprLocation.BOTH 33 | } 34 | } 35 | } 36 | } 37 | 38 | const getExprTokens = (expr: Expr | BoundedExpr): ExprToken[] => { 39 | switch (expr.op) { 40 | case Operator.Addition: { 41 | return getAdditionExprTokens(expr) 42 | } 43 | case Operator.Subtraction: { 44 | return getSubtractionExprTokens(expr) 45 | } 46 | case Operator.Multiplication: { 47 | return getMultiplicationExprTokens(expr) 48 | } 49 | } 50 | } 51 | 52 | export const getChallengeTokens = ( 53 | operators: Operator[], 54 | diff: Difficulty 55 | ): ExprToken[] => { 56 | const ops = [...operators] 57 | let opsLeft = operators.length 58 | const expr: ExprToken[] = [] 59 | let currentOp: Operator = ops[randomNum(0, ops.length - 1)] 60 | let subExprLocation: SubExprLocation = getSubExprLocations(opsLeft) 61 | opsLeft -= subExprLocation 62 | ops.splice(ops.indexOf(currentOp), 1) 63 | expr.push( 64 | ...getExprTokens({ diff, isBounded: false, op: currentOp, subExprLocation }) 65 | ) 66 | let subExprIndex = expr.findIndex((val) => val.type === TokenType.SubExpr) 67 | while (subExprIndex !== -1) { 68 | currentOp = ops[randomNum(0, ops.length - 1)] 69 | subExprLocation = getSubExprLocations(opsLeft) 70 | const subExprToken: SubExprToken = expr[subExprIndex] as SubExprToken 71 | const bound = subExprToken.value as number 72 | const subExpr: ExprToken[] = getExprTokens({ 73 | bound: bound, 74 | diff, 75 | isBounded: subExprToken.bounded, 76 | op: currentOp, 77 | subExprLocation 78 | }) 79 | expr.splice(subExprIndex, 1, ...subExpr) 80 | opsLeft -= subExprLocation 81 | ops.splice(ops.indexOf(currentOp), 1) 82 | subExprIndex = expr.findIndex((val) => val.type === TokenType.SubExpr) 83 | } 84 | return expr 85 | } 86 | -------------------------------------------------------------------------------- /src/engine/math_questions/expression/models.ts: -------------------------------------------------------------------------------- 1 | import { Difficulty } from '../../models/math_question' 2 | 3 | export enum Operator { 4 | Addition = 'Addition', 5 | Subtraction = 'Subtraction', 6 | Multiplication = 'Multiplication', 7 | } 8 | 9 | export interface Expr { 10 | op: Operator; 11 | diff: Difficulty; 12 | subExprLocation: SubExprLocation; 13 | isBounded: boolean; 14 | } 15 | 16 | export interface BoundedExpr extends Expr { 17 | bound: number; 18 | } 19 | 20 | export type MathOperations = Operator[] 21 | 22 | export type TokenGenerator = (expr: Expr) => ExprToken[] 23 | 24 | export enum TokenType { 25 | Operator = 'Operator', 26 | Number = 'Number', 27 | SubExpr = 'SubExpr', 28 | } 29 | 30 | export interface ExprToken { 31 | type: TokenType; 32 | value: number | Operator; 33 | } 34 | export interface SubExprToken extends ExprToken { 35 | bounded: boolean; 36 | } 37 | 38 | export enum SubExprLocation { 39 | LEFT = 1, 40 | RIGHT = 1, 41 | BOTH = 2, 42 | NEITHER = 0, 43 | } 44 | 45 | export interface DifficultyBounds { 46 | basic: ExprBound; 47 | normal: ExprBound; 48 | advanced: ExprBound; 49 | } 50 | 51 | export interface ExprBound { 52 | min: number; 53 | max: number; 54 | } 55 | 56 | export interface BoundedComplementTokens { 57 | boundedToken: ExprToken; 58 | complementToken: ExprToken; 59 | } 60 | -------------------------------------------------------------------------------- /src/engine/math_questions/expression/multiplication.ts: -------------------------------------------------------------------------------- 1 | import { positiveRandomNum, randomNum } from '../../utils' 2 | import { 3 | BoundedExpr, 4 | DifficultyBounds, 5 | Expr, 6 | ExprBound, 7 | ExprToken, 8 | Operator, 9 | SubExprLocation, 10 | SubExprToken, 11 | TokenType 12 | } from './models' 13 | import { isBounded } from './utils' 14 | 15 | const DifficultyBounds: DifficultyBounds = { 16 | basic: { min: 0, max: 5 }, 17 | normal: { min: 0, max: 9 }, 18 | // tslint:disable-next-line:object-literal-sort-keys 19 | advanced: { min: 0, max: 12 } 20 | } 21 | 22 | interface Multiples { 23 | left: number; 24 | right: number; 25 | } 26 | 27 | // const getSubExprToken = (expr: Expr): SubExprToken => { 28 | // const bounds: ExprBound = DifficultyBounds[expr.diff] 29 | // return { 30 | // bounded: false, 31 | // type: TokenType.SubExpr, 32 | // value: randomNum(bounds.min, bounds.max) 33 | // } 34 | // } 35 | 36 | const _getMultiplesForBound = (bound: number): Multiples[] => { 37 | const absBound = Math.abs(bound) 38 | const potentialMultiples = [] 39 | for (let i = 1; i <= absBound; i++) { 40 | potentialMultiples.push(i) 41 | } 42 | return potentialMultiples 43 | .filter((val: number) => absBound % val === 0) 44 | .map((val: number) => [val, Math.floor(absBound / val)]) 45 | .map((multiples: number[]) => { 46 | if (bound < 0) { 47 | if (Math.random() < 0.5) { 48 | return { left: multiples[0], right: multiples[1] * -1 } 49 | } else { 50 | return { left: multiples[0] * -1, right: multiples[1] } 51 | } 52 | } else { 53 | return { left: multiples[0], right: multiples[1] } 54 | } 55 | }) 56 | } 57 | 58 | const _getZeroBoundTokens = (expr: Expr): ExprToken[] => { 59 | const tokens: ExprToken[] = [] 60 | const randomValue: number = positiveRandomNum( 61 | DifficultyBounds[expr.diff].max 62 | ) 63 | switch (expr.subExprLocation) { 64 | case SubExprLocation.NEITHER: { 65 | const leftToken: ExprToken = { 66 | type: TokenType.Number, 67 | value: randomValue 68 | } 69 | const rightToken: ExprToken = { type: TokenType.Number, value: 0 } 70 | return tokens.concat([leftToken, rightToken]) 71 | } 72 | case SubExprLocation.BOTH: { 73 | const leftToken: SubExprToken = { 74 | type: TokenType.SubExpr, 75 | bounded: true, 76 | value: randomValue 77 | } 78 | const rightToken: SubExprToken = { 79 | type: TokenType.SubExpr, 80 | bounded: true, 81 | value: 0 82 | } 83 | return tokens.concat([leftToken, rightToken]) 84 | } 85 | case SubExprLocation.LEFT: { 86 | const leftToken: SubExprToken = { 87 | type: TokenType.SubExpr, 88 | bounded: true, 89 | value: randomValue 90 | } 91 | const rightToken: ExprToken = { type: TokenType.Number, value: 0 } 92 | return tokens.concat([leftToken, rightToken]) 93 | } 94 | case SubExprLocation.RIGHT: { 95 | const leftToken: ExprToken = { 96 | type: TokenType.Number, 97 | value: randomValue 98 | } 99 | const rightToken: SubExprToken = { 100 | type: TokenType.SubExpr, 101 | bounded: true, 102 | value: 0 103 | } 104 | return tokens.concat([leftToken, rightToken]) 105 | } 106 | } 107 | } 108 | 109 | const _getNoSubExprTokens = (expr: Expr): ExprToken[] => { 110 | const tokens: ExprToken[] = [] 111 | if (expr.isBounded) { 112 | const multiplesForBound: Multiples[] = _getMultiplesForBound( 113 | (expr as BoundedExpr).bound 114 | ) 115 | const multiples = 116 | multiplesForBound[randomNum(0, multiplesForBound.length - 1)] 117 | const leftToken: ExprToken = { 118 | type: TokenType.Number, 119 | value: multiples.left 120 | } 121 | const rightToken: ExprToken = { 122 | type: TokenType.Number, 123 | value: multiples.right 124 | } 125 | return tokens.concat([leftToken, rightToken]) 126 | } else { 127 | const max: number = DifficultyBounds[expr.diff].max 128 | const leftToken: ExprToken = { 129 | type: TokenType.Number, 130 | value: positiveRandomNum(max) + 1 131 | } 132 | const rightToken: ExprToken = { 133 | type: TokenType.Number, 134 | value: positiveRandomNum(max) + 1 135 | } 136 | return tokens.concat([leftToken, rightToken]) 137 | } 138 | } 139 | 140 | const _getBothSubExprTokens = (expr: Expr): ExprToken[] => { 141 | const tokens: ExprToken[] = [] 142 | if (expr.isBounded) { 143 | const multiplesForBound: Multiples[] = _getMultiplesForBound( 144 | (expr as BoundedExpr).bound 145 | ) 146 | const multiples = 147 | multiplesForBound[randomNum(0, DifficultyBounds[expr.diff].max)] 148 | const leftToken: SubExprToken = { 149 | type: TokenType.SubExpr, 150 | bounded: true, 151 | value: multiples.left 152 | } 153 | const rightToken: SubExprToken = { 154 | type: TokenType.SubExpr, 155 | bounded: true, 156 | value: multiples.right 157 | } 158 | return tokens.concat([leftToken, rightToken]) 159 | } else { 160 | const max: number = DifficultyBounds[expr.diff].max 161 | const leftToken: SubExprToken = { 162 | type: TokenType.SubExpr, 163 | bounded: true, 164 | value: positiveRandomNum(max) + 1 165 | } 166 | const rightToken: SubExprToken = { 167 | type: TokenType.SubExpr, 168 | bounded: true, 169 | value: positiveRandomNum(max) + 1 170 | } 171 | return tokens.concat([leftToken, rightToken]) 172 | } 173 | } 174 | 175 | const _getLeftSubExprTokens = (expr: Expr): ExprToken[] => { 176 | const tokens: ExprToken[] = [] 177 | if (expr.isBounded) { 178 | const multiplesForBound: Multiples[] = _getMultiplesForBound( 179 | (expr as BoundedExpr).bound 180 | ) 181 | const multiples = 182 | multiplesForBound[randomNum(0, DifficultyBounds[expr.diff].max)] 183 | const leftToken: SubExprToken = { 184 | type: TokenType.SubExpr, 185 | bounded: true, 186 | value: multiples.left 187 | } 188 | const rightToken: ExprToken = { 189 | type: TokenType.Number, 190 | value: multiples.right 191 | } 192 | return tokens.concat([leftToken, rightToken]) 193 | } else { 194 | const max: number = DifficultyBounds[expr.diff].max 195 | const leftToken: SubExprToken = { 196 | type: TokenType.SubExpr, 197 | bounded: true, 198 | value: positiveRandomNum(max) + 1 199 | } 200 | const rightToken: ExprToken = { 201 | type: TokenType.Number, 202 | value: positiveRandomNum(max) + 1 203 | } 204 | return tokens.concat([leftToken, rightToken]) 205 | } 206 | } 207 | 208 | const _getRightSubExprTokens = (expr: Expr): ExprToken[] => { 209 | const tokens: ExprToken[] = [] 210 | if (expr.isBounded) { 211 | const multiplesForBound: Multiples[] = _getMultiplesForBound( 212 | (expr as BoundedExpr).bound 213 | ) 214 | const multiples = 215 | multiplesForBound[randomNum(0, DifficultyBounds[expr.diff].max)] 216 | const leftToken: ExprToken = { 217 | type: TokenType.Number, 218 | value: multiples.left 219 | } 220 | const rightToken: SubExprToken = { 221 | type: TokenType.SubExpr, 222 | bounded: true, 223 | value: multiples.right 224 | } 225 | return tokens.concat([leftToken, rightToken]) 226 | } else { 227 | const max: number = DifficultyBounds[expr.diff].max 228 | const leftToken: ExprToken = { 229 | type: TokenType.Number, 230 | value: positiveRandomNum(max) + 1 231 | } 232 | const rightToken: SubExprToken = { 233 | type: TokenType.SubExpr, 234 | bounded: true, 235 | value: positiveRandomNum(max) + 1 236 | } 237 | return tokens.concat([leftToken, rightToken]) 238 | } 239 | } 240 | 241 | export const getMultiplicationExprTokens = (expr: Expr): ExprToken[] => { 242 | const tokens: ExprToken[] = [] 243 | tokens.push({ 244 | type: TokenType.Operator, 245 | value: Operator.Multiplication 246 | }) 247 | if (isBounded(expr) && expr.bound === 0) { 248 | return tokens.concat(_getZeroBoundTokens(expr)) 249 | } 250 | switch (expr.subExprLocation) { 251 | case SubExprLocation.NEITHER: { 252 | return tokens.concat(_getNoSubExprTokens(expr)) 253 | } 254 | case SubExprLocation.BOTH: { 255 | return tokens.concat(_getBothSubExprTokens(expr)) 256 | } 257 | case SubExprLocation.LEFT: { 258 | return tokens.concat(_getLeftSubExprTokens(expr)) 259 | } 260 | case SubExprLocation.RIGHT: { 261 | return tokens.concat(_getRightSubExprTokens(expr)) 262 | } 263 | } 264 | return tokens 265 | } 266 | -------------------------------------------------------------------------------- /src/engine/math_questions/expression/subtraction.ts: -------------------------------------------------------------------------------- 1 | import { positiveRandomNum, randomNum } from '../../utils' 2 | import { 3 | BoundedExpr, 4 | DifficultyBounds, 5 | Expr, 6 | ExprBound, 7 | ExprToken, 8 | Operator, 9 | SubExprLocation, 10 | SubExprToken, 11 | TokenType 12 | } from './models' 13 | import { isBounded } from './utils' 14 | 15 | const DifficultyBounds: DifficultyBounds = { 16 | basic: { min: 1, max: 10 }, 17 | normal: { min: 5, max: 20 }, 18 | // tslint:disable-next-line:object-literal-sort-keys 19 | advanced: { min: 20, max: 100 } 20 | } 21 | 22 | const getSubExprToken = (expr: Expr): SubExprToken => { 23 | const bounds: ExprBound = DifficultyBounds[expr.diff] 24 | return { 25 | bounded: false, 26 | type: TokenType.SubExpr, 27 | value: randomNum(bounds.min, bounds.max) 28 | } 29 | } 30 | 31 | const _getZeroBoundTokens = (expr: Expr | BoundedExpr): ExprToken[] => { 32 | const tokens: ExprToken[] = [] 33 | const randomValue = randomNum(DifficultyBounds[expr.diff].min, DifficultyBounds[expr.diff].max) 34 | const randomToken: ExprToken = { type: TokenType.Number, value: randomValue } 35 | const boundedRandomToken: SubExprToken = { type: TokenType.SubExpr, value: randomValue, bounded: true } 36 | switch (expr.subExprLocation) { 37 | case SubExprLocation.NEITHER: { 38 | return tokens.concat([randomToken, randomToken]) 39 | } 40 | case SubExprLocation.BOTH: { 41 | return tokens.concat([boundedRandomToken, boundedRandomToken]) 42 | } 43 | case SubExprLocation.LEFT: { 44 | return tokens.concat([boundedRandomToken, randomToken]) 45 | } 46 | case SubExprLocation.RIGHT: { 47 | return tokens.concat([randomToken, boundedRandomToken]) 48 | } 49 | } 50 | } 51 | 52 | const _getNoSubExprTokens = (expr: Expr | BoundedExpr): ExprToken[] => { 53 | const tokens: ExprToken[] = [] 54 | if (expr.isBounded) { 55 | const boundedExpr = expr as BoundedExpr 56 | const bound: number = Math.abs(boundedExpr.bound) 57 | if (boundedExpr.bound < 0) { 58 | const boundedRandom: number = positiveRandomNum(bound) 59 | const leftToken: ExprToken = { type: TokenType.Number, value: boundedRandom } 60 | const rightToken: ExprToken = { type: TokenType.Number, value: boundedRandom + bound } 61 | return tokens.concat([leftToken, rightToken]) 62 | } else { 63 | const boundedRandom: number = positiveRandomNum(bound) 64 | const leftToken: ExprToken = { type: TokenType.Number, value: boundedRandom + bound } 65 | const rightToken: ExprToken = { type: TokenType.Number, value: boundedRandom } 66 | return tokens.concat([leftToken, rightToken]) 67 | } 68 | } else { 69 | const leftToken: ExprToken = { type: TokenType.Number, value: positiveRandomNum(DifficultyBounds[expr.diff].max) } 70 | const rightToken: ExprToken = { type: TokenType.Number, value: positiveRandomNum(DifficultyBounds[expr.diff].max) } 71 | return tokens.concat([leftToken, rightToken]) 72 | } 73 | } 74 | 75 | const _getBothSubExprTokens = (expr: Expr | BoundedExpr): ExprToken[] => { 76 | const tokens: ExprToken[] = [] 77 | if (expr.isBounded) { 78 | const boundedExpr = expr as BoundedExpr 79 | const bound: number = Math.abs(boundedExpr.bound) 80 | const boundedRandom: number = positiveRandomNum(bound) 81 | const complement: number = boundedRandom + bound 82 | const boundedRandomToken: SubExprToken = { type: TokenType.SubExpr, bounded: true, value: boundedRandom } 83 | const complementToken: SubExprToken = { type: TokenType.SubExpr, bounded: true, value: complement } 84 | if (boundedExpr.bound < 0) { 85 | return tokens.concat([complementToken, boundedRandomToken]) 86 | } else { 87 | return tokens.concat([boundedRandomToken, complementToken]) 88 | } 89 | } else { 90 | return tokens.concat([getSubExprToken(expr), getSubExprToken(expr)]) 91 | } 92 | } 93 | 94 | const _getLeftSubExprTokens = (expr: Expr | BoundedExpr): ExprToken[] => { 95 | const tokens: ExprToken[] = [] 96 | if (expr.isBounded) { 97 | const boundedExpr = expr as BoundedExpr 98 | const bound: number = Math.abs(boundedExpr.bound) 99 | const boundedRandom: number = positiveRandomNum(bound) 100 | const complement: number = boundedRandom + bound 101 | if (bound < 0) { 102 | const leftToken: SubExprToken = { type: TokenType.SubExpr, bounded: true, value: complement } 103 | const rightToken: ExprToken = { type: TokenType.Number, value: boundedRandom } 104 | return tokens.concat([leftToken, rightToken]) 105 | } else { 106 | const leftToken: SubExprToken = { type: TokenType.SubExpr, bounded: true, value: boundedRandom } 107 | const rightToken: ExprToken = { type: TokenType.Number, value: complement } 108 | return tokens.concat([leftToken, rightToken]) 109 | } 110 | } else { 111 | const leftToken: SubExprToken = getSubExprToken(expr) 112 | const rightToken: ExprToken = { type: TokenType.Number, value: positiveRandomNum(DifficultyBounds[expr.diff].max) } 113 | return tokens.concat([leftToken, rightToken]) 114 | } 115 | } 116 | 117 | const _getRightSubExprTokens = (expr: Expr | BoundedExpr): ExprToken[] => { 118 | const tokens: ExprToken[] = [] 119 | if (expr.isBounded) { 120 | const boundedExpr = expr as BoundedExpr 121 | const bound: number = Math.abs(boundedExpr.bound) 122 | const boundedRandom: number = positiveRandomNum(bound) 123 | const complement: number = boundedRandom + bound 124 | if (bound < 0) { 125 | const leftToken: ExprToken = { type: TokenType.Number, value: boundedRandom } 126 | const rightToken: SubExprToken = { type: TokenType.SubExpr, bounded: true, value: complement } 127 | return tokens.concat([leftToken, rightToken]) 128 | } else { 129 | const leftToken: ExprToken = { type: TokenType.Number, value: complement } 130 | const rightToken: SubExprToken = { type: TokenType.SubExpr, bounded: true, value: boundedRandom } 131 | return tokens.concat([leftToken, rightToken]) 132 | } 133 | } else { 134 | const leftToken: ExprToken = { type: TokenType.Number, value: positiveRandomNum(DifficultyBounds[expr.diff].max) } 135 | const rightToken: SubExprToken = getSubExprToken(expr) 136 | return tokens.concat([leftToken, rightToken]) 137 | } 138 | } 139 | 140 | export const getSubtractionExprTokens = (expr: Expr): ExprToken[] => { 141 | const tokens: ExprToken[] = [] 142 | tokens.push({ 143 | type: TokenType.Operator, 144 | value: Operator.Subtraction 145 | }) 146 | if (isBounded(expr) && expr.bound === 0) { 147 | return tokens.concat(_getZeroBoundTokens(expr)) 148 | } 149 | switch (expr.subExprLocation) { 150 | case SubExprLocation.NEITHER: { 151 | return tokens.concat(_getNoSubExprTokens(expr)) 152 | } 153 | case SubExprLocation.BOTH: { 154 | return tokens.concat(_getBothSubExprTokens(expr)) 155 | } 156 | case SubExprLocation.LEFT: { 157 | return tokens.concat(_getLeftSubExprTokens(expr)) 158 | } 159 | case SubExprLocation.RIGHT: { 160 | return tokens.concat(_getRightSubExprTokens(expr)) 161 | } 162 | } 163 | return tokens 164 | } 165 | -------------------------------------------------------------------------------- /src/engine/math_questions/expression/utils.ts: -------------------------------------------------------------------------------- 1 | import { BoundedExpr, Expr, SubExprToken, TokenType } from './models' 2 | 3 | export const isBounded = (expr: any): expr is BoundedExpr => { 4 | return 'bound' in expr 5 | } 6 | -------------------------------------------------------------------------------- /src/engine/math_questions/factorization.ts: -------------------------------------------------------------------------------- 1 | // import { Difficulty, GenerateChallenge, MathQuestionType } from '../models/math_question'; 2 | // import { positiveRandomNum } from '../utils'; 3 | 4 | // interface FactorizationModel { 5 | // xCoeff1: number; 6 | // xCoeff2: number; 7 | // const1: number; 8 | // const2: number; 9 | // } 10 | 11 | // interface FactorizationRandomBounds { 12 | // xCoeffMax: number; 13 | // constMax: number; 14 | // } 15 | 16 | // export const generateFactorizationChallenge: GenerateChallenge = (difficulty: Difficulty) => { 17 | // const randomBounds = getRandomBounds(difficulty); 18 | // const model: FactorizationModel = { 19 | // const1: positiveRandomNum(randomBounds.xCoeffMax), 20 | // const2: positiveRandomNum(randomBounds.xCoeffMax), 21 | // xCoeff1: positiveRandomNum(randomBounds.xCoeffMax), 22 | // xCoeff2: positiveRandomNum(randomBounds.xCoeffMax), 23 | // }; 24 | // return { 25 | // difficulty, 26 | // infix: factorizationInfix(model), 27 | // latex: factorizationLatex(model), 28 | // type: MathQuestionType.Factorization, 29 | // }; 30 | // }; 31 | 32 | // const getRandomBounds = (difficulty: Difficulty): FactorizationRandomBounds => { 33 | // switch (difficulty) { 34 | // case Difficulty.Basic: { 35 | // return { xCoeffMax: 1, constMax: 5 }; 36 | // } 37 | // case Difficulty.Normal: { 38 | // return { xCoeffMax: 2, constMax: 10 }; 39 | // } 40 | // case Difficulty.Advanced: { 41 | // return { xCoeffMax: 3, constMax: 10 }; 42 | // } 43 | // } 44 | // }; 45 | 46 | // const factorizationInfix = (model: FactorizationModel): string => { 47 | // return `(${model.xCoeff1}x + ${model.const1})*(${model.xCoeff2}x + ${model.const2})`; 48 | // }; 49 | 50 | // const factorizationLatex = (model: FactorizationModel): string => { 51 | // const a = model.xCoeff1 * model.xCoeff2; 52 | // const b = model.xCoeff1 * model.const2 + model.xCoeff2 * model.const1; 53 | // const c = model.const1 * model.const2; 54 | // let factorLatex = '$$'; 55 | // factorLatex = a === 1 ? factorLatex.concat('x^{2}') : factorLatex.concat(`${a}x^{2}`); 56 | // if (b !== 0) { 57 | // factorLatex = b > 0 ? factorLatex.concat(` + ${b}x`) : factorLatex.concat(` ${b}x`); 58 | // } 59 | // if (c !== 0) { 60 | // factorLatex = c > 0 ? factorLatex.concat(` + ${c}`) : factorLatex.concat(` c`); 61 | // } 62 | // return factorLatex.concat('$$'); 63 | // }; 64 | -------------------------------------------------------------------------------- /src/engine/models/math_question.ts: -------------------------------------------------------------------------------- 1 | import { Operator } from '../math_questions/expression/models' 2 | 3 | export enum ChallengeType { 4 | Expression, 5 | Factorization, 6 | Binary, 7 | Hexadecimal, 8 | } 9 | 10 | export enum Difficulty { 11 | Basic = 'basic', 12 | Normal = 'normal', 13 | Advanced = 'advanced', 14 | } 15 | 16 | export enum PracticeMode { 17 | QUESTIONS = 'questions', 18 | TIME = 'time' 19 | } 20 | 21 | export interface ChallengeModel { 22 | type: ChallengeType; 23 | difficulty: Difficulty; 24 | infix: string; 25 | latex: string; 26 | } 27 | 28 | export interface ChallengeParams { 29 | difficulty: Difficulty; 30 | operators: Operator[]; 31 | } 32 | 33 | export type GenerateChallenge = (params: ChallengeParams) => ChallengeModel; 34 | -------------------------------------------------------------------------------- /src/engine/utils.ts: -------------------------------------------------------------------------------- 1 | export function randomNum (min: number, max: number): number { 2 | return Math.floor(Math.random() * (max - min + 1)) + min 3 | } 4 | 5 | export function positiveRandomNum (max: number): number { 6 | return randomNum(1, max) 7 | } 8 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | NODE_ENV: string; 4 | VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined; 5 | VUE_ROUTER_BASE: string | undefined; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/i18n/en-us/index.ts: -------------------------------------------------------------------------------- 1 | // This is just an example, 2 | // so you can safely delete all default props below 3 | 4 | export default { 5 | failed: 'Action failed', 6 | success: 'Action was successful' 7 | }; 8 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import enUS from './en-us'; 2 | 3 | export default { 4 | 'en-us': enUS 5 | }; 6 | -------------------------------------------------------------------------------- /src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= htmlWebpackPlugin.options.productName %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /src/layouts/MainLayout.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /src/pages/CustomizePracticePage.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 96 | 97 | -------------------------------------------------------------------------------- /src/pages/Error404.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 26 | -------------------------------------------------------------------------------- /src/pages/HomePage.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 37 | 38 | 78 | -------------------------------------------------------------------------------- /src/pages/PracticePage.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 54 | 55 | 100 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { route } from 'quasar/wrappers' 2 | import VueRouter from 'vue-router' 3 | import { RootState } from '../store' 4 | import routes from './routes' 5 | 6 | /* 7 | * If not building with SSR mode, you can 8 | * directly export the Router instantiation 9 | */ 10 | 11 | export default route(function ({ Vue }) { 12 | Vue.use(VueRouter) 13 | 14 | const Router = new VueRouter({ 15 | scrollBehavior: () => ({ x: 0, y: 0 }), 16 | routes, 17 | 18 | // Leave these as is and change from quasar.conf.js instead! 19 | // quasar.conf.js -> build -> vueRouterMode 20 | // quasar.conf.js -> build -> publicPath 21 | mode: process.env.VUE_ROUTER_MODE, 22 | base: process.env.VUE_ROUTER_BASE 23 | }) 24 | 25 | return Router 26 | }) 27 | -------------------------------------------------------------------------------- /src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteConfig } from 'vue-router'; 2 | 3 | const routes: RouteConfig[] = [ 4 | { 5 | path: '/', 6 | component: () => import('layouts/MainLayout.vue'), 7 | children: [ 8 | { path: '', component: () => import('pages/HomePage.vue') }, 9 | { path: 'practice', component: () => import('pages/PracticePage.vue') }, 10 | { path: 'customize', component: () => import('pages/CustomizePracticePage.vue') }, 11 | 12 | ] 13 | } 14 | ]; 15 | 16 | // Always leave this as last one 17 | if (process.env.MODE !== 'ssr') { 18 | routes.push({ 19 | path: '*', 20 | component: () => import('pages/Error404.vue') 21 | }); 22 | } 23 | 24 | export default routes; 25 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | // Mocks all files ending in `.vue` showing them as plain Vue instances 2 | declare module '*.vue' { 3 | import Vue from 'vue'; 4 | export default Vue; 5 | } 6 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { store } from 'quasar/wrappers' 2 | import Vuex from 'vuex' 3 | import { PracticeModule } from './practice/practice' 4 | 5 | /* 6 | * If not building with SSR mode, you can 7 | * directly export the Store instantiation 8 | */ 9 | 10 | export interface RootState { 11 | // Define your own store structure, using submodules if needed 12 | // example: typeof exampleState; 13 | example: unknown; 14 | } 15 | 16 | export default store(function ({ Vue }) { 17 | Vue.use(Vuex) 18 | 19 | const Store = new Vuex.Store({ 20 | modules: { 21 | PracticeModule 22 | }, 23 | 24 | // enable strict mode (adds overhead!) 25 | // for dev mode only 26 | strict: !!process.env.DEV 27 | }) 28 | 29 | return Store 30 | }) 31 | -------------------------------------------------------------------------------- /src/store/module-example/actions.ts: -------------------------------------------------------------------------------- 1 | export function someAction (/* context */) { 2 | // your code 3 | } 4 | -------------------------------------------------------------------------------- /src/store/module-example/getters.ts: -------------------------------------------------------------------------------- 1 | export function someGetter (/* state */) { 2 | // your code 3 | } 4 | -------------------------------------------------------------------------------- /src/store/module-example/index.ts: -------------------------------------------------------------------------------- 1 | import * as actions from './actions' 2 | import * as getters from './getters' 3 | import * as mutations from './mutations' 4 | import state from './state' 5 | 6 | export default { 7 | namespaced: true, 8 | getters, 9 | mutations, 10 | actions, 11 | state 12 | } 13 | -------------------------------------------------------------------------------- /src/store/module-example/mutations.ts: -------------------------------------------------------------------------------- 1 | export function someMutation (/* state */) { 2 | // your code 3 | } 4 | -------------------------------------------------------------------------------- /src/store/module-example/state.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | // your code 3 | } 4 | -------------------------------------------------------------------------------- /src/store/practice/practice.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree, GetterTree, Module, MutationTree } from 'vuex' 2 | import { RootState } from '../index' 3 | import { ChallengeModel, ChallengeType, Difficulty, PracticeMode } from '../../engine/models/math_question' 4 | import { generateExpressionChallenge } from '../../engine/math_questions/expression' 5 | import { Operator } from '../../engine/math_questions/expression/models' 6 | import { evaluate } from 'mathjs' 7 | 8 | export interface PracticeOptions { 9 | difficulty: Difficulty; 10 | operators: Operator[]; 11 | challengeTypes: ChallengeType[]; 12 | } 13 | 14 | export enum PracticeGetters { 15 | QUESTION_LATEX = 'questionLatex', 16 | ANSWER = 'answer', 17 | STREAK = 'streak', 18 | OPERATORS = 'operators', 19 | DIFFICULTY = 'difficulty', 20 | PRACTICE_MODE = 'practiceMode', 21 | PRACTICE_QUESTION_COUNT = 'practiceQuestionCount', 22 | PRACTICE_TIME = 'practiceTime', 23 | PRACTICE_TIME_LEFT = 'practiceTimeLeft', 24 | PRACTICE_CORRECT_QUESTION_COUNT = 'practiceCorrectQuestionCount', 25 | PRACTICE_SESSION_ACTIVE = 'practiceSessionActive', 26 | SHOWING_FEEDBACK = 'showingFeedback', 27 | PRACTICE_LAST_QUESTION_CORRECT = 'practiceLastQuestionCorrect' 28 | } 29 | 30 | export enum PracticeActions { 31 | INIT = 'init', 32 | NEW_QUESTION = 'newQuestion', 33 | SET_ANSWER = 'setAnswer', 34 | CHECK_ANSWER = 'checkAnswer', 35 | ON_CORRECT = 'onCorrect', 36 | ON_INCORRECT = 'onIncorrect', 37 | SET_PRACTICE_MODE = 'setPracticeMode', 38 | SET_PRACTICE_QUESTION_COUNT = 'setPracticeQuestionCount', 39 | SET_PRACTICE_TIME = 'setPracticeTime', 40 | SET_PRACTICE_TIMER_ID = 'setPracticeTimeId', 41 | FINISH_PRACTICE_SESSION = 'finishPracticeSession', 42 | PRACTICE_TIME_TICK = 'practiceTimeTick', 43 | SKIP_QUESTION = 'skipQuestion', 44 | SET_DIFFICULTY = 'setDifficulty', 45 | SELECT_ALL_CONCEPTS = 'selectAllConcepts', 46 | RESET_CONCPETS = 'resetConcepts' 47 | } 48 | 49 | enum PracticeMutations { 50 | SET_PRACTICE_OPTIONS = 'setPracticeOptions', 51 | SET_QUESTION = 'setQuestion', 52 | SET_ANSWER = 'setAnswer', 53 | SET_STREAK = 'setStreak', 54 | SET_SHOWING_FEEDBACK = 'setShowingFeedback', 55 | SET_PRACTICE_MODE = 'setPracticeMode', 56 | SET_PRACTICE_QUESTION_COUNT = 'setPracticeQuestionCount', 57 | SET_PRACTICE_TIME = 'setPracticeTime', 58 | SET_PRACTICE_TIME_LEFT = 'setPracticeTimeLeft', 59 | SET_PRACTICE_TIMER_ID = 'setPracticeTimerId', 60 | SET_PRACTICE_CORRECT_QUESTION_COUNT = 'setPracticeCorrectQuestionCount', 61 | RESET_PRACTICE_SESSION = 'resetPracticeSession', 62 | SET_PRACTICE_SESSION_ACTIVE = 'setPracticeSessionActive', 63 | SET_DIFFICULTY = 'setDifficulty', 64 | SET_OPERATOR_ENABLED = 'setOperatorEnabled', 65 | SET_OPERATOR_DISABLED = 'setOperatorDisabled', 66 | SET_PRACTICE_LAST_QUESTION_CORRECT = 'setPracticeLastQuestionCorrect' 67 | } 68 | 69 | export interface PracticeState { 70 | question: ChallengeModel; 71 | difficulty: Difficulty; 72 | operators: Operator[]; 73 | challengeTypes: ChallengeType[]; 74 | answer: string; 75 | streak: number; 76 | 77 | // We show feedback when the user enters a correct or incorrect answer 78 | showingFeedback: boolean; 79 | 80 | // We show feedback when the user enters a correct or incorrect answer 81 | practiceMode: PracticeMode; 82 | practiceQuestionCount: number; 83 | 84 | // Practice session's time in seconds 85 | practiceTime: number; 86 | practiceTimeLeft: number; 87 | 88 | // Keeps track of number of correct questions 89 | practiceCorrectQuestionCount: number; 90 | 91 | /* 92 | The ID of the practice session timer. We'll use this value 93 | with the clearInterval() method to cancel the timer 94 | */ 95 | practiceTimerId: number; 96 | 97 | practiceSessionActive: boolean; 98 | 99 | practiceLastQuestionCorrect: boolean; 100 | } 101 | 102 | const getters: GetterTree = { 103 | questionLatex: (state) => state.question.latex, 104 | answer: (state) => state.answer, 105 | streak: (state) => state.streak, 106 | operators: (state) => state.operators, 107 | difficulty: (state) => state.difficulty, 108 | practiceMode: (state) => state.practiceMode, 109 | practiceQuestionCount: (state) => state.practiceQuestionCount, 110 | practiceTime: (state) => state.practiceTime, 111 | practiceTimeLeft: (state) => state.practiceTimeLeft, 112 | practiceCorrectQuestionCount: (state) => state.practiceCorrectQuestionCount, 113 | showingFeedback: (state) => state.showingFeedback, 114 | practiceSessionActive: (state) => state.practiceSessionActive, 115 | practiceLastQuestionCorrect: (state) => state.practiceLastQuestionCorrect 116 | } 117 | 118 | const mutations: MutationTree = { 119 | setQuestion(state: PracticeState, question: ChallengeModel) { 120 | state.question = Object.assign({}, question) 121 | }, 122 | setAnswer(state: PracticeState, answer: string) { 123 | state.answer = answer 124 | }, 125 | setStreak(state: PracticeState, streak: number) { 126 | state.streak = streak 127 | }, 128 | setPracticeOptions(state: PracticeState, options: PracticeOptions) { 129 | state.operators = options.operators 130 | state.challengeTypes = options.challengeTypes 131 | state.difficulty = options.difficulty 132 | state.practiceTimeLeft = state.practiceTime 133 | }, 134 | setShowingFeedback(state: PracticeState, isShowingFeedback: boolean) { 135 | state.showingFeedback = isShowingFeedback; 136 | }, 137 | setOperatorEnabled(state: PracticeState, operator: Operator) { 138 | state.operators.push(operator) 139 | }, 140 | setOperatorDisabled(state: PracticeState, operator: Operator) { 141 | state.operators = state.operators.filter(op => op !== operator) 142 | }, 143 | setPracticeMode(state: PracticeState, mode: PracticeMode) { 144 | state.practiceMode = mode; 145 | }, 146 | setPracticeQuestionCount(state: PracticeState, questionCount: number) { 147 | state.practiceQuestionCount = questionCount; 148 | }, 149 | setPracticeTime(state: PracticeState, time: number) { 150 | state.practiceTime = time; 151 | }, 152 | setPracticeTimeLeft(state: PracticeState, time: number) { 153 | state.practiceTimeLeft = time; 154 | }, 155 | setPracticeCorrectQuestionCount(state: PracticeState, count: number) { 156 | state.practiceCorrectQuestionCount += 1; 157 | }, 158 | setPracticeTimerId(state: PracticeState, id: number) { 159 | state.practiceTimerId = id; 160 | }, 161 | resetPracticeSession(state: PracticeState) { 162 | state.streak = 0; 163 | state.practiceCorrectQuestionCount = 0; 164 | state.practiceTimerId = 0; 165 | state.practiceTimeLeft = 0; 166 | state.practiceSessionActive = false; 167 | }, 168 | setPracticeSessionActive(state: PracticeState, value: boolean) { 169 | state.practiceSessionActive = value; 170 | }, 171 | setDifficulty(state: PracticeState, difficulty: Difficulty) { 172 | state.difficulty = difficulty; 173 | }, 174 | setPracticeLastQuestionCorrect(state: PracticeState, practiceLastQuestionCorrect: boolean) { 175 | state.practiceLastQuestionCorrect = practiceLastQuestionCorrect; 176 | } 177 | } 178 | 179 | const newQuestion = (difficulty: Difficulty, operators: Operator[]) => { 180 | return generateExpressionChallenge({ difficulty, operators }) 181 | } 182 | 183 | const actions: ActionTree = { 184 | init(context, options: PracticeActions) { 185 | context.commit(PracticeMutations.SET_PRACTICE_SESSION_ACTIVE, true) 186 | context.commit(PracticeMutations.SET_PRACTICE_OPTIONS, options) 187 | if (context.state.practiceMode === PracticeMode.TIME) { 188 | const practiceTimerId = setInterval(() => context.dispatch(PracticeActions.PRACTICE_TIME_TICK), 1000) 189 | context.commit(PracticeMutations.SET_PRACTICE_TIMER_ID, practiceTimerId) 190 | } 191 | }, 192 | practiceTimeTick(context) { 193 | const newTimeLeft = context.state.practiceTimeLeft - 1 194 | if (newTimeLeft == 0) { 195 | context.dispatch(PracticeActions.FINISH_PRACTICE_SESSION) 196 | } else { 197 | context.commit(PracticeMutations.SET_PRACTICE_TIME_LEFT, newTimeLeft) 198 | } 199 | }, 200 | newQuestion(context) { 201 | context.commit( 202 | PracticeMutations.SET_QUESTION, 203 | newQuestion(context.state.difficulty, context.state.operators) 204 | ) 205 | }, 206 | setAnswer(context, answer: string) { 207 | context.commit(PracticeMutations.SET_ANSWER, answer) 208 | }, 209 | checkAnswer(context) { 210 | console.log(context.state.answer) 211 | console.log(context.state.question.infix) 212 | if (evaluate(`${context.state.answer} == ${context.state.question.infix}`)) { 213 | context.dispatch(PracticeActions.ON_CORRECT) 214 | } else { 215 | context.dispatch(PracticeActions.ON_INCORRECT) 216 | } 217 | }, 218 | 219 | /* 220 | On a correct or incorrect answer, we clear the answer, increment/reset the streak, and set the state of the 221 | practice session to be in 'Showing Feedback' mode which includes animations or encouragement prompts 222 | */ 223 | onCorrect(context) { 224 | context.commit(PracticeMutations.SET_PRACTICE_CORRECT_QUESTION_COUNT, context.state.practiceCorrectQuestionCount + 1) 225 | context.commit(PracticeMutations.SET_STREAK, context.state.streak + 1) 226 | context.commit(PracticeMutations.SET_ANSWER, '') 227 | context.commit(PracticeMutations.SET_PRACTICE_LAST_QUESTION_CORRECT, true) 228 | context.commit(PracticeMutations.SET_SHOWING_FEEDBACK, true) 229 | context.dispatch(PracticeActions.NEW_QUESTION) 230 | if(context.state.practiceCorrectQuestionCount == context.state.practiceQuestionCount && context.state.practiceMode == PracticeMode.QUESTIONS){ 231 | context.commit(PracticeMutations.RESET_PRACTICE_SESSION) 232 | } 233 | setTimeout(() => context.commit(PracticeMutations.SET_SHOWING_FEEDBACK, false), 600) 234 | }, 235 | onIncorrect(context) { 236 | context.commit(PracticeMutations.SET_STREAK, 0) 237 | context.commit(PracticeMutations.SET_ANSWER, '') 238 | context.commit(PracticeMutations.SET_PRACTICE_LAST_QUESTION_CORRECT, false) 239 | context.commit(PracticeMutations.SET_SHOWING_FEEDBACK, true) 240 | setTimeout(() => context.commit(PracticeMutations.SET_SHOWING_FEEDBACK, false), 600) 241 | }, 242 | setPracticeMode(context, mode: PracticeMode) { 243 | context.commit(PracticeMutations.SET_PRACTICE_MODE, mode) 244 | }, 245 | setPracticeQuestionCount(context, questionCount: number) { 246 | context.commit(PracticeMutations.SET_PRACTICE_QUESTION_COUNT, questionCount) 247 | }, 248 | setPracticeTime(context, time: number) { 249 | context.commit(PracticeMutations.SET_PRACTICE_TIME, time) 250 | }, 251 | finishPracticeSession(context) { 252 | console.log(context.state.practiceTimerId) 253 | clearInterval(context.state.practiceTimerId) 254 | context.commit(PracticeMutations.RESET_PRACTICE_SESSION) 255 | }, 256 | skipQuestion(context) { 257 | context.dispatch(PracticeActions.NEW_QUESTION) 258 | }, 259 | setDifficulty(context, difficulty) { 260 | context.commit(PracticeMutations.SET_DIFFICULTY, difficulty) 261 | }, 262 | selectAllConcepts(context) { 263 | for (let operator of Object.values(Operator)) { 264 | if (!context.state.operators.includes(operator)) 265 | context.commit(PracticeMutations.SET_OPERATOR_ENABLED, operator) 266 | } 267 | }, 268 | resetConcepts(context) { 269 | for (let operator of Object.values(Operator)) { 270 | context.commit(PracticeMutations.SET_OPERATOR_DISABLED, operator) 271 | } 272 | context.commit(PracticeMutations.SET_OPERATOR_ENABLED, Operator.Addition) 273 | context.commit(PracticeMutations.SET_OPERATOR_ENABLED, Operator.Subtraction) 274 | }, 275 | } 276 | 277 | export const PracticeModule: Module = { 278 | state: { 279 | question: {} as ChallengeModel, 280 | difficulty: Difficulty.Normal, 281 | operators: [Operator.Addition, Operator.Subtraction], 282 | challengeTypes: [], 283 | answer: '', 284 | streak: 0, 285 | showingFeedback: false, 286 | practiceMode: PracticeMode.TIME, 287 | practiceQuestionCount: 10, 288 | practiceTime: 60, 289 | practiceTimeLeft: 0, 290 | practiceCorrectQuestionCount: 0, 291 | practiceTimerId: 0, 292 | practiceSessionActive: false, 293 | practiceLastQuestionCorrect: false 294 | }, 295 | getters, 296 | actions, 297 | mutations 298 | } 299 | -------------------------------------------------------------------------------- /src/store/store-flag.d.ts: -------------------------------------------------------------------------------- 1 | // THIS FEATURE-FLAG FILE IS AUTOGENERATED, 2 | // REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING 3 | import 'quasar/dist/types/feature-flag' 4 | 5 | declare module 'quasar/dist/types/feature-flag' { 6 | interface QuasarFeatureFlags { 7 | store: true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/types/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import Vue, { VueConstructor } from 'vue'; 3 | const content: VueConstructor; 4 | export default content; 5 | } 6 | -------------------------------------------------------------------------------- /src/vue-katex.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-katex'; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@quasar/app/tsconfig-preset", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "typeRoots": ["node_modules", "src/types"], 6 | } 7 | } 8 | --------------------------------------------------------------------------------