├── .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 |
7 |
8 | # Material Math
9 |
10 |
11 | [](https://app.netlify.com/sites/material-math/deploys)
12 |
13 | [](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 | [](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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 | {{label}}
4 |
5 |
6 |
7 |
15 |
16 |
27 |
--------------------------------------------------------------------------------
/src/components/ChallengeHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
31 |
32 |
--------------------------------------------------------------------------------
/src/components/ChallengeStreak.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
19 |
20 |
27 |
--------------------------------------------------------------------------------
/src/components/ClassicChallenge.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
69 |
70 |
96 |
--------------------------------------------------------------------------------
/src/components/ClassicInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
93 |
94 |
95 |
134 |
135 |
--------------------------------------------------------------------------------
/src/components/ClassicQuestion.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
20 |
21 |
35 |
--------------------------------------------------------------------------------
/src/components/ConceptPicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
51 |
52 |
--------------------------------------------------------------------------------
/src/components/ConceptPickerButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
{{ operator }}
13 |
14 |
15 |
16 |
17 |
66 |
67 |
--------------------------------------------------------------------------------
/src/components/CustomizePracticeCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
18 | Customize Session
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Timed Practice
30 |
31 |
32 |
33 | Math Worksheet
34 |
35 |
36 |
37 |
38 | {{timeInMins}}
39 | Minutes
40 |
41 |
42 | {{practiceQuestionCount}}
43 | Questions
44 |
45 |
46 |
47 |
48 |
49 | Basic
50 |
51 |
52 |
53 | Normal
54 |
55 |
56 |
57 | Advanced
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
110 |
111 |
165 |
--------------------------------------------------------------------------------
/src/components/DifficultyPicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
Pick your difficulty
7 |
8 |
12 |
20 |
26 |
32 |
36 |
41 |
42 |
43 |
Basic
47 |
48 |
52 |
60 |
66 |
72 |
78 |
84 |
90 |
91 |
Normal
95 |
96 |
100 |
108 |
114 |
120 |
121 |
127 |
133 |
139 |
145 |
146 |
147 |
Advanced
151 |
152 |
153 |
154 |
155 |
156 |
182 |
183 |
--------------------------------------------------------------------------------
/src/components/DonationDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Thank you for donating! 💕
7 | This app was developed by Grey Software, a not-for-profit organization that empowers students to build open-source software for their communities and societies.
8 | You can support us as we envision and build the software ecosystem of the future by sponsoring us below.
9 |
10 |
11 |
12 |
13 |
23 |
33 |
43 |
44 |
45 |
46 |
47 |
53 |
54 |
55 |
56 |
76 |
77 |
87 |
--------------------------------------------------------------------------------
/src/components/MaterialMathIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
30 |
--------------------------------------------------------------------------------
/src/components/ModePicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
How would you like to practice?
7 |
11 |
15 |
23 |
Timed Practice
27 |
28 |
32 |
40 |
Maths Worksheet
44 |
45 |
46 |
47 |
48 |
49 |
83 |
84 |
--------------------------------------------------------------------------------
/src/components/PracticeSessionProgress.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
13 |
14 |
15 |
16 |
40 |
41 |
--------------------------------------------------------------------------------
/src/components/ValuePicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
How long would you like to practice for?
4 |
12 |
20 |
21 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
19 |
--------------------------------------------------------------------------------
/src/pages/CustomizePracticePage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
18 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
42 |
49 |
50 |
59 |
63 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
96 |
97 |
--------------------------------------------------------------------------------
/src/pages/Error404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 | Sorry, nothing here...(404)
11 |
12 |
18 |
19 |
20 |
21 |
26 |
--------------------------------------------------------------------------------
/src/pages/HomePage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
37 |
38 |
78 |
--------------------------------------------------------------------------------
/src/pages/PracticePage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
14 |
15 |
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 |
--------------------------------------------------------------------------------