├── .cursorrules
├── .env.example
├── .github
└── FUNDING.yml
├── .gitignore
├── .vscode
└── extensions.json
├── README.md
├── doc
├── TS-FSRS_README.md
├── more_screenshots.png
└── screenshot.png
├── example_learning_events.csv
├── generate
├── .doc.md
├── 000_generate_simplified_geojson.py
├── 100_make_complete_country_list.py
├── 101_make_africa_country_list.py
├── 200_generate_translation_de.py
├── worldmap.demo.geo.json
└── worldmap.geo.json
├── index.html
├── package-lock.json
├── package.json
├── playwright.config.ts
├── playwright
├── index.html
└── index.ts
├── public
└── favicon.ico
├── src
├── App.vue
├── env.d.ts
├── favicon.ico
├── main.ts
├── modules
│ ├── map-data
│ │ ├── .doc.md
│ │ ├── country-lists
│ │ │ ├── africa.json
│ │ │ ├── africa.webp
│ │ │ └── all-countries.json
│ │ ├── map.demo.geo.json
│ │ └── map.geo.json
│ ├── misc-views
│ │ ├── .doc.md
│ │ ├── settings-view
│ │ │ ├── SettingsView.vue
│ │ │ └── defaultSettings.ts
│ │ └── stats-view
│ │ │ ├── .doc.md
│ │ │ ├── main-stats
│ │ │ └── StatsView.vue
│ │ │ └── per-country-stats
│ │ │ ├── CountryStatsView.vue
│ │ │ ├── DistanceProgressChart.vue
│ │ │ └── TimeProgressChart.vue
│ ├── play
│ │ ├── .doc.md
│ │ ├── map-renderer
│ │ │ ├── .doc.md
│ │ │ ├── WorldMap.vue
│ │ │ ├── WorldMapGame.vue
│ │ │ └── useCustomCursor.ts
│ │ └── play-modes
│ │ │ ├── challenge-play
│ │ │ ├── .doc.md
│ │ │ ├── ChallengeCompletedCard.vue
│ │ │ ├── ChallengeResults.vue
│ │ │ ├── ChallengeRulesPopup.vue
│ │ │ ├── PlayChallengeView.vue
│ │ │ ├── types.ts
│ │ │ └── useDailyChallenge.ts
│ │ │ ├── custom-play
│ │ │ ├── CustomPlay.vue
│ │ │ └── filter-modal
│ │ │ │ ├── FilterModal.vue
│ │ │ │ └── tabs
│ │ │ │ ├── TabCountrySelection.vue
│ │ │ │ ├── TabQuickSelection.vue
│ │ │ │ ├── TabWizardSelection.vue
│ │ │ │ └── useCountrySelection.ts
│ │ │ └── standard-play
│ │ │ ├── .doc.md
│ │ │ ├── PlayStandardView.vue
│ │ │ └── standard-play-progress-bar
│ │ │ └── useLearningProgress.ts
│ ├── randomness
│ │ └── random.ts
│ ├── shared-types
│ │ ├── .doc.md
│ │ └── types.ts
│ └── spaced-repetition-learning
│ │ ├── .doc.md
│ │ ├── calculate-learning
│ │ ├── types.ts
│ │ ├── useDexie.ts
│ │ └── useGeographyLearning.ts
│ │ └── log-learning
│ │ └── firebase.ts
├── router.ts
├── shims-vue.d.ts
├── style.css
├── tests
│ └── record_correctly_select_jamaica.spec.ts
└── vite-env.d.ts
├── tests
├── hellotest.spec.ts
├── record_go_to_challenge_mode.spec.ts
├── record_jamaica_to_caiman_flow_works.spec.ts
├── record_see_that_jamaica_miss_hint_shows.spec.ts
└── record_to_challenge_and_back_works.spec.ts
├── tsconfig.app.json
├── tsconfig.app.tsbuildinfo
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.node.tsbuildinfo
├── vite.config.d.ts
├── vite.config.ts
└── vitest.config.ts
/.cursorrules:
--------------------------------------------------------------------------------
1 | // Vue 3 Composition API .cursorrules
2 |
3 | // Vue 3 Composition API best practices
4 |
5 | const vue3CompositionApiBestPractices = [
6 | "Ask follow-up questions if you think this will improve the quality of your answer before generating",
7 | "Use setup() function for component logic",
8 | "Utilize ref and reactive for reactive state",
9 | "Implement computed properties with computed()",
10 | "Use watch and watchEffect for side effects",
11 | "Implement lifecycle hooks with onMounted, onUpdated, etc.",
12 | "Utilize provide/inject for dependency injection",
13 | "Implement logic in external, well-organized composables whenever possible",
14 | "Avoid custom CSS and style tags when possible",
15 | "Components that the router loads are to be put into view",
16 | "Components should be self-contained, and reasonably small",
17 | "Follow DRY, but also YAGNI, and do not over-engineer"
18 | ];
19 |
20 | // Folder structure
21 |
22 | const folderStructure = `
23 | src/
24 | views/
25 | components/
26 | composables/
27 | services/
28 | views/
29 | router/
30 | store/
31 | assets/
32 | App.vue
33 | main.ts
34 | types.ts
35 | `;
36 |
37 | // Additional instructions
38 |
39 | const additionalInstructions = `
40 | 1. Use TypeScript for type safety, by defining types in types.ts
41 | 2. Implement proper props and emits definitions
42 | 5. Implement proper error handling
43 | 6. Follow Vue 3 style guide and naming conventions
44 | 7. Use Vite for fast development and building
45 | 8. Unit-test all complex logic
46 | 9. Use Tailwind.CSS and Daisy UI for css and interface logic
47 | 10. Use dexie.js to manage a local database to handle data
48 | `;
49 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | VITE_FIREBASE_API_KEY=your_api_key_here
2 | VITE_FIREBASE_AUTH_DOMAIN=your_auth_domain_here
3 | VITE_FIREBASE_PROJECT_ID=your_project_id_here
4 | VITE_FIREBASE_STORAGE_BUCKET=your_storage_bucket_here
5 | VITE_FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id_here
6 | VITE_FIREBASE_APP_ID=your_app_id_here
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: koljasam
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | # Environment variables
27 | .env
28 | .env.local
29 | .env.*.local
30 |
31 | # Playwright
32 | /test-results/
33 | /playwright-report/
34 | /blob-report/
35 | /playwright/.cache/
36 |
37 |
38 | # Python Stuff
39 |
40 | # Byte-compiled / optimized / DLL files
41 | __pycache__/
42 | *.py[cod]
43 | *$py.class
44 |
45 | # C extensions
46 | *.so
47 |
48 | # Distribution / packaging
49 | .Python
50 | build/
51 | develop-eggs/
52 | dist/
53 | downloads/
54 | eggs/
55 | .eggs/
56 | lib/
57 | lib64/
58 | parts/
59 | sdist/
60 | var/
61 | wheels/
62 | share/python-wheels/
63 | *.egg-info/
64 | .installed.cfg
65 | *.egg
66 | MANIFEST
67 |
68 | # PyInstaller
69 | # Usually these files are written by a python script from a template
70 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
71 | *.manifest
72 | *.spec
73 |
74 | # Installer logs
75 | pip-log.txt
76 | pip-delete-this-directory.txt
77 |
78 | # Unit test / coverage reports
79 | htmlcov/
80 | .tox/
81 | .nox/
82 | .coverage
83 | .coverage.*
84 | .cache
85 | nosetests.xml
86 | coverage.xml
87 | *.cover
88 | *.py,cover
89 | .hypothesis/
90 | .pytest_cache/
91 | cover/
92 |
93 | # Translations
94 | *.mo
95 | *.pot
96 |
97 | # Django stuff:
98 | *.log
99 | local_settings.py
100 | db.sqlite3
101 | db.sqlite3-journal
102 |
103 | # Flask stuff:
104 | instance/
105 | .webassets-cache
106 |
107 | # Scrapy stuff:
108 | .scrapy
109 |
110 | # Sphinx documentation
111 | docs/_build/
112 |
113 | # PyBuilder
114 | .pybuilder/
115 | target/
116 |
117 | # Jupyter Notebook
118 | .ipynb_checkpoints
119 |
120 | # IPython
121 | profile_default/
122 | ipython_config.py
123 |
124 | # pyenv
125 | # For a library or package, you might want to ignore these files since the code is
126 | # intended to run in multiple environments; otherwise, check them in:
127 | # .python-version
128 |
129 | # pipenv
130 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
131 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
132 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
133 | # install all needed dependencies.
134 | #Pipfile.lock
135 |
136 | # UV
137 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
138 | # This is especially recommended for binary packages to ensure reproducibility, and is more
139 | # commonly ignored for libraries.
140 | #uv.lock
141 |
142 | # poetry
143 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
144 | # This is especially recommended for binary packages to ensure reproducibility, and is more
145 | # commonly ignored for libraries.
146 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
147 | #poetry.lock
148 |
149 | # pdm
150 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
151 | #pdm.lock
152 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
153 | # in version control.
154 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
155 | .pdm.toml
156 | .pdm-python
157 | .pdm-build/
158 |
159 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
160 | __pypackages__/
161 |
162 | # Celery stuff
163 | celerybeat-schedule
164 | celerybeat.pid
165 |
166 | # SageMath parsed files
167 | *.sage.py
168 |
169 | # Environments
170 | .env
171 | .venv
172 | env/
173 | venv/
174 | ENV/
175 | env.bak/
176 | venv.bak/
177 |
178 | # Spyder project settings
179 | .spyderproject
180 | .spyproject
181 |
182 | # Rope project settings
183 | .ropeproject
184 |
185 | # mkdocs documentation
186 | /site
187 |
188 | # mypy
189 | .mypy_cache/
190 | .dmypy.json
191 | dmypy.json
192 |
193 | # Pyre type checker
194 | .pyre/
195 |
196 | # pytype static type analyzer
197 | .pytype/
198 |
199 | # Cython debug symbols
200 | cython_debug/
201 |
202 | # PyCharm
203 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
204 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
205 | # and can be added to the global gitignore or merged into this file. For a more nuclear
206 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
207 | #.idea/
208 |
209 | # Ruff stuff:
210 | .ruff_cache/
211 |
212 | # PyPI configuration file
213 | .pypirc
214 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar"]
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Learn the Worldmap
2 |
3 | 
4 |
5 | _do you know where the countries are?_
6 |
7 | **[Play the free online practice game](https://map.koljapluemer.com)**
8 |
9 | [](https://ko-fi.com/S6S81CWUVD)
10 |
11 | ## Features
12 |
13 | 1. Spaced-Repetition based selection of which country should be learned
14 | 2. As the learner gets better, they have to find a given country with more precision
15 | 3. Detailed statistics about how learning a country is going
16 | 4. *WIP:* Daily Challenge Mode
17 | 5. *Planned:* Custom Playlist Mode to practice exactly what you want to practice
18 |
19 | 
20 |
21 |
22 | ## Development
23 |
24 | First of all: Feedback is welcome, bug reports are welcome, contributions are welcome. If anything's unclear, head over to issues and write what's on your mind.
25 |
26 | ### Running It
27 |
28 | 1. Clone the repository
29 | 2. Make sure you have `npm` installed
30 | 3. Run `npm i`
31 | 4. Run `npm run dev` and open the displayed link in your browser
32 |
33 | ### Folder Structure
34 |
35 | #### `src/`
36 |
37 | The actual app, a frontend-only web app using Vue.js
38 |
39 | This project follows a module-based approach, similar to the one explained in [this video](https://www.youtube.com/watch?v=iuyzO2QkY7A).
40 |
41 | You will not find folders such as `composables/` or `components/` here, as is custom in many vue projects.
42 | Instead, code is separated across features, with the intent that changes to feature X or use case Y can be done in the
43 | folder dedicated for it, instead of all across the codebase.
44 |
45 | You can find per-folder documentation for important directories in the `.doc.md` of the given folder. Alternatively, use the list below to directly jump to the relevant documentation:
46 |
47 |
48 | - [map-data](src/modules/map-data/.doc.md)
49 | - [play](src/modules/play/.doc.md)
50 | - [misc-views](src/modules/.doc.md)
51 |
52 | #### `generate/`
53 |
54 | A small collection of scripts generating and transforming some data for convenience. These are ran during development to support features such as exposing all countries in a certain continent. The resulting data is committed to source and contained within `src/`. There should be no reason to re-run these.
55 |
56 | I recommend using `venv` when running python scripts. Always run python scripts from source, otherwise filepaths in the scripts will get confused.
57 |
58 | ### Testing
59 |
60 | For now, there are some E2E tests, which were recorded with Playwright.
61 | You'll find them in the `tests/` directory.
62 |
63 | To run them, execute the app with the test env var set, by running:
64 |
65 | ```
66 | npm run testdev
67 | ```
68 |
69 | Leave this running, and in another terminal, run the tests:
70 |
71 | ```
72 | npm run test:e2e
73 | ```
--------------------------------------------------------------------------------
/doc/TS-FSRS_README.md:
--------------------------------------------------------------------------------
1 | [Introduction](./README.md) | [简体中文](./README_CN.md) |[はじめに](./README_JA.md)
2 |
3 | ---
4 |
5 | # About The
6 | [](https://www.npmjs.com/package/ts-fsrs)
7 | [](https://www.npmjs.com/package/ts-fsrs)
8 | [](https://codecov.io/gh/open-spaced-repetition/ts-fsrs)
9 | [](https://github.com/open-spaced-repetition/ts-fsrs/actions/workflows/npm-publish.yml)
10 | [](https://github.com/open-spaced-repetition/ts-fsrs/actions/workflows/deploy.yml)
11 |
12 | ts-fsrs is a versatile package based on TypeScript that supports [ES modules](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c), [CommonJS](https://en.wikipedia.org/wiki/CommonJS), and UMD. It implements the [Free Spaced Repetition Scheduler (FSRS) algorithm](https://github.com/open-spaced-repetition/free-spaced-repetition-scheduler), enabling developers to integrate FSRS into their flashcard applications to enhance the user learning experience.
13 |
14 | The workflow for TS-FSRS can be referenced from the following resources:
15 | > - google driver: [ts-fsrs-workflow.drawio](https://drive.google.com/file/d/1FLKjpt4T3Iis02vjoA10q7vxKCWwClfR/view?usp=sharing) (You may provide commentary)
16 | > - github: [ts-fsrs-workflow.drawio](./ts-fsrs-workflow.drawio)
17 |
18 |
19 | # Usage
20 | The `ts-fsrs@3.x` package requires Node.js version `16.0.0` or higher. Starting with `ts-fsrs@4.x`, the minimum required Node.js version is `18.0.0`.
21 | From version `3.5.6` onwards, ts-fsrs supports CommonJS, ESM, and UMD module systems.
22 |
23 | ```
24 | npm install ts-fsrs
25 | yarn install ts-fsrs
26 | pnpm install ts-fsrs
27 | bun install ts-fsrs
28 | ```
29 |
30 | # Example
31 |
32 | ```typescript
33 | import {createEmptyCard, formatDate, fsrs, generatorParameters, Rating, Grades} from 'ts-fsrs';
34 |
35 | const params = generatorParameters({ enable_fuzz: true, enable_short_term: false });
36 | const f = fsrs(params);
37 | const card = createEmptyCard(new Date('2022-2-1 10:00:00'));// createEmptyCard();
38 | const now = new Date('2022-2-2 10:00:00');// new Date();
39 | const scheduling_cards = f.repeat(card, now);
40 |
41 | // console.log(scheduling_cards);
42 | for (const item of scheduling_cards) {
43 | // grades = [Rating.Again, Rating.Hard, Rating.Good, Rating.Easy]
44 | const grade = item.log.rating
45 | const { log, card } = item;
46 | console.group(`${Rating[grade]}`);
47 | console.table({
48 | [`card_${Rating[grade]}`]: {
49 | ...card,
50 | due: formatDate(card.due),
51 | last_review: formatDate(card.last_review as Date),
52 | },
53 | });
54 | console.table({
55 | [`log_${Rating[grade]}`]: {
56 | ...log,
57 | review: formatDate(log.review),
58 | },
59 | });
60 | console.groupEnd();
61 | console.log('----------------------------------------------------------------');
62 | }
63 | ```
64 |
65 | More refer:
66 | - [Docs - Github Pages](https://open-spaced-repetition.github.io/ts-fsrs/)
67 | - [Example.html - Github Pages](https://open-spaced-repetition.github.io/ts-fsrs/example)
68 | - [Browser](https://github.com/open-spaced-repetition/ts-fsrs/blob/master/example/example.html) (ts-fsrs package using CDN)
69 | - [ts-fsrs-demo - Next.js+Prisma](https://github.com/ishiko732/ts-fsrs-demo)
70 | - [spaced - Next.js+Drizzle+tRPC](https://github.com/zsh-eng/spaced)
71 |
72 | # Basic Use
73 |
74 | ## 1. **Initialization**:
75 | To begin, create an empty card instance and set the current date(default: current time from system):
76 |
77 | ```typescript
78 | import { Card, createEmptyCard } from "ts-fsrs";
79 | let card: Card = createEmptyCard();
80 | // createEmptyCard(new Date('2022-2-1 10:00:00'));
81 | // createEmptyCard(new Date(Date.UTC(2023, 9, 18, 14, 32, 3, 370)));
82 | // createEmptyCard(new Date('2023-09-18T14:32:03.370Z'));
83 | ```
84 |
85 | ## 2. **Parameter Configuration**:
86 | The library allows for customization of SRS parameters. Use `generatorParameters` to produce the final set of parameters for the SRS algorithm. Here's an example setting a maximum interval:
87 |
88 | ```typescript
89 | import { Card, createEmptyCard, generatorParameters, FSRSParameters } from "ts-fsrs";
90 | let card: Card = createEmptyCard();
91 | const params: FSRSParameters = generatorParameters({ maximum_interval: 1000 });
92 | ```
93 |
94 | ## 3. **Scheduling with FSRS**:
95 | The core functionality lies in the `fsrs` function. When invoked, it returns a collection of cards scheduled based on different potential user ratings:
96 |
97 | ```typescript
98 | import {
99 | Card,
100 | createEmptyCard,
101 | generatorParameters,
102 | FSRSParameters,
103 | FSRS,
104 | RecordLog,
105 | } from "ts-fsrs";
106 |
107 | let card: Card = createEmptyCard();
108 | const f: FSRS = new FSRS(); // or const f: FSRS = fsrs(params);
109 | let scheduling_cards: RecordLog = f.repeat(card, new Date());
110 | // if you want to specify the grade, you can use the following code: (ts-fsrs >=4.0.0)
111 | // let scheduling_card: RecordLog = f.next(card, new Date(), Rating.Good);
112 | ```
113 |
114 | ## 4. **Retrieving Scheduled Cards**:
115 | Once you have the `scheduling_cards` object, you can retrieve cards based on user ratings. For instance, to access the card scheduled for a 'Good' rating:
116 |
117 | ```typescript
118 | const good: RecordLogItem = scheduling_cards[Rating.Good];
119 | const newCard: Card = good.card;
120 | ```
121 |
122 | Get the new state of card for each rating:
123 | ```typescript
124 | scheduling_cards[Rating.Again].card
125 | scheduling_cards[Rating.Again].log
126 |
127 | scheduling_cards[Rating.Hard].card
128 | scheduling_cards[Rating.Hard].log
129 |
130 | scheduling_cards[Rating.Good].card
131 | scheduling_cards[Rating.Good].log
132 |
133 | scheduling_cards[Rating.Easy].card
134 | scheduling_cards[Rating.Easy].log
135 | ```
136 |
137 | ## 5. **Understanding Card Attributes**:
138 | Each `Card` object consists of various attributes that determine its status, scheduling, and other metrics:
139 |
140 | ```typescript
141 | type Card = {
142 | due: Date; // Date when the card is next due for review
143 | stability: number; // A measure of how well the information is retained
144 | difficulty: number; // Reflects the inherent difficulty of the card content
145 | elapsed_days: number; // Days since the card was last reviewed
146 | scheduled_days: number; // The interval at which the card is next scheduled
147 | reps: number; // Total number of times the card has been reviewed
148 | lapses: number; // Times the card was forgotten or remembered incorrectly
149 | state: State; // The current state of the card (New, Learning, Review, Relearning)
150 | last_review?: Date; // The most recent review date, if applicable
151 | };
152 | ```
153 |
154 | ## 6. **Understanding Log Attributes**:
155 | Each `ReviewLog` object contains various attributes that determine the review record information associated with the card, used for analysis, undoing the review, and [optimization (WIP)](https://github.com/open-spaced-repetition/fsrs-optimizer).
156 |
157 | ```typescript
158 | type ReviewLog = {
159 | rating: Rating; // Rating of the review (Again, Hard, Good, Easy)
160 | state: State; // State of the review (New, Learning, Review, Relearning)
161 | due: Date; // Date of the last scheduling
162 | stability: number; // Stability of the card before the review
163 | difficulty: number; // Difficulty of the card before the review
164 | elapsed_days: number; // Number of days elapsed since the last review
165 | last_elapsed_days: number; // Number of days between the last two reviews
166 | scheduled_days: number; // Number of days until the next review
167 | review: Date; // Date of the review
168 | }
169 | ```
--------------------------------------------------------------------------------
/doc/more_screenshots.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koljapluemer/learn-worldmap/3f46c03dd2cf1498b094693e1ce42487abeaf9ed/doc/more_screenshots.png
--------------------------------------------------------------------------------
/doc/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koljapluemer/learn-worldmap/3f46c03dd2cf1498b094693e1ce42487abeaf9ed/doc/screenshot.png
--------------------------------------------------------------------------------
/example_learning_events.csv:
--------------------------------------------------------------------------------
1 | id,deviceId,timestamp,country,msFromExerciseToFirstClick,msFromExerciseToFinishClick,numberOfClicksNeeded,distanceOfFirstClickToCenterOfCountry
2 | 1,550e8400-e29b-41d4-a716-446655440000,2024-03-20T14:30:00.000Z,France,1500,3000,2,156.7
--------------------------------------------------------------------------------
/generate/.doc.md:
--------------------------------------------------------------------------------
1 | Python scrips to define simplified data for specific needs.
2 |
3 |
4 | Always run from base directory so the paths check out.
--------------------------------------------------------------------------------
/generate/000_generate_simplified_geojson.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | def process_geojson():
5 | # Read the input GeoJSON file
6 | with open('generate/worldmap.geo.json', 'r', encoding='utf-8') as f:
7 | data = json.load(f)
8 |
9 | # Create a new FeatureCollection with the same structure but simplified properties
10 | simplified_data = {
11 | "type": "FeatureCollection",
12 | "features": [
13 | {
14 | "type": "Feature",
15 | "properties": {
16 | "name": feature['properties']['name']
17 | },
18 | "geometry": feature['geometry']
19 | }
20 | for feature in data['features']
21 | ]
22 | }
23 |
24 | # Create the output directory if it doesn't exist
25 | os.makedirs('src/modules/map-data', exist_ok=True)
26 |
27 | # Save the simplified data (compressed)
28 | output_path = 'src/modules/map-data/map.geo.json'
29 | with open(output_path, 'w', encoding='utf-8') as f:
30 | json.dump(simplified_data, f, ensure_ascii=False, separators=(',', ':'))
31 |
32 | # Create demo file with just one country (pretty printed)
33 | demo_data = {
34 | "type": "FeatureCollection",
35 | "features": [
36 | {
37 | "type": "Feature",
38 | "properties": {
39 | "name": "Costa Rica"
40 | },
41 | "geometry": next(f['geometry'] for f in data['features'] if f['properties']['name'] == "Costa Rica")
42 | }
43 | ]
44 | }
45 | demo_path = 'src/modules/map-data/map.demo.geo.json'
46 | with open(demo_path, 'w', encoding='utf-8') as f:
47 | json.dump(demo_data, f, ensure_ascii=False, indent=2)
48 |
49 | print(f"Processed {len(simplified_data['features'])} countries")
50 | print(f"Output saved to {output_path}")
51 | print(f"Demo file saved to {demo_path}")
52 |
53 | if __name__ == '__main__':
54 | process_geojson()
55 |
--------------------------------------------------------------------------------
/generate/100_make_complete_country_list.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | def extract_all_countries():
5 | # Get the path to the GeoJSON file
6 | current_dir = os.path.dirname(os.path.abspath(__file__))
7 | geo_json_path = 'generate/worldmap.geo.json'
8 |
9 | try:
10 | # Read the GeoJSON file
11 | with open(geo_json_path, 'r', encoding='utf-8') as f:
12 | data = json.load(f)
13 |
14 | # Extract all countries
15 | all_countries = []
16 | for feature in data['features']:
17 | country_name = feature['properties'].get('name')
18 | if country_name:
19 | all_countries.append(country_name)
20 |
21 | # Sort the list alphabetically
22 | all_countries.sort()
23 |
24 | # Save to all-countries.json
25 | output_path = 'src/modules/map-data/country-lists/all-countries.json'
26 | with open(output_path, 'w', encoding='utf-8') as f:
27 | json.dump(all_countries, f, indent=2)
28 |
29 | print(f"Found {len(all_countries)} countries")
30 | print("Countries saved to all-countries.json")
31 |
32 | # Print the countries found for verification
33 | if all_countries:
34 | print("\nCountries found:")
35 | for country in all_countries:
36 | print(f"- {country}")
37 | else:
38 | print("\nNo countries found in the GeoJSON file.")
39 |
40 | except FileNotFoundError:
41 | print(f"Error: Could not find the file at {geo_json_path}")
42 | except json.JSONDecodeError:
43 | print("Error: The GeoJSON file is not properly formatted")
44 | except Exception as e:
45 | print(f"An unexpected error occurred: {str(e)}")
46 |
47 | if __name__ == '__main__':
48 | extract_all_countries()
49 |
--------------------------------------------------------------------------------
/generate/101_make_africa_country_list.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | def extract_african_countries():
5 | # Get the path to the GeoJSON file
6 | current_dir = os.path.dirname(os.path.abspath(__file__))
7 | geo_json_path = 'generate/worldmap.geo.json'
8 |
9 |
10 | try:
11 | # Read the GeoJSON file
12 | with open(geo_json_path, 'r', encoding='utf-8') as f:
13 | data = json.load(f)
14 |
15 | # Extract African countries
16 | african_countries = []
17 | for feature in data['features']:
18 | # Check if the country is in Africa (continent property)
19 | if feature['properties'].get('continent') == 'Africa':
20 | country_name = feature['properties'].get('name')
21 | if country_name:
22 | african_countries.append(country_name)
23 |
24 | # Sort the list alphabetically
25 | african_countries.sort()
26 |
27 | # Save to africa.json
28 | output_path = 'src/modules/map-data/country-lists/africa.json'
29 | with open(output_path, 'w', encoding='utf-8') as f:
30 | json.dump(african_countries, f, indent=2)
31 |
32 | print(f"Found {len(african_countries)} African countries")
33 | print("Countries saved to africa.json")
34 |
35 | # Print the countries found for verification
36 | if african_countries:
37 | print("\nCountries found:")
38 | for country in african_countries:
39 | print(f"- {country}")
40 | else:
41 | print("\nNo African countries found in the demo file.")
42 | print("Note: The demo file currently contains Costa Rica data as a test case.")
43 |
44 | except FileNotFoundError:
45 | print(f"Error: Could not find the file at {geo_json_path}")
46 | except json.JSONDecodeError:
47 | print("Error: The GeoJSON file is not properly formatted")
48 | except Exception as e:
49 | print(f"An unexpected error occurred: {str(e)}")
50 |
51 | if __name__ == '__main__':
52 | extract_african_countries()
53 |
--------------------------------------------------------------------------------
/generate/200_generate_translation_de.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koljapluemer/learn-worldmap/3f46c03dd2cf1498b094693e1ce42487abeaf9ed/generate/200_generate_translation_de.py
--------------------------------------------------------------------------------
/generate/worldmap.demo.geo.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "FeatureCollection",
3 | "features": [
4 | {
5 | "type": "Feature",
6 | "properties": {
7 | "featurecla": "Admin-0 country",
8 | "scalerank": 1,
9 | "labelrank": 5,
10 | "sovereignt": "Costa Rica",
11 | "sov_a3": "CRI",
12 | "adm0_dif": 0,
13 | "level": 2,
14 | "type": "Sovereign country",
15 | "tlc": "1",
16 | "admin": "Costa Rica",
17 | "adm0_a3": "CRI",
18 | "geou_dif": 0,
19 | "geounit": "Costa Rica",
20 | "gu_a3": "CRI",
21 | "su_dif": 0,
22 | "subunit": "Costa Rica",
23 | "su_a3": "CRI",
24 | "brk_diff": 0,
25 | "name": "Costa Rica",
26 | "name_long": "Costa Rica",
27 | "brk_a3": "CRI",
28 | "brk_name": "Costa Rica",
29 | "brk_group": null,
30 | "abbrev": "C.R.",
31 | "postal": "CR",
32 | "formal_en": "Republic of Costa Rica",
33 | "formal_fr": null,
34 | "name_ciawf": "Costa Rica",
35 | "note_adm0": null,
36 | "note_brk": null,
37 | "name_sort": "Costa Rica",
38 | "name_alt": null,
39 | "mapcolor7": 3,
40 | "mapcolor8": 2,
41 | "mapcolor9": 4,
42 | "mapcolor13": 2,
43 | "pop_est": 5047561,
44 | "pop_rank": 13,
45 | "pop_year": 2019,
46 | "gdp_md": 61801,
47 | "gdp_year": 2019,
48 | "economy": "5. Emerging region: G20",
49 | "income_grp": "3. Upper middle income",
50 | "fips_10": "CS",
51 | "iso_a2": "CR",
52 | "iso_a2_eh": "CR",
53 | "iso_a3": "CRI",
54 | "iso_a3_eh": "CRI",
55 | "iso_n3": "188",
56 | "iso_n3_eh": "188",
57 | "un_a3": "188",
58 | "wb_a2": "CR",
59 | "wb_a3": "CRI",
60 | "woe_id": 23424791,
61 | "woe_id_eh": 23424791,
62 | "woe_note": "Exact WOE match as country",
63 | "adm0_iso": "CRI",
64 | "adm0_diff": null,
65 | "adm0_tlc": "CRI",
66 | "adm0_a3_us": "CRI",
67 | "adm0_a3_fr": "CRI",
68 | "adm0_a3_ru": "CRI",
69 | "adm0_a3_es": "CRI",
70 | "adm0_a3_cn": "CRI",
71 | "adm0_a3_tw": "CRI",
72 | "adm0_a3_in": "CRI",
73 | "adm0_a3_np": "CRI",
74 | "adm0_a3_pk": "CRI",
75 | "adm0_a3_de": "CRI",
76 | "adm0_a3_gb": "CRI",
77 | "adm0_a3_br": "CRI",
78 | "adm0_a3_il": "CRI",
79 | "adm0_a3_ps": "CRI",
80 | "adm0_a3_sa": "CRI",
81 | "adm0_a3_eg": "CRI",
82 | "adm0_a3_ma": "CRI",
83 | "adm0_a3_pt": "CRI",
84 | "adm0_a3_ar": "CRI",
85 | "adm0_a3_jp": "CRI",
86 | "adm0_a3_ko": "CRI",
87 | "adm0_a3_vn": "CRI",
88 | "adm0_a3_tr": "CRI",
89 | "adm0_a3_id": "CRI",
90 | "adm0_a3_pl": "CRI",
91 | "adm0_a3_gr": "CRI",
92 | "adm0_a3_it": "CRI",
93 | "adm0_a3_nl": "CRI",
94 | "adm0_a3_se": "CRI",
95 | "adm0_a3_bd": "CRI",
96 | "adm0_a3_ua": "CRI",
97 | "adm0_a3_un": -99,
98 | "adm0_a3_wb": -99,
99 | "continent": "North America",
100 | "region_un": "Americas",
101 | "subregion": "Central America",
102 | "region_wb": "Latin America & Caribbean",
103 | "name_len": 10,
104 | "long_len": 10,
105 | "abbrev_len": 4,
106 | "tiny": -99,
107 | "homepart": 1,
108 | "min_zoom": 0,
109 | "min_label": 2.5,
110 | "max_label": 8,
111 | "label_x": -84.077922,
112 | "label_y": 10.0651,
113 | "ne_id": 1159320525,
114 | "wikidataid": "Q800",
115 | "name_ar": "كوستاريكا",
116 | "name_bn": "কোস্টা রিকা",
117 | "name_de": "Costa Rica",
118 | "name_en": "Costa Rica",
119 | "name_es": "Costa Rica",
120 | "name_fa": "کاستاریکا",
121 | "name_fr": "Costa Rica",
122 | "name_el": "Κόστα Ρίκα",
123 | "name_he": "קוסטה ריקה",
124 | "name_hi": "कोस्टा रीका",
125 | "name_hu": "Costa Rica",
126 | "name_id": "Kosta Rika",
127 | "name_it": "Costa Rica",
128 | "name_ja": "コスタリカ",
129 | "name_ko": "코스타리카",
130 | "name_nl": "Costa Rica",
131 | "name_pl": "Kostaryka",
132 | "name_pt": "Costa Rica",
133 | "name_ru": "Коста-Рика",
134 | "name_sv": "Costa Rica",
135 | "name_tr": "Kosta Rika",
136 | "name_uk": "Коста-Рика",
137 | "name_ur": "کوسٹاریکا",
138 | "name_vi": "Costa Rica",
139 | "name_zh": "哥斯达黎加",
140 | "name_zht": "哥斯大黎加",
141 | "fclass_iso": "Admin-0 country",
142 | "tlc_diff": null,
143 | "fclass_tlc": "Admin-0 country",
144 | "fclass_us": null,
145 | "fclass_fr": null,
146 | "fclass_ru": null,
147 | "fclass_es": null,
148 | "fclass_cn": null,
149 | "fclass_tw": null,
150 | "fclass_in": null,
151 | "fclass_np": null,
152 | "fclass_pk": null,
153 | "fclass_de": null,
154 | "fclass_gb": null,
155 | "fclass_br": null,
156 | "fclass_il": null,
157 | "fclass_ps": null,
158 | "fclass_sa": null,
159 | "fclass_eg": null,
160 | "fclass_ma": null,
161 | "fclass_pt": null,
162 | "fclass_ar": null,
163 | "fclass_jp": null,
164 | "fclass_ko": null,
165 | "fclass_vn": null,
166 | "fclass_tr": null,
167 | "fclass_id": null,
168 | "fclass_pl": null,
169 | "fclass_gr": null,
170 | "fclass_it": null,
171 | "fclass_nl": null,
172 | "fclass_se": null,
173 | "fclass_bd": null,
174 | "fclass_ua": null,
175 | "filename": "CRI.geojson"
176 | },
177 | "geometry": {}
178 | }
179 | ]
180 | }
181 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Learn the World Map
7 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-learning-app-template",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vue-tsc && vite build",
9 | "preview": "vite preview",
10 | "test": "NODE_NO_WARNINGS=1 playwright test",
11 | "test:ui": "NODE_NO_WARNINGS=1 vitest --ui",
12 | "test:coverage": "NODE_NO_WARNINGS=1 vitest run --coverage",
13 | "test:e2e": "NODE_NO_WARNINGS=1 playwright test",
14 | "testdev": "npm run dev -- --mode test"
15 | },
16 | "dependencies": {
17 | "@tailwindcss/vite": "^4.0.17",
18 | "@types/uuid": "^10.0.0",
19 | "@types/vue": "^2.0.0",
20 | "@vue/runtime-core": "^3.5.13",
21 | "d3": "^7.9.0",
22 | "dexie": "^4.0.11",
23 | "echarts": "^5.6.0",
24 | "firebase": "^11.5.0",
25 | "ts-fsrs": "^4.7.0",
26 | "uuid": "^11.1.0",
27 | "vue": "^3.5.13",
28 | "vue-router": "^4.5.0"
29 | },
30 | "devDependencies": {
31 | "@playwright/experimental-ct-vue": "^1.51.1",
32 | "@testing-library/vue": "^8.1.0",
33 | "@tsconfig/node18": "^18.2.4",
34 | "@types/d3": "^7.4.3",
35 | "@types/node": "^22.13.14",
36 | "@vitejs/plugin-vue": "^5.2.3",
37 | "@vue/test-utils": "^2.4.6",
38 | "@vue/tsconfig": "^0.7.0",
39 | "autoprefixer": "^10.4.21",
40 | "daisyui": "^5.0.9",
41 | "tailwindcss": "^4.0.17",
42 | "typescript": "^5.8.2",
43 | "vite": "^6.2.3",
44 | "vitest": "^3.0.9",
45 | "vue-tsc": "^2.2.8"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/experimental-ct-vue';
2 |
3 | /**
4 | * See https://playwright.dev/docs/test-configuration.
5 | */
6 | export default defineConfig({
7 | testDir: 'tests',
8 | /* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */
9 | snapshotDir: './__snapshots__',
10 | /* Maximum time one test can run for. */
11 | timeout: 10 * 1000,
12 | /* Run tests in files in parallel */
13 | fullyParallel: true,
14 | /* Fail the build on CI if you accidentally left test.only in the source code. */
15 | forbidOnly: !!process.env.CI,
16 | /* Retry on CI only */
17 | retries: process.env.CI ? 2 : 0,
18 | /* Opt out of parallel tests on CI. */
19 | workers: process.env.CI ? 1 : undefined,
20 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
21 | reporter: 'html',
22 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
23 | use: {
24 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
25 | trace: 'on-first-retry',
26 |
27 | /* Port to use for Playwright component endpoint. */
28 | ctPort: 3100,
29 | },
30 |
31 | /* Configure projects for major browsers */
32 | projects: [
33 | {
34 | name: 'chromium',
35 | use: { ...devices['Desktop Chrome'] },
36 | },
37 | ],
38 |
39 | });
40 |
--------------------------------------------------------------------------------
/playwright/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Testing Page
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/playwright/index.ts:
--------------------------------------------------------------------------------
1 | // Import styles, initialize component theme here.
2 | // import '../src/common.css';
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koljapluemer/learn-worldmap/3f46c03dd2cf1498b094693e1ce42487abeaf9ed/public/favicon.ico
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Learn the World Map
6 |
7 | Play
8 | Custom
9 | Challenge
10 | Stats
11 | Settings
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Made with ♥ by Kolja
30 | Sam
31 |
32 |
34 |
35 |
36 |
37 | I'm using the privacy-friendly Goatcounter to track page views and an I store some pseudonymous learning data. No personal
39 | data is collected, and cookies are used solely for tracking your learning progress on your device. This app is
40 | open source .
42 |
43 |
44 |
45 |
46 |
47 |
50 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module '*.vue' {
4 | import type { DefineComponent } from 'vue'
5 | const component: DefineComponent<{}, {}, any>
6 | export default component
7 | }
8 |
9 | // Ensure TypeScript picks up Vue type declarations
10 | import 'vue'
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koljapluemer/learn-worldmap/3f46c03dd2cf1498b094693e1ce42487abeaf9ed/src/favicon.ico
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import './style.css'
3 | import App from './App.vue'
4 | import router from './router'
5 |
6 | const app = createApp(App)
7 | app.use(router)
8 | app.mount('#app')
9 |
--------------------------------------------------------------------------------
/src/modules/map-data/.doc.md:
--------------------------------------------------------------------------------
1 | - this module handles the actual hard map data
2 | - contains a hardcoded geojson with map data, as well as a script to expose this data to the parts of the app that need it.
--------------------------------------------------------------------------------
/src/modules/map-data/country-lists/africa.json:
--------------------------------------------------------------------------------
1 | [
2 | "Algeria",
3 | "Angola",
4 | "Benin",
5 | "Botswana",
6 | "Burkina Faso",
7 | "Burundi",
8 | "Cabo Verde",
9 | "Cameroon",
10 | "Central African Rep.",
11 | "Chad",
12 | "Comoros",
13 | "Congo",
14 | "C\u00f4te d'Ivoire",
15 | "Dem. Rep. Congo",
16 | "Djibouti",
17 | "Egypt",
18 | "Eq. Guinea",
19 | "Eritrea",
20 | "Ethiopia",
21 | "Gabon",
22 | "Gambia",
23 | "Ghana",
24 | "Guinea",
25 | "Guinea-Bissau",
26 | "Kenya",
27 | "Lesotho",
28 | "Liberia",
29 | "Libya",
30 | "Madagascar",
31 | "Malawi",
32 | "Mali",
33 | "Mauritania",
34 | "Morocco",
35 | "Mozambique",
36 | "Namibia",
37 | "Niger",
38 | "Nigeria",
39 | "Rwanda",
40 | "S. Sudan",
41 | "Senegal",
42 | "Sierra Leone",
43 | "Somalia",
44 | "Somaliland",
45 | "South Africa",
46 | "Sudan",
47 | "S\u00e3o Tom\u00e9 and Principe",
48 | "Tanzania",
49 | "Togo",
50 | "Tunisia",
51 | "Uganda",
52 | "W. Sahara",
53 | "Zambia",
54 | "Zimbabwe",
55 | "eSwatini"
56 | ]
--------------------------------------------------------------------------------
/src/modules/map-data/country-lists/africa.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koljapluemer/learn-worldmap/3f46c03dd2cf1498b094693e1ce42487abeaf9ed/src/modules/map-data/country-lists/africa.webp
--------------------------------------------------------------------------------
/src/modules/map-data/country-lists/all-countries.json:
--------------------------------------------------------------------------------
1 | [
2 | "Afghanistan",
3 | "Albania",
4 | "Algeria",
5 | "American Samoa",
6 | "Andorra",
7 | "Angola",
8 | "Anguilla",
9 | "Antigua and Barb.",
10 | "Argentina",
11 | "Armenia",
12 | "Aruba",
13 | "Ashmore and Cartier Is.",
14 | "Australia",
15 | "Austria",
16 | "Azerbaijan",
17 | "Bahamas",
18 | "Bahrain",
19 | "Bangladesh",
20 | "Barbados",
21 | "Belarus",
22 | "Belgium",
23 | "Belize",
24 | "Benin",
25 | "Bermuda",
26 | "Bhutan",
27 | "Bolivia",
28 | "Bosnia and Herz.",
29 | "Botswana",
30 | "Br. Indian Ocean Ter.",
31 | "Brazil",
32 | "British Virgin Is.",
33 | "Brunei",
34 | "Bulgaria",
35 | "Burkina Faso",
36 | "Burundi",
37 | "Cabo Verde",
38 | "Cambodia",
39 | "Cameroon",
40 | "Canada",
41 | "Cayman Is.",
42 | "Central African Rep.",
43 | "Chad",
44 | "Chile",
45 | "China",
46 | "Colombia",
47 | "Comoros",
48 | "Congo",
49 | "Cook Is.",
50 | "Costa Rica",
51 | "Croatia",
52 | "Cuba",
53 | "Cura\u00e7ao",
54 | "Cyprus",
55 | "Czechia",
56 | "C\u00f4te d'Ivoire",
57 | "Dem. Rep. Congo",
58 | "Denmark",
59 | "Djibouti",
60 | "Dominica",
61 | "Dominican Rep.",
62 | "Ecuador",
63 | "Egypt",
64 | "El Salvador",
65 | "Eq. Guinea",
66 | "Eritrea",
67 | "Estonia",
68 | "Ethiopia",
69 | "Faeroe Is.",
70 | "Falkland Is.",
71 | "Fiji",
72 | "Finland",
73 | "Fr. Polynesia",
74 | "France",
75 | "Gabon",
76 | "Gambia",
77 | "Georgia",
78 | "Germany",
79 | "Ghana",
80 | "Greece",
81 | "Greenland",
82 | "Grenada",
83 | "Guam",
84 | "Guatemala",
85 | "Guernsey",
86 | "Guinea",
87 | "Guinea-Bissau",
88 | "Guyana",
89 | "Haiti",
90 | "Heard I. and McDonald Is.",
91 | "Honduras",
92 | "Hong Kong",
93 | "Hungary",
94 | "Iceland",
95 | "India",
96 | "Indian Ocean Ter.",
97 | "Indonesia",
98 | "Iran",
99 | "Iraq",
100 | "Ireland",
101 | "Isle of Man",
102 | "Israel",
103 | "Italy",
104 | "Jamaica",
105 | "Japan",
106 | "Jersey",
107 | "Jordan",
108 | "Kazakhstan",
109 | "Kenya",
110 | "Kiribati",
111 | "Kosovo",
112 | "Kuwait",
113 | "Kyrgyzstan",
114 | "Laos",
115 | "Latvia",
116 | "Lebanon",
117 | "Lesotho",
118 | "Liberia",
119 | "Libya",
120 | "Liechtenstein",
121 | "Lithuania",
122 | "Luxembourg",
123 | "Macao",
124 | "Madagascar",
125 | "Malawi",
126 | "Malaysia",
127 | "Maldives",
128 | "Mali",
129 | "Malta",
130 | "Marshall Is.",
131 | "Mauritania",
132 | "Mauritius",
133 | "Mexico",
134 | "Micronesia",
135 | "Moldova",
136 | "Monaco",
137 | "Mongolia",
138 | "Montenegro",
139 | "Montserrat",
140 | "Morocco",
141 | "Mozambique",
142 | "Myanmar",
143 | "N. Cyprus",
144 | "N. Mariana Is.",
145 | "Namibia",
146 | "Nauru",
147 | "Nepal",
148 | "Netherlands",
149 | "New Caledonia",
150 | "New Zealand",
151 | "Nicaragua",
152 | "Niger",
153 | "Nigeria",
154 | "Niue",
155 | "Norfolk Island",
156 | "North Korea",
157 | "North Macedonia",
158 | "Norway",
159 | "Oman",
160 | "Pakistan",
161 | "Palau",
162 | "Palestine",
163 | "Panama",
164 | "Papua New Guinea",
165 | "Paraguay",
166 | "Peru",
167 | "Philippines",
168 | "Pitcairn Is.",
169 | "Poland",
170 | "Portugal",
171 | "Puerto Rico",
172 | "Qatar",
173 | "Romania",
174 | "Russia",
175 | "Rwanda",
176 | "S. Geo. and the Is.",
177 | "S. Sudan",
178 | "Saint Helena",
179 | "Saint Lucia",
180 | "Samoa",
181 | "San Marino",
182 | "Saudi Arabia",
183 | "Senegal",
184 | "Serbia",
185 | "Seychelles",
186 | "Siachen Glacier",
187 | "Sierra Leone",
188 | "Singapore",
189 | "Sint Maarten",
190 | "Slovakia",
191 | "Slovenia",
192 | "Solomon Is.",
193 | "Somalia",
194 | "Somaliland",
195 | "South Africa",
196 | "South Korea",
197 | "Spain",
198 | "Sri Lanka",
199 | "St-Barth\u00e9lemy",
200 | "St-Martin",
201 | "St. Kitts and Nevis",
202 | "St. Pierre and Miquelon",
203 | "St. Vin. and Gren.",
204 | "Sudan",
205 | "Suriname",
206 | "Sweden",
207 | "Switzerland",
208 | "Syria",
209 | "S\u00e3o Tom\u00e9 and Principe",
210 | "Taiwan",
211 | "Tajikistan",
212 | "Tanzania",
213 | "Thailand",
214 | "Timor-Leste",
215 | "Togo",
216 | "Tonga",
217 | "Trinidad and Tobago",
218 | "Tunisia",
219 | "Turkey",
220 | "Turkmenistan",
221 | "Turks and Caicos Is.",
222 | "Tuvalu",
223 | "U.S. Virgin Is.",
224 | "Uganda",
225 | "Ukraine",
226 | "United Arab Emirates",
227 | "United Kingdom",
228 | "United States of America",
229 | "Uruguay",
230 | "Uzbekistan",
231 | "Vanuatu",
232 | "Vatican",
233 | "Venezuela",
234 | "Vietnam",
235 | "W. Sahara",
236 | "Wallis and Futuna Is.",
237 | "Yemen",
238 | "Zambia",
239 | "Zimbabwe",
240 | "eSwatini",
241 | "\u00c5land"
242 | ]
--------------------------------------------------------------------------------
/src/modules/map-data/map.demo.geo.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "FeatureCollection",
3 | "features": [
4 | {
5 | "type": "Feature",
6 | "properties": {
7 | "name": "Costa Rica"
8 | },
9 | "geometry": {
10 | "type": "Polygon",
11 | "coordinates": [
12 | [
13 | [
14 | -82.56357421874999,
15 | 9.57666015625
16 | ],
17 | [
18 | -82.56923828125,
19 | 9.558203125
20 | ],
21 | [
22 | -82.58652343749999,
23 | 9.538818359375
24 | ],
25 | [
26 | -82.611279296875,
27 | 9.519238281249997
28 | ],
29 | [
30 | -82.64409179687499,
31 | 9.505859375
32 | ],
33 | [
34 | -82.723388671875,
35 | 9.54609375
36 | ],
37 | [
38 | -82.801025390625,
39 | 9.591796875
40 | ],
41 | [
42 | -82.843994140625,
43 | 9.57080078125
44 | ],
45 | [
46 | -82.86015624999999,
47 | 9.511474609375
48 | ],
49 | [
50 | -82.88896484374999,
51 | 9.481005859374989
52 | ],
53 | [
54 | -82.925048828125,
55 | 9.469042968749989
56 | ],
57 | [
58 | -82.93984375,
59 | 9.449169921874997
60 | ],
61 | [
62 | -82.942822265625,
63 | 9.248876953124991
64 | ],
65 | [
66 | -82.94033203125,
67 | 9.060107421874989
68 | ],
69 | [
70 | -82.88134765625,
71 | 9.055859375
72 | ],
73 | [
74 | -82.78305664062499,
75 | 8.990283203124989
76 | ],
77 | [
78 | -82.741162109375,
79 | 8.951708984374989
80 | ],
81 | [
82 | -82.72783203124999,
83 | 8.916064453124989
84 | ],
85 | [
86 | -82.739990234375,
87 | 8.898583984374994
88 | ],
89 | [
90 | -82.8119140625,
91 | 8.857421875
92 | ],
93 | [
94 | -82.881982421875,
95 | 8.805322265624994
96 | ],
97 | [
98 | -82.91704101562499,
99 | 8.740332031249991
100 | ],
101 | [
102 | -82.855712890625,
103 | 8.635302734374989
104 | ],
105 | [
106 | -82.84262695312499,
107 | 8.56396484375
108 | ],
109 | [
110 | -82.84477539062499,
111 | 8.489355468749991
112 | ],
113 | [
114 | -82.86162109374999,
115 | 8.453515625
116 | ],
117 | [
118 | -82.99755859375,
119 | 8.367773437499991
120 | ],
121 | [
122 | -83.02734375,
123 | 8.337744140624991
124 | ],
125 | [
126 | -83.023388671875,
127 | 8.316015625
128 | ],
129 | [
130 | -82.9484375,
131 | 8.2568359375
132 | ],
133 | [
134 | -82.91289062499999,
135 | 8.199609375
136 | ],
137 | [
138 | -82.88330078125,
139 | 8.130566406249997
140 | ],
141 | [
142 | -82.879345703125,
143 | 8.070654296874991
144 | ],
145 | [
146 | -82.947265625,
147 | 8.181738281249991
148 | ],
149 | [
150 | -83.041455078125,
151 | 8.287744140624994
152 | ],
153 | [
154 | -83.12333984374999,
155 | 8.353076171874989
156 | ],
157 | [
158 | -83.12958984375,
159 | 8.50546875
160 | ],
161 | [
162 | -83.16240234374999,
163 | 8.588183593749989
164 | ],
165 | [
166 | -83.285791015625,
167 | 8.664355468749989
168 | ],
169 | [
170 | -83.39140624999999,
171 | 8.717724609374997
172 | ],
173 | [
174 | -83.4697265625,
175 | 8.706835937499989
176 | ],
177 | [
178 | -83.42177734375,
179 | 8.619238281249991
180 | ],
181 | [
182 | -83.29775390625,
183 | 8.506884765624989
184 | ],
185 | [
186 | -83.28955078125,
187 | 8.463818359374997
188 | ],
189 | [
190 | -83.29150390625,
191 | 8.406005859375
192 | ],
193 | [
194 | -83.37680664062499,
195 | 8.414892578124991
196 | ],
197 | [
198 | -83.45205078125,
199 | 8.4384765625
200 | ],
201 | [
202 | -83.54375,
203 | 8.445849609374989
204 | ],
205 | [
206 | -83.604736328125,
207 | 8.480322265624991
208 | ],
209 | [
210 | -83.73408203125,
211 | 8.614453125
212 | ],
213 | [
214 | -83.6421875,
215 | 8.72890625
216 | ],
217 | [
218 | -83.613720703125,
219 | 8.804052734374991
220 | ],
221 | [
222 | -83.616162109375,
223 | 8.959814453124991
224 | ],
225 | [
226 | -83.63725585937499,
227 | 9.035351562499997
228 | ],
229 | [
230 | -83.73691406249999,
231 | 9.150292968749994
232 | ],
233 | [
234 | -83.89555664062499,
235 | 9.276416015624989
236 | ],
237 | [
238 | -84.11787109375,
239 | 9.379443359374989
240 | ],
241 | [
242 | -84.22236328125,
243 | 9.4625
244 | ],
245 | [
246 | -84.482666015625,
247 | 9.526171874999989
248 | ],
249 | [
250 | -84.58159179687499,
251 | 9.568359375
252 | ],
253 | [
254 | -84.65888671875,
255 | 9.646679687499997
256 | ],
257 | [
258 | -84.67045898437499,
259 | 9.702880859375
260 | ],
261 | [
262 | -84.64306640625,
263 | 9.789404296874991
264 | ],
265 | [
266 | -84.71494140624999,
267 | 9.8994140625
268 | ],
269 | [
270 | -85.025048828125,
271 | 10.11572265625
272 | ],
273 | [
274 | -85.1984375,
275 | 10.1953125
276 | ],
277 | [
278 | -85.23564453124999,
279 | 10.242089843749994
280 | ],
281 | [
282 | -85.26318359375,
283 | 10.256640624999989
284 | ],
285 | [
286 | -85.2365234375,
287 | 10.107373046874997
288 | ],
289 | [
290 | -85.16074218749999,
291 | 10.017431640624991
292 | ],
293 | [
294 | -84.96279296875,
295 | 9.933447265624991
296 | ],
297 | [
298 | -84.908349609375,
299 | 9.884570312499989
300 | ],
301 | [
302 | -84.88642578125,
303 | 9.820947265624994
304 | ],
305 | [
306 | -85.00126953124999,
307 | 9.699267578124989
308 | ],
309 | [
310 | -85.059716796875,
311 | 9.668310546874991
312 | ],
313 | [
314 | -85.07705078125,
315 | 9.601953125
316 | ],
317 | [
318 | -85.114501953125,
319 | 9.581787109375
320 | ],
321 | [
322 | -85.15400390625,
323 | 9.620068359374997
324 | ],
325 | [
326 | -85.31455078124999,
327 | 9.8109375
328 | ],
329 | [
330 | -85.62485351562499,
331 | 9.902441406249991
332 | ],
333 | [
334 | -85.68100585937499,
335 | 9.95859375
336 | ],
337 | [
338 | -85.796484375,
339 | 10.132861328124989
340 | ],
341 | [
342 | -85.84965820312499,
343 | 10.292041015624989
344 | ],
345 | [
346 | -85.83061523437499,
347 | 10.398144531249997
348 | ],
349 | [
350 | -85.703125,
351 | 10.5634765625
352 | ],
353 | [
354 | -85.663330078125,
355 | 10.635449218749997
356 | ],
357 | [
358 | -85.67143554687499,
359 | 10.679785156249991
360 | ],
361 | [
362 | -85.667236328125,
363 | 10.745019531249994
364 | ],
365 | [
366 | -85.71484375,
367 | 10.790576171874989
368 | ],
369 | [
370 | -85.83286132812499,
371 | 10.849951171874991
372 | ],
373 | [
374 | -85.90800781249999,
375 | 10.897558593749991
376 | ],
377 | [
378 | -85.88740234375,
379 | 10.921289062499994
380 | ],
381 | [
382 | -85.75224609374999,
383 | 10.985253906249994
384 | ],
385 | [
386 | -85.74370117187499,
387 | 11.04296875
388 | ],
389 | [
390 | -85.7443359375,
391 | 11.062109375
392 | ],
393 | [
394 | -85.722265625,
395 | 11.066259765624991
396 | ],
397 | [
398 | -85.70263671875,
399 | 11.08154296875
400 | ],
401 | [
402 | -85.69052734374999,
403 | 11.097460937499989
404 | ],
405 | [
406 | -85.65366210937499,
407 | 11.153076171875
408 | ],
409 | [
410 | -85.62138671874999,
411 | 11.184472656249994
412 | ],
413 | [
414 | -85.5841796875,
415 | 11.189453125
416 | ],
417 | [
418 | -85.538720703125,
419 | 11.166308593749989
420 | ],
421 | [
422 | -85.368359375,
423 | 11.1064453125
424 | ],
425 | [
426 | -85.178955078125,
427 | 11.039941406249994
428 | ],
429 | [
430 | -84.9091796875,
431 | 10.9453125
432 | ],
433 | [
434 | -84.79736328125,
435 | 11.005908203124989
436 | ],
437 | [
438 | -84.701171875,
439 | 11.052197265624997
440 | ],
441 | [
442 | -84.6341796875,
443 | 11.045605468749997
444 | ],
445 | [
446 | -84.48916015625,
447 | 10.991650390624997
448 | ],
449 | [
450 | -84.40185546875,
451 | 10.974462890624991
452 | ],
453 | [
454 | -84.348291015625,
455 | 10.979882812499994
456 | ],
457 | [
458 | -84.25556640625,
459 | 10.900732421874991
460 | ],
461 | [
462 | -84.20498046875,
463 | 10.84130859375
464 | ],
465 | [
466 | -84.19658203124999,
467 | 10.801708984374997
468 | ],
469 | [
470 | -84.168359375,
471 | 10.780371093749991
472 | ],
473 | [
474 | -84.09619140625,
475 | 10.775683593749989
476 | ],
477 | [
478 | -83.91928710937499,
479 | 10.7353515625
480 | ],
481 | [
482 | -83.811181640625,
483 | 10.743261718749991
484 | ],
485 | [
486 | -83.71293945312499,
487 | 10.785888671875
488 | ],
489 | [
490 | -83.658935546875,
491 | 10.836865234374997
492 | ],
493 | [
494 | -83.6419921875,
495 | 10.917236328125
496 | ],
497 | [
498 | -83.61728515624999,
499 | 10.877490234374989
500 | ],
501 | [
502 | -83.58818359374999,
503 | 10.814990234374989
504 | ],
505 | [
506 | -83.57529296874999,
507 | 10.734716796874991
508 | ],
509 | [
510 | -83.4482421875,
511 | 10.465917968749991
512 | ],
513 | [
514 | -83.346826171875,
515 | 10.315380859374997
516 | ],
517 | [
518 | -83.124609375,
519 | 10.041601562499991
520 | ],
521 | [
522 | -83.028515625,
523 | 9.991259765624989
524 | ],
525 | [
526 | -82.86630859374999,
527 | 9.770947265624997
528 | ],
529 | [
530 | -82.810302734375,
531 | 9.734570312499997
532 | ],
533 | [
534 | -82.77841796874999,
535 | 9.66953125
536 | ],
537 | [
538 | -82.61015624999999,
539 | 9.616015624999989
540 | ],
541 | [
542 | -82.56357421874999,
543 | 9.57666015625
544 | ]
545 | ]
546 | ]
547 | }
548 | }
549 | ]
550 | }
--------------------------------------------------------------------------------
/src/modules/misc-views/.doc.md:
--------------------------------------------------------------------------------
1 | Views/screens not directly related to the core gameplay, such as settings and user statistics.
--------------------------------------------------------------------------------
/src/modules/misc-views/settings-view/SettingsView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Settings
5 |
9 | Reset to Defaults
10 |
11 |
12 |
13 |
14 |
15 |
32 |
33 |
34 |
51 |
52 |
53 |
70 |
71 |
72 |
73 |
74 | Border Thickness (px)
75 |
76 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/src/modules/misc-views/settings-view/defaultSettings.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_MAP_SETTINGS = {
2 | waterColor: '#dcefff',
3 | landColor: '#e6e6e6',
4 | borderColor: '#333333',
5 | borderThickness: 1
6 | } as const
7 |
8 | export type MapSettings = {
9 | waterColor: string
10 | landColor: string
11 | borderColor: string
12 | borderThickness: number
13 | }
14 |
15 | export function getMapSettings(): MapSettings {
16 | return {
17 | waterColor: localStorage.getItem('waterColor') || DEFAULT_MAP_SETTINGS.waterColor,
18 | landColor: localStorage.getItem('landColor') || DEFAULT_MAP_SETTINGS.landColor,
19 | borderColor: localStorage.getItem('borderColor') || DEFAULT_MAP_SETTINGS.borderColor,
20 | borderThickness: Number(localStorage.getItem('borderThickness')) || DEFAULT_MAP_SETTINGS.borderThickness
21 | }
22 | }
--------------------------------------------------------------------------------
/src/modules/misc-views/stats-view/.doc.md:
--------------------------------------------------------------------------------
1 | This is everything you see in the `Stats` tab.
--------------------------------------------------------------------------------
/src/modules/misc-views/stats-view/main-stats/StatsView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Learning Progress
4 |
5 |
6 |
7 |
8 |
9 | Country
10 | Due Date
11 | Stability
12 | Difficulty
13 | Level
14 |
15 |
16 |
17 |
23 | {{ card.countryName }}
24 | {{ formatDate(card.due) }}
25 | {{ card.stability.toFixed(2) }}
26 | {{ card.difficulty.toFixed(2) }}
27 | {{ card.level || 0 }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/modules/misc-views/stats-view/per-country-stats/CountryStatsView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 | ← Back to Stats
9 |
10 |
Learning Progress for {{ country }}
11 |
12 |
13 |
14 |
18 |
19 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/modules/misc-views/stats-view/per-country-stats/DistanceProgressChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Distance from Country Center
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/modules/misc-views/stats-view/per-country-stats/TimeProgressChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Time to Find Country
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/modules/play/.doc.md:
--------------------------------------------------------------------------------
1 | Everything directly related to gameplay.
--------------------------------------------------------------------------------
/src/modules/play/map-renderer/.doc.md:
--------------------------------------------------------------------------------
1 | This submodule is responsible to actually render the map the user sees, including the cursor, highlighted countries, and so on.
--------------------------------------------------------------------------------
/src/modules/play/map-renderer/WorldMap.vue:
--------------------------------------------------------------------------------
1 |
370 |
371 |
372 |
377 |
378 |
379 |
380 |
381 |
421 |
422 |
--------------------------------------------------------------------------------
/src/modules/play/map-renderer/WorldMapGame.vue:
--------------------------------------------------------------------------------
1 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
183 |
184 |
185 |
--------------------------------------------------------------------------------
/src/modules/play/map-renderer/useCustomCursor.ts:
--------------------------------------------------------------------------------
1 | import { ref, onMounted, onUnmounted } from 'vue'
2 |
3 | interface PointerPosition {
4 | clientX: number
5 | clientY: number
6 | }
7 |
8 | interface TouchEventWithTouches extends Event {
9 | touches: Touch[]
10 | preventDefault: () => void
11 | }
12 |
13 | interface CursorState {
14 | element: HTMLElement | null
15 | isTouchDevice: boolean
16 | isVisible: boolean
17 | isDragging: boolean
18 | }
19 |
20 | // Pure functions for calculations
21 | const calculateDistance = (x1: number, y1: number, x2: number, y2: number): number => {
22 | const dx = x1 - x2
23 | const dy = y1 - y2
24 | return Math.sqrt(dx * dx + dy * dy)
25 | }
26 |
27 | const getClosestPointOnRectangle = (
28 | rect: DOMRect,
29 | pointX: number,
30 | pointY: number
31 | ): { x: number; y: number } => ({
32 | x: Math.max(rect.left, Math.min(pointX, rect.right)),
33 | y: Math.max(rect.top, Math.min(pointY, rect.bottom))
34 | })
35 |
36 | const isPointInCircle = (
37 | pointX: number,
38 | pointY: number,
39 | centerX: number,
40 | centerY: number,
41 | radius: number
42 | ): boolean => {
43 | const distance = calculateDistance(pointX, pointY, centerX, centerY)
44 | return distance <= radius
45 | }
46 |
47 | const getElementCenter = (element: HTMLElement): { x: number; y: number } => {
48 | const rect = element.getBoundingClientRect()
49 | return {
50 | x: rect.left + rect.width / 2,
51 | y: rect.top + rect.height / 2
52 | }
53 | }
54 |
55 | // DOM manipulation functions
56 | const createCursorElement = (size: number): HTMLElement => {
57 | const cursor = document.createElement('div')
58 | cursor.className = 'custom-cursor'
59 | document.body.appendChild(cursor)
60 | return cursor
61 | }
62 |
63 | const createCursorStyles = (size: number): string => `
64 | body.hovering-map { cursor: none; }
65 | .custom-cursor {
66 | width: ${size}px;
67 | height: ${size}px;
68 | background: rgba(255, 107, 107, 0.2);
69 | border: 2px solid #ff6b6b;
70 | border-radius: 50%;
71 | position: fixed;
72 | pointer-events: none;
73 | z-index: 9999;
74 | transform: translate(-50%, -50%);
75 | opacity: 0;
76 | transition: opacity 0.2s ease;
77 | }
78 | body.hovering-map .custom-cursor {
79 | opacity: 1;
80 | }
81 | @media (hover: none) {
82 | body.hovering-map {
83 | cursor: auto;
84 | }
85 | .custom-cursor {
86 | display: block !important;
87 | opacity: 1 !important;
88 | pointer-events: auto !important;
89 | }
90 | }
91 | `
92 |
93 | const applyCursorStyles = (size: number): void => {
94 | const style = document.createElement('style')
95 | style.textContent = createCursorStyles(size)
96 | document.head.appendChild(style)
97 | }
98 |
99 | // Touch handling functions
100 | const getTouchFromEvent = (e: Event): Touch | null => {
101 | const touchEvent = e as TouchEventWithTouches
102 | return ('touches' in touchEvent && touchEvent.touches.length > 0) ? touchEvent.touches[0] : null
103 | }
104 |
105 | const isTouchOnCursor = (
106 | touch: Touch,
107 | cursor: HTMLElement,
108 | size: number
109 | ): boolean => {
110 | const rect = cursor.getBoundingClientRect()
111 | const cursorCenterX = rect.left + rect.width / 2
112 | const cursorCenterY = rect.top + rect.height / 2
113 |
114 | return isPointInCircle(
115 | touch.clientX,
116 | touch.clientY,
117 | cursorCenterX,
118 | cursorCenterY,
119 | size / 2
120 | )
121 | }
122 |
123 | const findTouchedCountries = (
124 | container: HTMLElement | null,
125 | cursorX: number,
126 | cursorY: number,
127 | size: number,
128 | detectionRadiusMultiplier: number = 1
129 | ): string[] => {
130 | if (!container) return []
131 |
132 | const touchedCountries: string[] = []
133 | const countryElements = container.querySelectorAll('path')
134 | const cursorRadius = (size / 2) * detectionRadiusMultiplier
135 |
136 | countryElements.forEach(element => {
137 | const rect = element.getBoundingClientRect()
138 | const { x: closestX, y: closestY } = getClosestPointOnRectangle(rect, cursorX, cursorY)
139 | const distance = calculateDistance(cursorX, cursorY, closestX, closestY)
140 |
141 | if (distance <= cursorRadius) {
142 | const countryName = element.getAttribute('data-country')
143 | if (countryName) touchedCountries.push(countryName)
144 | }
145 | })
146 |
147 | return touchedCountries
148 | }
149 |
150 | const dispatchMapClickEvent = (
151 | container: HTMLElement | null,
152 | cursorX: number,
153 | cursorY: number
154 | ): void => {
155 | if (!container) return
156 |
157 | const clickEvent = new MouseEvent('click', {
158 | bubbles: true,
159 | cancelable: true,
160 | clientX: cursorX,
161 | clientY: cursorY
162 | })
163 | container.dispatchEvent(clickEvent)
164 | }
165 |
166 | export function useCustomCursor(
167 | size: number = 76,
168 | emit?: (event: 'mapClicked', touchedCountries: string[], distanceToTarget?: number) => void
169 | ) {
170 | const state = ref({
171 | element: null,
172 | isTouchDevice: false,
173 | isVisible: false,
174 | isDragging: false
175 | })
176 | const containerRef = ref(null)
177 |
178 | const updateCursorVisibility = (isVisible: boolean): void => {
179 | document.body.classList.toggle('hovering-map', isVisible)
180 | }
181 |
182 | const updateCursorPosition = (cursor: HTMLElement, position: PointerPosition): void => {
183 | if (!state.value.element || !containerRef.value) return
184 |
185 | const rect = containerRef.value.getBoundingClientRect()
186 | const cursorRadius = size / 2
187 |
188 | // Constrain the cursor position within the map boundaries
189 | const constrainedX = Math.max(rect.left + cursorRadius, Math.min(position.clientX, rect.right - cursorRadius))
190 | const constrainedY = Math.max(rect.top + cursorRadius, Math.min(position.clientY, rect.bottom - cursorRadius))
191 |
192 | cursor.style.left = `${constrainedX}px`
193 | cursor.style.top = `${constrainedY}px`
194 | }
195 |
196 | const initializeCursorPosition = () => {
197 | if (!state.value.element || !containerRef.value) return
198 |
199 | const rect = containerRef.value.getBoundingClientRect()
200 | const centerX = rect.left + rect.width / 2
201 | const centerY = rect.top + rect.height / 2
202 |
203 | updateCursorPosition(state.value.element, {
204 | clientX: centerX,
205 | clientY: centerY
206 | })
207 | }
208 |
209 | const handleMouseMove = (e: MouseEvent) => {
210 | if (state.value.isVisible && state.value.element) {
211 | updateCursorPosition(state.value.element, {
212 | clientX: e.clientX,
213 | clientY: e.clientY
214 | })
215 | }
216 | }
217 |
218 | const handleContainerEnter = () => {
219 | if (!state.value.isTouchDevice) {
220 | state.value.isVisible = true
221 | updateCursorVisibility(true)
222 | }
223 | }
224 |
225 | const handleContainerLeave = () => {
226 | if (!state.value.isTouchDevice) {
227 | state.value.isVisible = false
228 | updateCursorVisibility(false)
229 | }
230 | }
231 |
232 | const handleTouchStart = (e: Event) => {
233 | const touch = getTouchFromEvent(e)
234 | if (!touch || !state.value.element || !containerRef.value) return
235 |
236 | const rect = containerRef.value.getBoundingClientRect()
237 | const cursorRadius = size / 2
238 |
239 | // Only handle touch events that start within the map boundaries
240 | if (touch.clientX >= rect.left && touch.clientX <= rect.right &&
241 | touch.clientY >= rect.top && touch.clientY <= rect.bottom) {
242 | state.value.isDragging = true
243 | state.value.isVisible = true
244 | updateCursorVisibility(true)
245 | updateCursorPosition(state.value.element, {
246 | clientX: touch.clientX,
247 | clientY: touch.clientY
248 | })
249 | ;(e as TouchEventWithTouches).preventDefault()
250 | }
251 | }
252 |
253 | const handleTouchMove = (e: Event) => {
254 | if (!state.value.isDragging || !state.value.element) return
255 |
256 | const touch = getTouchFromEvent(e)
257 | if (!touch) return
258 |
259 | updateCursorPosition(state.value.element, {
260 | clientX: touch.clientX,
261 | clientY: touch.clientY
262 | })
263 | }
264 |
265 | const handleTouchEnd = (e: Event) => {
266 | if (!state.value.isDragging || !state.value.element) return
267 |
268 | const { x: cursorX, y: cursorY } = getElementCenter(state.value.element)
269 |
270 | // Only check for correctness when the user finishes dragging
271 | const touchedCountries = findTouchedCountries(
272 | containerRef.value,
273 | cursorX,
274 | cursorY,
275 | size
276 | )
277 |
278 | if (touchedCountries.length > 0) {
279 | if (emit) {
280 | // Directly emit the event instead of dispatching a click
281 | emit('mapClicked', touchedCountries)
282 | } else {
283 | // Fallback to click event for backward compatibility
284 | dispatchMapClickEvent(containerRef.value, cursorX, cursorY)
285 | }
286 | }
287 |
288 | state.value.isDragging = false
289 | state.value.isVisible = false
290 | updateCursorVisibility(false)
291 | }
292 |
293 | onMounted(() => {
294 | if (!containerRef.value) return
295 |
296 | // Detect touch device once on mount
297 | state.value.isTouchDevice = 'ontouchstart' in window ||
298 | navigator.maxTouchPoints > 0 ||
299 | (navigator as any).msMaxTouchPoints > 0
300 |
301 | state.value.element = createCursorElement(size)
302 | applyCursorStyles(size)
303 |
304 | // Initialize cursor position
305 | initializeCursorPosition()
306 |
307 | if (!state.value.isTouchDevice) {
308 | // Mouse device event listeners
309 | document.addEventListener('mousemove', handleMouseMove)
310 | containerRef.value.addEventListener('mouseenter', handleContainerEnter)
311 | containerRef.value.addEventListener('mouseleave', handleContainerLeave)
312 | } else {
313 | // Touch device event listeners - only for drag and drop
314 | document.addEventListener('touchstart', handleTouchStart, { passive: false })
315 | document.addEventListener('touchmove', handleTouchMove)
316 | document.addEventListener('touchend', handleTouchEnd)
317 | }
318 |
319 | // Add resize handler to reposition cursor if needed
320 | window.addEventListener('resize', initializeCursorPosition)
321 | })
322 |
323 | onUnmounted(() => {
324 | if (!state.value.isTouchDevice && containerRef.value) {
325 | document.removeEventListener('mousemove', handleMouseMove)
326 | containerRef.value.removeEventListener('mouseenter', handleContainerEnter)
327 | containerRef.value.removeEventListener('mouseleave', handleContainerLeave)
328 | } else {
329 | document.removeEventListener('touchstart', handleTouchStart)
330 | document.removeEventListener('touchmove', handleTouchMove)
331 | document.removeEventListener('touchend', handleTouchEnd)
332 | }
333 |
334 | window.removeEventListener('resize', initializeCursorPosition)
335 |
336 | if (state.value.element?.parentNode) {
337 | state.value.element.parentNode.removeChild(state.value.element)
338 | }
339 | })
340 |
341 | return {
342 | containerRef,
343 | cursor: state.value.element,
344 | isTouchDevice: state.value.isTouchDevice,
345 | isVisible: state.value.isVisible,
346 | isDragging: state.value.isDragging,
347 | isCursorOverlappingElement: (element: Element, cursorX: number, cursorY: number) =>
348 | findTouchedCountries(containerRef.value, cursorX, cursorY, size).length > 0,
349 | findTouchedCountries: (container: HTMLElement | null, cursorX: number, cursorY: number) =>
350 | findTouchedCountries(container, cursorX, cursorY, size)
351 | }
352 | }
--------------------------------------------------------------------------------
/src/modules/play/play-modes/challenge-play/.doc.md:
--------------------------------------------------------------------------------
1 | The logic for everything that happens in the `Challenge` tab.
--------------------------------------------------------------------------------
/src/modules/play/play-modes/challenge-play/ChallengeCompletedCard.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
Challenge Already Completed
11 |
12 |
13 |
You've already completed today's challenge! Come back tomorrow for a new set of countries.
14 |
In the meantime, you can practice in the regular play mode to improve your skills.
15 |
16 |
17 |
18 |
22 | Go to Play Mode
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/modules/play/play-modes/challenge-play/ChallengeResults.vue:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
61 |
62 |
Challenge Complete!
63 |
64 |
65 |
66 |
67 |
Total Score
68 |
{{ totalScore }}
69 |
70 |
71 |
Total Time
72 |
{{ formatTime(totalTimeMs) }}
73 |
74 |
75 |
Average Time
76 |
{{ formatTime(averageTimeMs) }}
77 |
78 |
79 |
Success Rate
80 |
81 | {{ Math.round((results.filter(r => r.correct).length / results.length) * 100) }}%
82 |
83 |
84 |
85 |
86 |
87 |
88 |
Results by Country
89 |
90 |
91 |
92 |
93 |
102 |
103 | {{ formatTime(result.timeMs) }}
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
Share Your Results
115 |
155 |
156 |
157 |
158 |
159 |
163 | Back to Play
164 |
165 |
166 |
167 |
168 |
--------------------------------------------------------------------------------
/src/modules/play/play-modes/challenge-play/ChallengeRulesPopup.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
36 |
37 |
Daily Challenge Rules
38 |
39 |
40 |
1. You'll be presented with 10 random countries to locate
41 |
2. Place the red circle on the country as fast as you can
42 |
3. You will get more points the faster you are, but miss and you get 0 points
43 |
4. Challenge can only be completed once per day
44 |
By the way, you don't need to zoom. You only need to be as accurate as the circle is big.
45 |
46 |
47 |
48 |
49 |
{{ countdown }}
50 |
51 |
52 |
53 |
57 | Practice
58 |
59 |
64 | Start Challenge
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/modules/play/play-modes/challenge-play/PlayChallengeView.vue:
--------------------------------------------------------------------------------
1 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
Daily Challenge
78 |
79 |
80 | Score: {{ currentScore }}
81 |
82 |
83 | {{ currentCountryIndex + 1 }}/10
84 |
85 |
86 |
87 |
88 |
89 |
90 |
108 |
109 |
110 |
111 |
118 |
119 |
120 |
121 |
126 |
127 |
128 |
console.log('Shared on:', platform)"
135 | />
136 |
137 |
138 |
141 |
142 |
--------------------------------------------------------------------------------
/src/modules/play/play-modes/challenge-play/types.ts:
--------------------------------------------------------------------------------
1 | export interface DailyChallenge {
2 | date: string;
3 | totalScore: number;
4 | totalTimeMs: number;
5 | averageTimeMs: number;
6 | results: {
7 | country: string;
8 | correct: boolean;
9 | timeMs: number;
10 | zoomLevel: number;
11 | }[];
12 | }
--------------------------------------------------------------------------------
/src/modules/play/play-modes/challenge-play/useDailyChallenge.ts:
--------------------------------------------------------------------------------
1 | import allCountries from '@/modules/map-data/country-lists/all-countries.json'
2 | import { getDailySeed, seededRandomInt } from '@/modules/randomness/random'
3 | import { ref, computed } from 'vue'
4 |
5 | enum ChallengeState {
6 | NOT_STARTED = 'NOT_STARTED',
7 | RULES_SHOWN = 'RULES_SHOWN',
8 | IN_PROGRESS = 'IN_PROGRESS',
9 | COMPLETED = 'COMPLETED'
10 | }
11 |
12 | interface ChallengeResult {
13 | country: string
14 | correct: boolean
15 | timeMs: number
16 | zoomLevel: number
17 | }
18 |
19 | // Calculate score based on time
20 | function calculateScore(timeMs: number): number {
21 | if (timeMs === 0) return 1000
22 | if (timeMs >= 5000) return 50
23 |
24 | // Logarithmic scaling between 50 and 1000 points
25 | const minScore = 50
26 | const maxScore = 1000
27 | const minTime = 0
28 | const maxTime = 5000
29 |
30 | const normalizedTime = (timeMs - minTime) / (maxTime - minTime)
31 | const score = maxScore - (maxScore - minScore) * Math.log10(1 + 9 * normalizedTime)
32 |
33 | return Math.round(score)
34 | }
35 |
36 | export function useDailyChallenge() {
37 | const state = ref(ChallengeState.NOT_STARTED)
38 | const currentCountryIndex = ref(0)
39 | const results = ref([])
40 | const startTime = ref(null)
41 | const currentScore = ref(0)
42 | const totalTimeMs = ref(0)
43 |
44 | // Generate 10 random countries and zoom levels for the day
45 | const dailyChallenge = computed(() => {
46 | const seed = getDailySeed()
47 | const countries = [...allCountries]
48 | const challengeCountries: { country: string; zoomLevel: number }[] = []
49 |
50 | for (let i = 0; i < 10; i++) {
51 | const country = countries[seededRandomInt(seed + i, 0, countries.length)]
52 | // Generate random zoom level between 100 (world view) and 175 (zoomed in)
53 | const zoomLevel = seededRandomInt(seed + i + 1000, 100, 176)
54 | challengeCountries.push({ country, zoomLevel })
55 | }
56 |
57 | return challengeCountries
58 | })
59 |
60 | // Check if challenge has been completed today
61 | const hasCompletedToday = () => {
62 | const today = new Date().toISOString().split('T')[0]
63 | return localStorage.getItem(`challenge_completed_${today}`) === 'true'
64 | }
65 |
66 | // Start the challenge
67 | const startChallenge = () => {
68 | if (hasCompletedToday()) {
69 | throw new Error('Challenge already completed today')
70 | }
71 |
72 | state.value = ChallengeState.IN_PROGRESS
73 | currentCountryIndex.value = 0
74 | results.value = []
75 | startTime.value = Date.now()
76 | currentScore.value = 0
77 | totalTimeMs.value = 0
78 | }
79 |
80 | // Show rules
81 | const showRules = () => {
82 | state.value = ChallengeState.RULES_SHOWN
83 | }
84 |
85 | // Handle country completion
86 | const handleCountryCompletion = (correct: boolean, timeMs: number) => {
87 | if (state.value !== ChallengeState.IN_PROGRESS || !startTime.value) return
88 |
89 | console.log('handleCountryCompletion - Before:', {
90 | currentIndex: currentCountryIndex.value,
91 | resultsLength: results.value.length,
92 | state: state.value
93 | })
94 |
95 | const currentCountry = dailyChallenge.value[currentCountryIndex.value]
96 | const score = correct ? calculateScore(timeMs) : 0
97 |
98 | results.value.push({
99 | country: currentCountry.country,
100 | correct,
101 | timeMs,
102 | zoomLevel: currentCountry.zoomLevel
103 | })
104 |
105 | currentScore.value += score
106 | totalTimeMs.value += timeMs
107 |
108 | console.log('handleCountryCompletion - After push:', {
109 | resultsLength: results.value.length,
110 | lastResult: results.value[results.value.length - 1]
111 | })
112 |
113 | // Move to next country
114 | if (currentCountryIndex.value < 9) {
115 | currentCountryIndex.value++
116 | }
117 |
118 | console.log('handleCountryCompletion - End:', {
119 | currentIndex: currentCountryIndex.value,
120 | resultsLength: results.value.length,
121 | state: state.value
122 | })
123 | }
124 |
125 | return {
126 | state,
127 | currentCountryIndex,
128 | results,
129 | currentScore,
130 | totalTimeMs,
131 | dailyChallenge,
132 | hasCompletedToday,
133 | startChallenge,
134 | showRules,
135 | handleCountryCompletion,
136 | ChallengeState // Export the enum for use in components
137 | }
138 | }
--------------------------------------------------------------------------------
/src/modules/play/play-modes/custom-play/CustomPlay.vue:
--------------------------------------------------------------------------------
1 |
40 |
41 |
42 |
43 |
44 |
45 |
49 |
50 |
51 |
52 | Filter Countries
53 |
54 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
67 |
68 | Please select at least one country
69 |
70 |
71 |
72 |
73 |
74 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/modules/play/play-modes/custom-play/filter-modal/FilterModal.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
25 | ✕
26 |
27 |
28 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
61 |
62 |
--------------------------------------------------------------------------------
/src/modules/play/play-modes/custom-play/filter-modal/tabs/TabCountrySelection.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 | {{ selectedCount }} countries selected
32 |
33 |
34 |
35 |
36 |
41 |
47 | {{ country }}
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/modules/play/play-modes/custom-play/filter-modal/tabs/TabQuickSelection.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 | {{ selectedCount }} countries selected
25 |
26 |
27 |
28 |
29 |
33 | Select All
34 |
35 |
39 | Deselect All
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/modules/play/play-modes/custom-play/filter-modal/tabs/TabWizardSelection.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
Now practicing only African countries
45 |
46 |
47 |
48 |
52 | Practice Only Africa
53 |
54 |
55 | Focus your learning on African countries. All other countries will be deselected.
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/modules/play/play-modes/custom-play/filter-modal/tabs/useCountrySelection.ts:
--------------------------------------------------------------------------------
1 | import { ref, computed, watch } from 'vue'
2 | import allCountries from '@/modules/map-data/country-lists/all-countries.json'
3 |
4 | const STORAGE_KEY = 'customPlayCountries'
5 |
6 | // Initialize from localStorage or default to all countries selected
7 | const initializeCountrySelection = () => {
8 | const stored = localStorage.getItem(STORAGE_KEY)
9 | if (stored) {
10 | return JSON.parse(stored)
11 | }
12 | // Default: all countries selected
13 | const defaultSelection = Object.fromEntries(
14 | allCountries.map(country => [country, true])
15 | )
16 | localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultSelection))
17 | return defaultSelection
18 | }
19 |
20 | const countrySelection = ref>(initializeCountrySelection())
21 |
22 | // Computed property for selected countries
23 | const selectedCountries = computed(() =>
24 | Object.entries(countrySelection.value)
25 | .filter(([_, selected]) => selected)
26 | .map(([country]) => country)
27 | )
28 |
29 | // Computed property for total selected count
30 | const selectedCount = computed(() => selectedCountries.value.length)
31 |
32 | // Toggle country selection
33 | const toggleCountry = (country: string) => {
34 | countrySelection.value[country] = !countrySelection.value[country]
35 | }
36 |
37 | // Check if at least one country is selected
38 | const hasSelectedCountries = computed(() => selectedCount.value > 0)
39 |
40 | // Watch for changes and persist to localStorage
41 | watch(countrySelection, (newValue) => {
42 | localStorage.setItem(STORAGE_KEY, JSON.stringify(newValue))
43 | }, { deep: true })
44 |
45 | export function useCountrySelection() {
46 | return {
47 | countrySelection,
48 | selectedCountries,
49 | selectedCount,
50 | toggleCountry,
51 | hasSelectedCountries
52 | }
53 | }
--------------------------------------------------------------------------------
/src/modules/play/play-modes/standard-play/.doc.md:
--------------------------------------------------------------------------------
1 | The logic for everything that happens in the normal gameplay mode, which is what the user sees when first opening the app.
--------------------------------------------------------------------------------
/src/modules/play/play-modes/standard-play/PlayStandardView.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/src/modules/play/play-modes/standard-play/standard-play-progress-bar/useLearningProgress.ts:
--------------------------------------------------------------------------------
1 | import { useDexie } from '@/modules/spaced-repetition-learning/calculate-learning/useDexie'
2 | import { ref, computed } from 'vue'
3 |
4 | export interface LearningProgress {
5 | notDue: number
6 | due: number
7 | neverLearned: number
8 | total: number
9 | }
10 |
11 | export function useLearningProgress() {
12 | const { getAllCards, getDueCards } = useDexie()
13 | const availableCountries = ref([])
14 | const progress = ref({
15 | notDue: 0,
16 | due: 0,
17 | neverLearned: 0,
18 | total: 0
19 | })
20 |
21 | const setAvailableCountries = (countries: string[]) => {
22 | availableCountries.value = countries
23 | progress.value.total = countries.length
24 | }
25 |
26 | const updateProgress = async () => {
27 | const allCards = await getAllCards()
28 | const dueCards = await getDueCards()
29 |
30 | // Create a set of all countries that have cards
31 | const learnedCountries = new Set(allCards.map(card => card.countryName))
32 |
33 | // Count due cards
34 | const dueCount = dueCards.length
35 |
36 | // Count not due cards (learned but not due)
37 | const notDueCount = allCards.length - dueCount
38 |
39 | // Count never learned countries
40 | const neverLearnedCount = availableCountries.value.length - learnedCountries.size
41 |
42 | progress.value = {
43 | notDue: notDueCount,
44 | due: dueCount,
45 | neverLearned: neverLearnedCount,
46 | total: availableCountries.value.length
47 | }
48 | }
49 |
50 | const progressPercentages = computed(() => ({
51 | notDue: (progress.value.notDue / progress.value.total) * 100,
52 | due: (progress.value.due / progress.value.total) * 100,
53 | neverLearned: (progress.value.neverLearned / progress.value.total) * 100
54 | }))
55 |
56 | // Create a custom event for progress updates
57 | const progressUpdateEvent = new Event('learning-progress-update')
58 |
59 | return {
60 | progress,
61 | progressPercentages,
62 | setAvailableCountries,
63 | updateProgress,
64 | progressUpdateEvent
65 | }
66 | }
--------------------------------------------------------------------------------
/src/modules/randomness/random.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Seeded random number generator
3 | * @param seed - The seed value
4 | * @returns A random number between 0 and 1
5 | */
6 | export function seededRandom(seed: number): number {
7 | const x = Math.sin(seed++) * 10000
8 | return x - Math.floor(x)
9 | }
10 |
11 | /**
12 | * Get the current seed value based on environment
13 | * @returns A number representing the seed
14 | */
15 | function getSeedValue(): number {
16 | // In test mode, return a consistent seed value
17 | if (import.meta.env.MODE === 'test') {
18 | return 123456789 // Consistent seed for tests
19 | }
20 |
21 | const now = new Date()
22 | const utcDateTime = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')}T${String(now.getUTCHours()).padStart(2, '0')}:${String(now.getUTCMinutes()).padStart(2, '0')}:${String(now.getUTCSeconds()).padStart(2, '0')}.${String(now.getUTCMilliseconds()).padStart(3, '0')}`
23 | return utcDateTime.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
24 | }
25 |
26 | /**
27 | * Generate a daily seed from UTC date
28 | * @returns A number representing the seed for the current UTC date
29 | */
30 | export function getDailySeed(): number {
31 | // In test mode, return a consistent seed value
32 | if (import.meta.env.MODE === 'test') {
33 | return 987654321 // Consistent daily seed for tests
34 | }
35 |
36 | const now = new Date()
37 | const utcDate = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-${String(now.getUTCDate()).padStart(2, '0')}`
38 | return utcDate.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
39 | }
40 |
41 | /**
42 | * Generate a seed from the current UTC datetime including milliseconds
43 | * @returns A number representing the seed for the current UTC datetime
44 | */
45 | export function getCurrentSeed(): number {
46 | return getSeedValue()
47 | }
48 |
49 | /**
50 | * Get a random integer between min (inclusive) and max (exclusive) using a seed
51 | * @param seed - The seed value
52 | * @param min - The minimum value (inclusive)
53 | * @param max - The maximum value (exclusive)
54 | * @returns A random integer between min and max
55 | */
56 | export function seededRandomInt(seed: number, min: number, max: number): number {
57 | return Math.floor(seededRandom(seed) * (max - min)) + min
58 | }
59 |
60 | /**
61 | * Get a random element from an array using a seed
62 | * @param seed - The seed value
63 | * @param array - The array to select from
64 | * @returns A random element from the array
65 | */
66 | export function seededRandomElement(seed: number, array: T[]): T {
67 | const index = seededRandomInt(seed, 0, array.length)
68 | return array[index]
69 | }
--------------------------------------------------------------------------------
/src/modules/shared-types/.doc.md:
--------------------------------------------------------------------------------
1 | Typescript types/interfaces needed across the whole app, or at least multiple modules.
--------------------------------------------------------------------------------
/src/modules/shared-types/types.ts:
--------------------------------------------------------------------------------
1 | import type { Card } from "ts-fsrs";
2 |
3 | export interface LearningEvent {
4 | id?: number; // Auto-incremented primary key
5 | deviceId: string;
6 | timestamp: Date;
7 | country: string;
8 | msFromExerciseToFirstClick: number;
9 | msFromExerciseToFinishClick: number;
10 | numberOfClicksNeeded: number;
11 | distanceOfFirstClickToCenterOfCountry: number;
12 | }
13 |
14 | export interface CountryCard extends Card {
15 | countryName: string;
16 | winStreak: number;
17 | failStreak: number;
18 | level: number;
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/modules/spaced-repetition-learning/.doc.md:
--------------------------------------------------------------------------------
1 | Responsible for handling learning logic (currently mostly using `ts-fsrs`) as well as saving that learning data to a local database (`dexie`).
--------------------------------------------------------------------------------
/src/modules/spaced-repetition-learning/calculate-learning/types.ts:
--------------------------------------------------------------------------------
1 | import type { Card } from 'ts-fsrs';
2 |
3 | export interface Flashcard {
4 | id?: number;
5 | front: string;
6 | back: string;
7 | card: Card;
8 | lastReviewDate?: Date;
9 | }
10 |
--------------------------------------------------------------------------------
/src/modules/spaced-repetition-learning/calculate-learning/useDexie.ts:
--------------------------------------------------------------------------------
1 | import Dexie, { type Table } from 'dexie'
2 | import { v4 as uuidv4 } from 'uuid'
3 | import { logLearningEventToFirebase } from '../log-learning/firebase'
4 | import type { CountryCard, LearningEvent } from '@/modules/shared-types/types';
5 |
6 |
7 | interface DeviceInfo {
8 | id: string;
9 | deviceId: string;
10 | }
11 |
12 | export class GeographyDatabase extends Dexie {
13 | countryCards!: Table;
14 | learningEvents!: Table;
15 | deviceInfo!: Table;
16 |
17 | constructor() {
18 | super('GeographyDatabase');
19 | this.version(3).stores({
20 | countryCards: 'countryName, due, stability, difficulty, elapsed_days, scheduled_days, reps, lapses, state, last_review, winStreak, failStreak, level',
21 | learningEvents: '++id, deviceId, timestamp, country',
22 | dailyChallenges: 'date',
23 | deviceInfo: 'id'
24 | });
25 | }
26 | }
27 |
28 | const db = new GeographyDatabase();
29 |
30 | // Initialize or get device ID
31 | async function getOrCreateDeviceId(): Promise {
32 | const deviceInfoKey = 'device';
33 | let deviceInfo = await db.deviceInfo.get(deviceInfoKey);
34 |
35 | if (!deviceInfo) {
36 | deviceInfo = {
37 | id: deviceInfoKey,
38 | deviceId: uuidv4()
39 | };
40 | await db.deviceInfo.put(deviceInfo);
41 | }
42 |
43 | return deviceInfo.deviceId;
44 | }
45 |
46 | export function useDexie() {
47 | const getCard = async (countryName: string): Promise => {
48 | return await db.countryCards.get(countryName);
49 | }
50 |
51 | const saveCard = async (card: CountryCard): Promise => {
52 | await db.countryCards.put(card);
53 | }
54 |
55 | const deleteCard = async (countryName: string): Promise => {
56 | await db.countryCards.delete(countryName);
57 | }
58 |
59 | const getAllCards = async (): Promise => {
60 | return await db.countryCards.toArray();
61 | }
62 |
63 | const getDueCards = async (): Promise => {
64 | const now = new Date();
65 | console.log('Checking for due cards at:', now.toISOString());
66 | return await db.countryCards
67 | .where('due')
68 | .below(now)
69 | .toArray();
70 | }
71 |
72 | const saveLearningEvent = async (event: Omit): Promise => {
73 | const deviceId = await getOrCreateDeviceId();
74 | const fullEvent = {
75 | ...event,
76 | deviceId
77 | };
78 |
79 | // Save to local Dexie database
80 | await db.learningEvents.add(fullEvent);
81 |
82 | // Log to Firebase (don't await - fire and forget)
83 | logLearningEventToFirebase(fullEvent).catch(error => {
84 | console.error('Failed to log learning event to Firebase:', error);
85 | });
86 | }
87 |
88 | const getLearningEventsForCountry = async (country: string): Promise => {
89 | return await db.learningEvents
90 | .where('country')
91 | .equals(country)
92 | .toArray();
93 | }
94 |
95 | const resetDatabase = async (): Promise => {
96 | await db.delete();
97 | await db.open();
98 | }
99 |
100 | return {
101 | getCard,
102 | saveCard,
103 | deleteCard,
104 | getAllCards,
105 | getDueCards,
106 | saveLearningEvent,
107 | getLearningEventsForCountry,
108 | resetDatabase
109 | }
110 | }
--------------------------------------------------------------------------------
/src/modules/spaced-repetition-learning/calculate-learning/useGeographyLearning.ts:
--------------------------------------------------------------------------------
1 | import { ref, computed } from 'vue'
2 | import type { ComputedRef } from 'vue'
3 | import { createEmptyCard, fsrs, Rating } from 'ts-fsrs'
4 | import { useDexie } from './useDexie'
5 | import { getCurrentSeed, seededRandomElement } from '@/modules/randomness/random'
6 | import type { CountryCard } from '@/modules/shared-types/types'
7 |
8 | export interface GeographyLearning {
9 | targetCountryToClick: ComputedRef
10 | targetCountryIsHighlighted: ComputedRef
11 | message: ComputedRef
12 | setAvailableCountries: (countries: string[]) => void
13 | selectRandomCountry: () => Promise
14 | handleCountryClick: (country: string) => Promise
15 | handleGameCompletion: (country: string, attempts: number) => Promise
16 | }
17 |
18 | export function useGeographyLearning(): GeographyLearning {
19 | const { getCard, saveCard, getDueCards, getAllCards, deleteCard } = useDexie()
20 | const targetCountryToClick = ref(null)
21 | const message = ref('')
22 | const targetCountryIsHighlighted = ref(false)
23 | const isFirstTry = ref(true)
24 | const availableCountries = ref([])
25 | const lastPlayedCountry = ref(null)
26 |
27 | const setAvailableCountries = async (countries: string[]) => {
28 | availableCountries.value = countries
29 | await cleanupInvalidCountries()
30 | }
31 |
32 | // Function to clean up invalid countries from the database
33 | const cleanupInvalidCountries = async () => {
34 | const allCards = await getAllCards()
35 | const validCountries = new Set(availableCountries.value)
36 |
37 | for (const card of allCards) {
38 | if (!validCountries.has(card.countryName)) {
39 | console.log(`Removing invalid country from database: ${card.countryName}`)
40 | await deleteCard(card.countryName)
41 | }
42 | }
43 | }
44 |
45 | const selectRandomCountry = async () => {
46 | // Reset state for new country
47 | targetCountryIsHighlighted.value = false
48 | isFirstTry.value = true
49 |
50 | // First check for due cards
51 | const dueCards = await getDueCards()
52 |
53 | // Filter out invalid countries and the last played country from due cards
54 | const validDueCards = dueCards.filter(card =>
55 | availableCountries.value.includes(card.countryName) &&
56 | card.countryName !== lastPlayedCountry.value
57 | )
58 |
59 | if (validDueCards.length > 0) {
60 | // Select from available due cards using seeded random
61 | const seed = getCurrentSeed()
62 | const randomDueCard = seededRandomElement(seed, validDueCards)
63 | targetCountryToClick.value = randomDueCard.countryName
64 | lastPlayedCountry.value = randomDueCard.countryName
65 | return
66 | }
67 |
68 | // If no available due cards, get all cards to find unseen countries
69 | const allCards = await getAllCards()
70 | const seenCountries = new Set(allCards.map(card => card.countryName))
71 | const unseenCountries = availableCountries.value.filter(country =>
72 | !seenCountries.has(country) && country !== lastPlayedCountry.value
73 | )
74 |
75 | if (unseenCountries.length > 0) {
76 | // Select from unseen countries using seeded random
77 | const seed = getCurrentSeed()
78 | const selectedCountry = seededRandomElement(seed, unseenCountries)
79 |
80 | // Create and save empty card for the new country
81 | const emptyCard = createEmptyCard()
82 | const newCard = {
83 | ...emptyCard,
84 | countryName: selectedCountry,
85 | winStreak: 0,
86 | failStreak: 0,
87 | level: 0
88 | } as CountryCard
89 | await saveCard(newCard)
90 |
91 | targetCountryToClick.value = selectedCountry
92 | lastPlayedCountry.value = selectedCountry
93 | message.value = `Click ${targetCountryToClick.value}`
94 | return
95 | }
96 |
97 | // If all countries have been seen, pick a random one that's not the last played
98 | const availableForRandom = availableCountries.value.filter(country =>
99 | country !== lastPlayedCountry.value
100 | )
101 |
102 | const seed = getCurrentSeed()
103 | targetCountryToClick.value = seededRandomElement(seed, availableForRandom)
104 | lastPlayedCountry.value = targetCountryToClick.value
105 | message.value = `Click ${targetCountryToClick.value}`
106 | }
107 |
108 | const updateCardStreaks = (card: CountryCard, isCorrect: boolean, isFirstTry: boolean): void => {
109 | if (!card.winStreak) card.winStreak = 0
110 | if (!card.failStreak) card.failStreak = 0
111 | if (!card.level) card.level = 0
112 |
113 | if (isCorrect) {
114 | if (isFirstTry) {
115 | card.winStreak += 1
116 | card.failStreak = 0
117 |
118 | // Level up logic with three tiers of difficulty
119 | const requiredWinStreak =
120 | card.level <= 2 ? 1 : // Levels 0-2: need 1 win
121 | card.level <= 5 ? 2 : // Levels 3-5: need 2 wins
122 | 3 // Levels 6+: need 3 wins
123 |
124 | if (card.winStreak === requiredWinStreak) {
125 | card.level += 1
126 | card.winStreak = 0
127 | }
128 | } else {
129 | card.winStreak = 0
130 | card.failStreak += 1
131 | }
132 | } else {
133 | card.winStreak = 0
134 | card.failStreak += 1
135 |
136 | // Level down when failStreak hits 3
137 | if (card.failStreak === 3) {
138 | card.level -= 1
139 | card.failStreak = 0
140 | }
141 | }
142 | }
143 |
144 | const handleGameCompletion = async (country: string, attempts: number) => {
145 | let rating: Rating
146 | switch (attempts) {
147 | case 1:
148 | rating = Rating.Good
149 | break
150 | case 2:
151 | rating = Rating.Hard
152 | break
153 | default:
154 | rating = Rating.Again
155 | }
156 |
157 | let card = await getCard(country)
158 | if (!card) {
159 | console.error('Card not found for country:', country)
160 | return
161 | }
162 |
163 | // Track if this will be a level up before we modify the card
164 | const oldWinStreak = card.winStreak || 0
165 | const oldLevel = card.level || 0
166 | const requiredWinStreak =
167 | oldLevel <= 2 ? 1 : // Levels 0-2: need 1 win
168 | oldLevel <= 5 ? 2 : // Levels 3-5: need 2 wins
169 | 3 // Levels 6+: need 3 wins
170 | const willLevelUp = attempts === 1 && oldWinStreak + 1 === requiredWinStreak
171 |
172 | // First update our streaks and level
173 | if (attempts === 1) {
174 | card.winStreak = oldWinStreak + 1
175 | card.failStreak = 0
176 |
177 | if (willLevelUp) {
178 | card.level = oldLevel + 1
179 | card.winStreak = 0
180 | }
181 | } else {
182 | card.winStreak = 0
183 | card.failStreak = (card.failStreak || 0) + 1
184 |
185 | // Level down when failStreak hits 3
186 | if (card.failStreak === 3) {
187 | card.level = (card.level || 0) - 1
188 | card.failStreak = 0
189 | }
190 | }
191 |
192 | // Then apply FSRS with our modified card
193 | const f = fsrs()
194 | const result = f.next(card, new Date(), rating)
195 |
196 | // Save with our modifications taking precedence for everything EXCEPT due date
197 | const finalCard = {
198 | ...result.card, // FSRS base updates including due date
199 | countryName: country, // Our overrides
200 | winStreak: card.winStreak,
201 | failStreak: card.failStreak,
202 | level: card.level,
203 | // Only override due date for level ups
204 | ...(willLevelUp && { due: new Date(new Date().getTime() + 10 * 1000) })
205 | }
206 | await saveCard(finalCard)
207 |
208 | // Dispatch progress update event
209 | window.dispatchEvent(new Event('learning-progress-update'))
210 |
211 | // Select next country after a delay
212 | setTimeout(selectRandomCountry, 2000)
213 | }
214 |
215 | const handleCountryClick = async (clickedCountry: string) => {
216 | if (!targetCountryToClick.value) return
217 |
218 | if (clickedCountry === targetCountryToClick.value) {
219 | // Correct click
220 | let rating: Rating
221 | if (isFirstTry.value) {
222 | rating = Rating.Good
223 | message.value = `Correct! That's ${clickedCountry}!`
224 | } else {
225 | rating = Rating.Hard
226 | message.value = `Good job finding ${clickedCountry} on the second try!`
227 | }
228 |
229 | // Get card (it should exist now)
230 | let card = await getCard(clickedCountry)
231 | if (!card) {
232 | console.error('Card not found for country:', clickedCountry)
233 | return
234 | }
235 |
236 | // Track if this will be a level up before we modify the card
237 | const oldWinStreak = card.winStreak || 0
238 | const oldLevel = card.level || 0
239 | const requiredWinStreak =
240 | oldLevel <= 2 ? 1 : // Levels 0-2: need 1 win
241 | oldLevel <= 5 ? 2 : // Levels 3-5: need 2 wins
242 | 3 // Levels 6+: need 3 wins
243 | const willLevelUp = isFirstTry.value && oldWinStreak + 1 === requiredWinStreak
244 |
245 | // First update our streaks and level
246 | updateCardStreaks(card, true, isFirstTry.value)
247 |
248 | // Then apply FSRS with our modified card
249 | const f = fsrs()
250 | const result = f.next(card, new Date(), rating)
251 |
252 | // Save with our modifications taking precedence for everything EXCEPT due date
253 | const finalCard = {
254 | ...result.card, // FSRS base updates including due date
255 | countryName: clickedCountry, // Our overrides
256 | winStreak: card.winStreak,
257 | failStreak: card.failStreak,
258 | level: card.level,
259 | // Only override due date for level ups
260 | ...(willLevelUp && { due: new Date(new Date().getTime() + 30 * 1000) })
261 | }
262 | await saveCard(finalCard)
263 |
264 | // Dispatch progress update event
265 | window.dispatchEvent(new Event('learning-progress-update'))
266 |
267 | // Select next country after a short delay
268 | setTimeout(selectRandomCountry, 2000)
269 | } else {
270 | // Incorrect click
271 | if (isFirstTry.value) {
272 | isFirstTry.value = false
273 | targetCountryIsHighlighted.value = true
274 | message.value = `Nope, ${targetCountryToClick.value} is highlighted now. Try again!`
275 | } else {
276 | // Failed second attempt
277 | message.value = `That's not quite right. ${targetCountryToClick.value} was highlighted.`
278 |
279 | let card = await getCard(targetCountryToClick.value)
280 | if (!card) {
281 | console.error('Card not found for country:', targetCountryToClick.value)
282 | return
283 | }
284 |
285 | // First update our streaks and level
286 | updateCardStreaks(card, false, false)
287 |
288 | // Then apply FSRS with our modified card
289 | const f = fsrs()
290 | const result = f.next(card, new Date(), Rating.Again)
291 |
292 | // Save with our modifications taking precedence for everything EXCEPT due date
293 | const finalCard = {
294 | ...result.card, // FSRS base updates including due date
295 | countryName: targetCountryToClick.value, // Our overrides
296 | winStreak: card.winStreak,
297 | failStreak: card.failStreak,
298 | level: card.level
299 | }
300 | await saveCard(finalCard)
301 |
302 | // Dispatch progress update event
303 | window.dispatchEvent(new Event('learning-progress-update'))
304 |
305 | // Move to next country after a delay
306 | setTimeout(selectRandomCountry, 2000)
307 | }
308 | }
309 | }
310 |
311 | return {
312 | targetCountryToClick: computed(() => targetCountryToClick.value),
313 | message: computed(() => message.value),
314 | targetCountryIsHighlighted: computed(() => targetCountryIsHighlighted.value),
315 | setAvailableCountries,
316 | selectRandomCountry,
317 | handleCountryClick,
318 | handleGameCompletion
319 | }
320 | }
--------------------------------------------------------------------------------
/src/modules/spaced-repetition-learning/log-learning/firebase.ts:
--------------------------------------------------------------------------------
1 | import type { LearningEvent } from "@/modules/shared-types/types";
2 | import { initializeApp } from "firebase/app";
3 | import { getFirestore, collection, addDoc } from "firebase/firestore";
4 |
5 | const firebaseConfig = {
6 | apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
7 | authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
8 | projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
9 | storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
10 | messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
11 | appId: import.meta.env.VITE_FIREBASE_APP_ID
12 | };
13 |
14 | // Initialize Firebase
15 | const app = initializeApp(firebaseConfig);
16 | const db = getFirestore(app);
17 |
18 | export async function logLearningEventToFirebase(event: LearningEvent): Promise {
19 | try {
20 | const learningEventsRef = collection(db, "learning-data-webgame");
21 | await addDoc(learningEventsRef, {
22 | ...event,
23 | timestamp: event.timestamp.toISOString(), // Convert Date to ISO string for Firestore
24 | });
25 | } catch (error) {
26 | console.error("Error logging learning event to Firebase:", error);
27 | // Just log the error but don't throw - we don't want Firebase errors to affect the app
28 | }
29 | }
--------------------------------------------------------------------------------
/src/router.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from 'vue-router';
2 | import PlayStandardView from './modules/play/play-modes/standard-play/PlayStandardView.vue';
3 | import StatsView from './modules/misc-views/stats-view/main-stats/StatsView.vue';
4 | import CountryStatsView from './modules/misc-views/stats-view/per-country-stats/CountryStatsView.vue';
5 | import PlayChallengeView from './modules/play/play-modes/challenge-play/PlayChallengeView.vue';
6 | import SettingsView from './modules/misc-views/settings-view/SettingsView.vue';
7 | import CustomPlay from './modules/play/play-modes/custom-play/CustomPlay.vue';
8 |
9 | const router = createRouter({
10 | history: createWebHashHistory(),
11 | routes: [
12 | {
13 | path: '/',
14 | redirect: { name: 'play' }
15 | },
16 | {
17 | path: '/play',
18 | name: 'play',
19 | component: PlayStandardView
20 | },
21 | {
22 | path: '/play-custom',
23 | name: 'playCustom',
24 | component: CustomPlay,
25 | props: (route) => ({
26 | initialContinents: route.query.continents?.toString().toLowerCase().split(',') || []
27 | })
28 | },
29 | {
30 | path: '/stats',
31 | name: 'stats',
32 | component: StatsView
33 | },
34 | {
35 | path: '/stats/:country',
36 | name: 'countryStats',
37 | component: CountryStatsView,
38 | props: true
39 | },
40 | {
41 | path: '/challenge',
42 | name: 'challenge',
43 | component: PlayChallengeView
44 | },
45 | {
46 | path: '/settings',
47 | name: 'settings',
48 | component: SettingsView
49 | }
50 | ]
51 | });
52 |
53 | export default router;
--------------------------------------------------------------------------------
/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | declare module '*.vue' {
3 | import type { DefineComponent } from 'vue'
4 | const component: DefineComponent<{}, {}, any>
5 | export default component
6 | }
7 |
8 | declare module 'vue' {
9 | import { CompatVue } from '@vue/runtime-dom'
10 | const Vue: CompatVue
11 | export default Vue
12 | export * from '@vue/runtime-dom'
13 | const { configureCompat } = Vue
14 | export { configureCompat }
15 | }
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @plugin "daisyui";
--------------------------------------------------------------------------------
/src/tests/record_correctly_select_jamaica.spec.ts:
--------------------------------------------------------------------------------
1 |
2 | test('test', async ({ page }) => {
3 | await page.goto('http://localhost:5173/#/play');
4 | await page.locator('path:nth-child(9)').click();
5 | await expect(page.locator('#app')).toContainText('Place the red circle so that it touches Cayman Is.');
6 | });
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tests/hellotest.spec.ts:
--------------------------------------------------------------------------------
1 | // tests/playwright.spec.ts
2 | import { test, expect } from 'playwright/test';
3 |
4 | test('app starts and displays expected content', async ({ page }) => {
5 | // Navigate to your app's URL (adjust the port if needed)
6 | await page.goto('http://localhost:5173');
7 |
8 | // Check that the app loaded by verifying that an element is visible
9 | const header = page.locator('h1');
10 | await expect(header).toBeVisible();
11 | });
12 |
--------------------------------------------------------------------------------
/tests/record_go_to_challenge_mode.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from 'playwright/test';
2 |
3 | test('test', async ({ page }) => {
4 | await page.goto('http://localhost:5173/#/play');
5 | await page.getByRole('link', { name: 'Challenge' }).click();
6 | await expect(page.locator('h2')).toContainText('Daily Challenge Rules');
7 | });
--------------------------------------------------------------------------------
/tests/record_jamaica_to_caiman_flow_works.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from 'playwright/test';
2 |
3 | test('test', async ({ page }) => {
4 | await page.goto('http://localhost:5173/#/play');
5 | await page.locator('path:nth-child(36)').click();
6 | await expect(page.locator('#app')).toContainText('Place the red circle so that it touches Cayman Is.');
7 | });
--------------------------------------------------------------------------------
/tests/record_see_that_jamaica_miss_hint_shows.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from 'playwright/test';
2 |
3 | test('test', async ({ page }) => {
4 | await page.goto('http://localhost:5173/#/play');
5 | await page.getByRole('img').click();
6 | await expect(page.locator('#app')).toContainText('Jamaica is here, try again.');
7 | });
--------------------------------------------------------------------------------
/tests/record_to_challenge_and_back_works.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from 'playwright/test';
2 |
3 | test('test', async ({ page }) => {
4 | await page.goto('http://localhost:5173/#/play');
5 | await page.getByRole('link', { name: 'Challenge' }).click();
6 | await page.getByRole('button', { name: 'Practice' }).click();
7 | await expect(page.locator('#app')).toContainText('Place the red circle so that it touches Jamaica');
8 | });
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@vue/tsconfig/tsconfig.dom.json",
3 | "compilerOptions": {
4 | "target": "ES2022",
5 | "useDefineForClassFields": true,
6 | "module": "ESNext",
7 | "lib": ["ES2023", "DOM", "DOM.Iterable"],
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "preserve",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true,
24 |
25 | /* Path resolution */
26 | "baseUrl": ".",
27 | "paths": {
28 | "@/*": ["./src/*"]
29 | },
30 |
31 | /* Type Declarations */
32 | "types": ["vite/client", "vue"]
33 | },
34 | "include": [
35 | "src/**/*.ts",
36 | "src/**/*.d.ts",
37 | "src/**/*.tsx",
38 | "src/**/*.vue",
39 | "src/shims-vue.d.ts"
40 | ],
41 | "references": [{ "path": "./tsconfig.node.json" }]
42 | }
43 |
--------------------------------------------------------------------------------
/tsconfig.app.tsbuildinfo:
--------------------------------------------------------------------------------
1 | {"root":["./src/env.d.ts","./src/main.ts","./src/shims-vue.d.ts","./src/types.ts","./src/vite-env.d.ts","./src/composables/useCustomCursor.ts","./src/composables/useGeographyLearning.ts","./src/db/database.ts","./src/router/index.ts","./src/services/database.ts"],"version":"5.8.2"}
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/node18/tsconfig.json",
3 | "compilerOptions": {
4 | "composite": true,
5 | "skipLibCheck": true,
6 | "module": "ESNext",
7 | "moduleResolution": "bundler",
8 | "allowSyntheticDefaultImports": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.node.tsbuildinfo:
--------------------------------------------------------------------------------
1 | {"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/compatibility/index.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/filereader.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/dom-events.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/estree/index.d.ts","./node_modules/rollup/dist/rollup.d.ts","./node_modules/rollup/dist/parseAst.d.ts","./node_modules/vite/types/hmrPayload.d.ts","./node_modules/vite/types/customEvent.d.ts","./node_modules/vite/types/hot.d.ts","./node_modules/vite/dist/node/moduleRunnerTransport.d-CXw_Ws6P.d.ts","./node_modules/vite/dist/node/module-runner.d.ts","./node_modules/esbuild/lib/main.d.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/postcss/lib/postcss.d.mts","./node_modules/lightningcss/node/ast.d.ts","./node_modules/lightningcss/node/targets.d.ts","./node_modules/lightningcss/node/index.d.ts","./node_modules/vite/types/internal/lightningcssOptions.d.ts","./node_modules/vite/types/internal/cssPreprocessorOptions.d.ts","./node_modules/vite/types/importGlob.d.ts","./node_modules/vite/types/metadata.d.ts","./node_modules/vite/dist/node/index.d.ts","./node_modules/@babel/types/lib/index.d.ts","./node_modules/@vue/shared/dist/shared.d.ts","./node_modules/@babel/parser/typings/babel-parser.d.ts","./node_modules/@vue/compiler-core/dist/compiler-core.d.ts","./node_modules/magic-string/dist/magic-string.es.d.mts","./node_modules/typescript/lib/typescript.d.ts","./node_modules/@vue/compiler-sfc/dist/compiler-sfc.d.ts","./node_modules/vue/compiler-sfc/index.d.mts","./node_modules/@vitejs/plugin-vue/dist/index.d.mts","./node_modules/@tailwindcss/vite/dist/index.d.mts","./vite.config.ts","./node_modules/@types/aria-query/index.d.ts","./node_modules/@types/d3-array/index.d.ts","./node_modules/@types/d3-selection/index.d.ts","./node_modules/@types/d3-axis/index.d.ts","./node_modules/@types/d3-brush/index.d.ts","./node_modules/@types/d3-chord/index.d.ts","./node_modules/@types/d3-color/index.d.ts","./node_modules/@types/geojson/index.d.ts","./node_modules/@types/d3-contour/index.d.ts","./node_modules/@types/d3-delaunay/index.d.ts","./node_modules/@types/d3-dispatch/index.d.ts","./node_modules/@types/d3-drag/index.d.ts","./node_modules/@types/d3-dsv/index.d.ts","./node_modules/@types/d3-ease/index.d.ts","./node_modules/@types/d3-fetch/index.d.ts","./node_modules/@types/d3-force/index.d.ts","./node_modules/@types/d3-format/index.d.ts","./node_modules/@types/d3-geo/index.d.ts","./node_modules/@types/d3-hierarchy/index.d.ts","./node_modules/@types/d3-interpolate/index.d.ts","./node_modules/@types/d3-path/index.d.ts","./node_modules/@types/d3-polygon/index.d.ts","./node_modules/@types/d3-quadtree/index.d.ts","./node_modules/@types/d3-random/index.d.ts","./node_modules/@types/d3-time/index.d.ts","./node_modules/@types/d3-scale/index.d.ts","./node_modules/@types/d3-scale-chromatic/index.d.ts","./node_modules/@types/d3-shape/index.d.ts","./node_modules/@types/d3-time-format/index.d.ts","./node_modules/@types/d3-timer/index.d.ts","./node_modules/@types/d3-transition/index.d.ts","./node_modules/@types/d3-zoom/index.d.ts","./node_modules/@types/d3/index.d.ts"],"fileIdsList":[[67,109,197],[67,109],[67,109,196],[67,109,210,238],[67,109,209,215],[67,109,220],[67,109,215],[67,109,214],[67,109,232],[67,109,228],[67,109,210,227,238],[67,109,209,210,211,212,213,214,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239],[67,106,109],[67,108,109],[109],[67,109,114,144],[67,109,110,115,121,122,129,141,152],[67,109,110,111,121,129],[62,63,64,67,109],[67,109,112,153],[67,109,113,114,122,130],[67,109,114,141,149],[67,109,115,117,121,129],[67,108,109,116],[67,109,117,118],[67,109,121],[67,109,119,121],[67,108,109,121],[67,109,121,122,123,141,152],[67,109,121,122,123,136,141,144],[67,104,109,157],[67,104,109,117,121,124,129,141,152],[67,109,121,122,124,125,129,141,149,152],[67,109,124,126,141,149,152],[65,66,67,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158],[67,109,121,127],[67,109,128,152],[67,109,117,121,129,141],[67,109,130],[67,109,131],[67,108,109,132],[67,106,107,108,109,110,111,112,113,114,115,116,117,118,119,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158],[67,109,134],[67,109,135],[67,109,121,136,137],[67,109,136,138,153,155],[67,109,121,141,142,144],[67,109,141,143],[67,109,141,142],[67,109,144],[67,109,145],[67,106,109,141],[67,109,121,147,148],[67,109,147,148],[67,109,114,129,141,149],[67,109,150],[67,109,129,151],[67,109,124,135,152],[67,109,114,153],[67,109,141,154],[67,109,128,155],[67,109,156],[67,109,114,121,123,132,141,152,155,157],[67,109,141,158],[67,109,196,204],[67,109,197,198,199],[67,109,188,197,199,200,201,202],[67,109,189,190],[67,109,184],[67,109,182,184],[67,109,173,181,182,183,185],[67,109,171],[67,109,174,179,184,187],[67,109,170,187],[67,109,174,175,178,179,180,187],[67,109,174,175,176,178,179,187],[67,109,171,172,173,174,175,179,180,181,183,184,185,187],[67,109,187],[67,109,169,171,172,173,174,175,176,178,179,180,181,182,183,184,185,186],[67,109,169,187],[67,109,174,176,177,179,180,187],[67,109,178,187],[67,109,179,180,184,187],[67,109,172,182],[67,109,161,195,196],[67,109,160,161],[67,76,80,109,152],[67,76,109,141,152],[67,71,109],[67,73,76,109,149,152],[67,109,129,149],[67,109,159],[67,71,109,159],[67,73,76,109,129,152],[67,68,69,72,75,109,121,141,152],[67,76,83,109],[67,68,74,109],[67,76,97,98,109],[67,72,76,109,144,152,159],[67,97,109,159],[67,70,71,109,159],[67,76,109],[67,70,71,72,73,74,75,76,77,78,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,98,99,100,101,102,103,109],[67,76,91,109],[67,76,83,84,109],[67,74,76,84,85,109],[67,75,109],[67,68,71,76,109],[67,76,80,84,85,109],[67,80,109],[67,74,76,79,109,152],[67,68,73,76,83,109],[67,109,141],[67,71,76,97,109,157,159],[67,109,121,122,124,125,126,129,141,149,152,158,159,161,162,163,164,166,167,168,188,192,193,194,195,196],[67,109,163,164,165,166],[67,109,163],[67,109,164],[67,109,191],[67,109,161,196],[67,109,203],[67,109,196,205,206]],"fileInfos":[{"version":"69684132aeb9b5642cbcd9e22dff7818ff0ee1aa831728af0ecf97d3364d5546","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"936e80ad36a2ee83fc3caf008e7c4c5afe45b3cf3d5c24408f039c1d47bdc1df","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fef8cfad2e2dc5f5b3d97a6f4f2e92848eb1b88e897bb7318cef0e2820bceaab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"b5ce7a470bc3628408429040c4e3a53a27755022a32fd05e2cb694e7015386c7","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"70521b6ab0dcba37539e5303104f29b721bfb2940b2776da4cc818c07e1fefc1","affectsGlobalScope":true,"impliedFormat":1},{"version":"030e350db2525514580ed054f712ffb22d273e6bc7eddc1bb7eda1e0ba5d395e","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"a79e62f1e20467e11a904399b8b18b18c0c6eea6b50c1168bf215356d5bebfaf","affectsGlobalScope":true,"impliedFormat":1},{"version":"8fa51737611c21ba3a5ac02c4e1535741d58bec67c9bdf94b1837a31c97a2263","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"24bd580b5743dc56402c440dc7f9a4f5d592ad7a419f25414d37a7bfe11e342b","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"6bdc71028db658243775263e93a7db2fd2abfce3ca569c3cca5aee6ed5eb186d","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"d2bc987ae352271d0d615a420dcf98cc886aa16b87fb2b569358c1fe0ca0773d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4f0539c58717cbc8b73acb29f9e992ab5ff20adba5f9b57130691c7f9b186a4d","impliedFormat":1},{"version":"7394959e5a741b185456e1ef5d64599c36c60a323207450991e7a42e08911419","impliedFormat":1},{"version":"76103716ba397bbb61f9fa9c9090dca59f39f9047cb1352b2179c5d8e7f4e8d0","impliedFormat":1},{"version":"f9677e434b7a3b14f0a9367f9dfa1227dfe3ee661792d0085523c3191ae6a1a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"4314c7a11517e221f7296b46547dbc4df047115b182f544d072bdccffa57fc72","impliedFormat":1},{"version":"115971d64632ea4742b5b115fb64ed04bcaae2c3c342f13d9ba7e3f9ee39c4e7","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"9057f224b79846e3a95baf6dad2c8103278de2b0c5eebda23fc8188171ad2398","affectsGlobalScope":true,"impliedFormat":1},{"version":"19d5f8d3930e9f99aa2c36258bf95abbe5adf7e889e6181872d1cdba7c9a7dd5","impliedFormat":1},{"version":"e6f5a38687bebe43a4cef426b69d34373ef68be9a6b1538ec0a371e69f309354","impliedFormat":1},{"version":"a6bf63d17324010ca1fbf0389cab83f93389bb0b9a01dc8a346d092f65b3605f","impliedFormat":1},{"version":"e009777bef4b023a999b2e5b9a136ff2cde37dc3f77c744a02840f05b18be8ff","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"ee1ee365d88c4c6c0c0a5a5701d66ebc27ccd0bcfcfaa482c6e2e7fe7b98edf7","affectsGlobalScope":true,"impliedFormat":1},{"version":"88bc59b32d0d5b4e5d9632ac38edea23454057e643684c3c0b94511296f2998c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ff5a53a58e756d2661b73ba60ffe274231a4432d21f7a2d0d9e4f6aa99f4283","impliedFormat":1},{"version":"1e289f30a48126935a5d408a91129a13a59c9b0f8c007a816f9f16ef821e144e","impliedFormat":1},{"version":"2ea254f944dfe131df1264d1fb96e4b1f7d110195b21f1f5dbb68fdd394e5518","impliedFormat":1},{"version":"5135bdd72cc05a8192bd2e92f0914d7fc43ee077d1293dc622a049b7035a0afb","impliedFormat":1},{"version":"4f80de3a11c0d2f1329a72e92c7416b2f7eab14f67e92cac63bb4e8d01c6edc8","impliedFormat":1},{"version":"6d386bc0d7f3afa1d401afc3e00ed6b09205a354a9795196caed937494a713e6","impliedFormat":1},{"version":"f579f267a2f4c2278cca2ec84613e95059368b503ce96586972d304e5e40125b","affectsGlobalScope":true,"impliedFormat":1},{"version":"23459c1915878a7c1e86e8bdb9c187cddd3aea105b8b1dfce512f093c969bc7e","impliedFormat":1},{"version":"b1b6ee0d012aeebe11d776a155d8979730440082797695fc8e2a5c326285678f","impliedFormat":1},{"version":"45875bcae57270aeb3ebc73a5e3fb4c7b9d91d6b045f107c1d8513c28ece71c0","impliedFormat":1},{"version":"1dc73f8854e5c4506131c4d95b3a6c24d0c80336d3758e95110f4c7b5cb16397","affectsGlobalScope":true,"impliedFormat":1},{"version":"5f6f1d54779d0b9ed152b0516b0958cd34889764c1190434bbf18e7a8bb884cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"c6b4e0a02545304935ecbf7de7a8e056a31bb50939b5b321c9d50a405b5a0bba","impliedFormat":1},{"version":"fab29e6d649aa074a6b91e3bdf2bff484934a46067f6ee97a30fcd9762ae2213","impliedFormat":1},{"version":"8145e07aad6da5f23f2fcd8c8e4c5c13fb26ee986a79d03b0829b8fce152d8b2","impliedFormat":1},{"version":"e1120271ebbc9952fdc7b2dd3e145560e52e06956345e6fdf91d70ca4886464f","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"f7b1df115dbd1b8522cba4f404a9f4fdcd5169e2137129187ffeee9d287e4fd1","impliedFormat":1},{"version":"c878f74b6d10b267f6075c51ac1d8becd15b4aa6a58f79c0cfe3b24908357f60","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"93452d394fdd1dc551ec62f5042366f011a00d342d36d50793b3529bfc9bd633","impliedFormat":1},{"version":"fbf68fc8057932b1c30107ebc37420f8d8dc4bef1253c4c2f9e141886c0df5ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2754d8221d77c7b382096651925eb476f1066b3348da4b73fe71ced7801edada","impliedFormat":1},{"version":"993985beef40c7d113f6dd8f0ba26eed63028b691fbfeb6a5b63f26408dd2c6d","affectsGlobalScope":true,"impliedFormat":1},{"version":"bef91efa0baea5d0e0f0f27b574a8bc100ce62a6d7e70220a0d58af6acab5e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"282fd2a1268a25345b830497b4b7bf5037a5e04f6a9c44c840cb605e19fea841","impliedFormat":1},{"version":"5360a27d3ebca11b224d7d3e38e3e2c63f8290cb1fcf6c3610401898f8e68bc3","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"7d6ff413e198d25639f9f01f16673e7df4e4bd2875a42455afd4ecc02ef156da","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb094bb347d7df3380299eb69836c2c8758626ecf45917577707c03cf816b6f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"f689c4237b70ae6be5f0e4180e8833f34ace40529d1acc0676ab8fb8f70457d7","impliedFormat":1},{"version":"b02784111b3fc9c38590cd4339ff8718f9329a6f4d3fd66e9744a1dcd1d7e191","impliedFormat":1},{"version":"ac5ed35e649cdd8143131964336ab9076937fa91802ec760b3ea63b59175c10a","impliedFormat":1},{"version":"52a8e7e8a1454b6d1b5ad428efae3870ffc56f2c02d923467f2940c454aa9aec","affectsGlobalScope":true,"impliedFormat":1},{"version":"78dc0513cc4f1642906b74dda42146bcbd9df7401717d6e89ea6d72d12ecb539","impliedFormat":1},{"version":"ad90122e1cb599b3bc06a11710eb5489101be678f2920f2322b0ac3e195af78d","impliedFormat":1},{"version":"785b9d575b49124ce01b46f5b9402157c7611e6532effa562ac6aebec0074dfc","impliedFormat":1},{"version":"b2950c2ab847031219cd1802fd55bcb854968f56ef65cf0e5df4c6fe5433e70b","affectsGlobalScope":true,"impliedFormat":1},{"version":"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","impliedFormat":1},{"version":"02b1133807234b1a7d9bf9b1419ee19444dd8c26b101bc268aa8181591241f1f","impliedFormat":1},{"version":"6222e987b58abfe92597e1273ad7233626285bc2d78409d4a7b113d81a83496b","impliedFormat":1},{"version":"cbe726263ae9a7bf32352380f7e8ab66ee25b3457137e316929269c19e18a2be","impliedFormat":1},{"version":"0a25f947e7937ee5e01a21eb10d49de3b467eba752d3b42ea442e9e773f254ef","impliedFormat":99},{"version":"f11151a83668f94c1e763e39d89c0022ceb74618f1bfcf67596044acbe306094","impliedFormat":99},{"version":"b8caba62c0d2ef625f31cbb4fde09d851251af2551086ccf068611b0a69efd81","affectsGlobalScope":true,"impliedFormat":1},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"8885cf05f3e2abf117590bbb951dcf6359e3e5ac462af1c901cfd24c6a6472e2","impliedFormat":1},{"version":"33f3718dababfc26dfd9832c150149ea4e934f255130f8c118a59ae69e5ed441","impliedFormat":1},{"version":"e61df3640a38d535fd4bc9f4a53aef17c296b58dc4b6394fd576b808dd2fe5e6","impliedFormat":1},{"version":"459920181700cec8cbdf2a5faca127f3f17fd8dd9d9e577ed3f5f3af5d12a2e4","impliedFormat":1},{"version":"4719c209b9c00b579553859407a7e5dcfaa1c472994bd62aa5dd3cc0757eb077","impliedFormat":1},{"version":"7ec359bbc29b69d4063fe7dad0baaf35f1856f914db16b3f4f6e3e1bca4099fa","impliedFormat":1},{"version":"70790a7f0040993ca66ab8a07a059a0f8256e7bb57d968ae945f696cbff4ac7a","impliedFormat":1},{"version":"d1b9a81e99a0050ca7f2d98d7eedc6cda768f0eb9fa90b602e7107433e64c04c","impliedFormat":1},{"version":"a022503e75d6953d0e82c2c564508a5c7f8556fad5d7f971372d2d40479e4034","impliedFormat":1},{"version":"b215c4f0096f108020f666ffcc1f072c81e9f2f95464e894a5d5f34c5ea2a8b1","impliedFormat":1},{"version":"644491cde678bd462bb922c1d0cfab8f17d626b195ccb7f008612dc31f445d2d","impliedFormat":1},{"version":"dfe54dab1fa4961a6bcfba68c4ca955f8b5bbeb5f2ab3c915aa7adaa2eabc03a","impliedFormat":1},{"version":"1bb61aa2f08ab4506d41dbe16c5f3f5010f014bbf46fa3d715c0cbe3b00f4e1c","impliedFormat":1},{"version":"47865c5e695a382a916b1eedda1b6523145426e48a2eae4647e96b3b5e52024f","impliedFormat":1},{"version":"e42820cd611b15910c204cd133f692dcd602532b39317d4f2a19389b27e6f03d","impliedFormat":1},{"version":"331b8f71bfae1df25d564f5ea9ee65a0d847c4a94baa45925b6f38c55c7039bf","impliedFormat":1},{"version":"2a771d907aebf9391ac1f50e4ad37952943515eeea0dcc7e78aa08f508294668","impliedFormat":1},{"version":"0146fd6262c3fd3da51cb0254bb6b9a4e42931eb2f56329edd4c199cb9aaf804","impliedFormat":1},{"version":"183f480885db5caa5a8acb833c2be04f98056bdcc5fb29e969ff86e07efe57ab","impliedFormat":99},{"version":"875ebeac487c4cafe5b14db9b5b36b2b4dc83cf0e49fa82871075388622cd1ba","impliedFormat":1},{"version":"a18642ddf216f162052a16cba0944892c4c4c977d3306a87cb673d46abbb0cbf","impliedFormat":1},{"version":"509f8efdfc5f9f6b52284170e8d7413552f02d79518d1db691ee15acc0088676","impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":1},{"version":"7870becb94cbc11d2d01b77c4422589adcba4d8e59f726246d40cd0d129784d8","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"8f07f2b6514744ac96e51d7cb8518c0f4de319471237ea10cf688b8d0e9d0225","impliedFormat":1},{"version":"08971f8379717d46a8a990ce9a7eed3af3e47e22c3d45c3a046054b7a2fffe7a","impliedFormat":99},{"version":"8d27e5f73b75340198b2df36f39326f693743e64006bd7b88a925a5f285df628","impliedFormat":1},{"version":"a7e9e5bb507146e1c06aae94b548c9227d41f2c773da5fbb152388558710bae2","impliedFormat":1},{"version":"1c2cd862994b1fbed3cde0d1e8de47835ff112d197a3debfddf7b2ee3b2c52bc","impliedFormat":1},{"version":"ed5b366679b223fe16e583b32d4a724dcea8a70f378ecc9268d472c1f95b3580","impliedFormat":1},{"version":"2be2227c3810dfd84e46674fd33b8d09a4a28ad9cb633ed536effd411665ea1e","impliedFormat":99},{"version":"c302df1d6f371c6064cb5f4d0b41165425b682b287a3b8625527b2752eb433ee","impliedFormat":1},{"version":"a3ba438d3b86d2bf70ae20150ddbe653d098ee996f62218c066ded4372f88d23","impliedFormat":1},{"version":"3feec212c0aeb91e5a6e62caaf9f128954590210f8c302910ea377c088f6b61a","impliedFormat":99},{"version":"d27eadfc7a0c340fbbb62294e70eb5cf27751e1dcf47ee688ca38dd64d15502c","impliedFormat":99},{"version":"54895c782637a5cd4696a22ea361c107abe8b9e0655ec1b2881504c05af5f6cf","impliedFormat":99},{"version":"95018f021c62a54ee264d40702a91546c9c405f498d655b48e054c48b0b481d0","signature":"4b96dd19fd2949d28ce80e913412b0026dc421e5bf6c31d87c7b5eb11b5753b4"},{"version":"ae77d81a5541a8abb938a0efedf9ac4bea36fb3a24cc28cfa11c598863aba571","impliedFormat":1},{"version":"e0c868a08451c879984ccf4d4e3c1240b3be15af8988d230214977a3a3dad4ce","impliedFormat":1},{"version":"469532350a366536390c6eb3bde6839ec5c81fe1227a6b7b6a70202954d70c40","impliedFormat":1},{"version":"17c9f569be89b4c3c17dc17a9fb7909b6bab34f73da5a9a02d160f502624e2e8","impliedFormat":1},{"version":"003df7b9a77eaeb7a524b795caeeb0576e624e78dea5e362b053cb96ae89132a","impliedFormat":1},{"version":"7ba17571f91993b87c12b5e4ecafe66b1a1e2467ac26fcb5b8cee900f6cf8ff4","impliedFormat":1},{"version":"6fc1a4f64372593767a9b7b774e9b3b92bf04e8785c3f9ea98973aa9f4bbe490","impliedFormat":1},{"version":"d30e67059f5c545c5f8f0cc328a36d2e03b8c4a091b4301bc1d6afb2b1491a3a","impliedFormat":1},{"version":"8b219399c6a743b7c526d4267800bd7c84cf8e27f51884c86ad032d662218a9d","impliedFormat":1},{"version":"bad6d83a581dbd97677b96ee3270a5e7d91b692d220b87aab53d63649e47b9ad","impliedFormat":1},{"version":"7f15c8d21ca2c062f4760ff3408e1e0ec235bad2ca4e2842d1da7fc76bb0b12f","impliedFormat":1},{"version":"54e79224429e911b5d6aeb3cf9097ec9fd0f140d5a1461bbdece3066b17c232c","impliedFormat":1},{"version":"e1b666b145865bc8d0d843134b21cf589c13beba05d333c7568e7c30309d933a","impliedFormat":1},{"version":"ff09b6fbdcf74d8af4e131b8866925c5e18d225540b9b19ce9485ca93e574d84","impliedFormat":1},{"version":"c836b5d8d84d990419548574fc037c923284df05803b098fe5ddaa49f88b898a","impliedFormat":1},{"version":"3a2b8ed9d6b687ab3e1eac3350c40b1624632f9e837afe8a4b5da295acf491cb","impliedFormat":1},{"version":"189266dd5f90a981910c70d7dfa05e2bca901a4f8a2680d7030c3abbfb5b1e23","impliedFormat":1},{"version":"5ec8dcf94c99d8f1ed7bb042cdfa4ef6a9810ca2f61d959be33bcaf3f309debe","impliedFormat":1},{"version":"a80e02af710bdac31f2d8308890ac4de4b6a221aafcbce808123bfc2903c5dc2","impliedFormat":1},{"version":"d5895252efa27a50f134a9b580aa61f7def5ab73d0a8071f9b5bf9a317c01c2d","impliedFormat":1},{"version":"2c378d9368abcd2eba8c29b294d40909845f68557bc0b38117e4f04fc56e5f9c","impliedFormat":1},{"version":"0f345151cece7be8d10df068b58983ea8bcbfead1b216f0734037a6c63d8af87","impliedFormat":1},{"version":"37fd7bde9c88aa142756d15aeba872498f45ad149e0d1e56f3bccc1af405c520","impliedFormat":1},{"version":"2a920fd01157f819cf0213edfb801c3fb970549228c316ce0a4b1885020bad35","impliedFormat":1},{"version":"56208c500dcb5f42be7e18e8cb578f257a1a89b94b3280c506818fed06391805","impliedFormat":1},{"version":"0c94c2e497e1b9bcfda66aea239d5d36cd980d12a6d9d59e66f4be1fa3da5d5a","impliedFormat":1},{"version":"a67774ceb500c681e1129b50a631fa210872bd4438fae55e5e8698bac7036b19","impliedFormat":1},{"version":"bb220eaac1677e2ad82ac4e7fd3e609a0c7b6f2d6d9c673a35068c97f9fcd5cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"dd8936160e41420264a9d5fade0ff95cc92cab56032a84c74a46b4c38e43121e","impliedFormat":1},{"version":"1f366bde16e0513fa7b64f87f86689c4d36efd85afce7eb24753e9c99b91c319","impliedFormat":1},{"version":"421c3f008f6ef4a5db2194d58a7b960ef6f33e94b033415649cd557be09ef619","impliedFormat":1},{"version":"57568ff84b8ba1a4f8c817141644b49252cc39ec7b899e4bfba0ec0557c910a0","impliedFormat":1},{"version":"e6f10f9a770dedf552ca0946eef3a3386b9bfb41509233a30fc8ca47c49db71c","impliedFormat":1}],"root":[207],"options":{"allowSyntheticDefaultImports":true,"composite":true,"esModuleInterop":true,"module":99,"skipLibCheck":true,"strict":true,"target":9},"referencedMap":[[199,1],[197,2],[206,3],[208,2],[209,2],[211,4],[212,4],[213,2],[214,2],[216,5],[217,2],[218,2],[219,4],[220,2],[221,2],[222,6],[223,2],[224,2],[225,7],[226,2],[227,8],[228,2],[229,2],[230,2],[231,2],[234,2],[233,9],[210,2],[235,10],[236,2],[232,2],[237,2],[238,4],[239,11],[240,12],[160,2],[215,2],[106,13],[107,13],[108,14],[67,15],[109,16],[110,17],[111,18],[62,2],[65,19],[63,2],[64,2],[112,20],[113,21],[114,22],[115,23],[116,24],[117,25],[118,25],[120,26],[119,27],[121,28],[122,29],[123,30],[105,31],[66,2],[124,32],[125,33],[126,34],[159,35],[127,36],[128,37],[129,38],[130,39],[131,40],[132,41],[133,42],[134,43],[135,44],[136,45],[137,45],[138,46],[139,2],[140,2],[141,47],[143,48],[142,49],[144,50],[145,51],[146,52],[147,53],[148,54],[149,55],[150,56],[151,57],[152,58],[153,59],[154,60],[155,61],[156,62],[157,63],[158,64],[205,65],[200,66],[203,67],[198,2],[168,2],[189,2],[191,68],[190,2],[201,2],[185,69],[183,70],[184,71],[172,72],[173,70],[180,73],[171,74],[176,75],[186,2],[177,76],[182,77],[188,78],[187,79],[170,80],[178,81],[179,82],[174,83],[181,69],[175,84],[162,85],[161,86],[169,2],[60,2],[61,2],[12,2],[11,2],[2,2],[13,2],[14,2],[15,2],[16,2],[17,2],[18,2],[19,2],[20,2],[3,2],[21,2],[22,2],[4,2],[23,2],[27,2],[24,2],[25,2],[26,2],[28,2],[29,2],[30,2],[5,2],[31,2],[32,2],[33,2],[34,2],[6,2],[38,2],[35,2],[36,2],[37,2],[39,2],[7,2],[40,2],[45,2],[46,2],[41,2],[42,2],[43,2],[44,2],[8,2],[50,2],[47,2],[48,2],[49,2],[51,2],[9,2],[52,2],[53,2],[54,2],[56,2],[55,2],[57,2],[58,2],[10,2],[59,2],[1,2],[202,2],[83,87],[93,88],[82,87],[103,89],[74,90],[73,91],[102,92],[96,93],[101,94],[76,95],[90,96],[75,97],[99,98],[71,99],[70,92],[100,100],[72,101],[77,102],[78,2],[81,102],[68,2],[104,103],[94,104],[85,105],[86,106],[88,107],[84,108],[87,109],[97,92],[79,110],[80,111],[89,112],[69,113],[92,104],[91,102],[95,2],[98,114],[196,115],[167,116],[166,117],[164,117],[163,2],[165,118],[194,2],[193,2],[192,119],[195,120],[204,121],[207,122]],"latestChangedDtsFile":"./vite.config.d.ts","version":"5.8.2"}
--------------------------------------------------------------------------------
/vite.config.d.ts:
--------------------------------------------------------------------------------
1 | declare const _default: import("vite").UserConfig;
2 | export default _default;
3 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import tailwindcss from '@tailwindcss/vite'
4 | import path from 'path'
5 |
6 | // https://vite.dev/config/
7 | export default defineConfig({
8 | plugins: [vue(), tailwindcss()],
9 | resolve: {
10 | alias: {
11 | '@': path.resolve(__dirname, './src')
12 | }
13 | }
14 | })
15 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config';
2 | import vue from '@vitejs/plugin-vue';
3 |
4 | export default defineConfig({
5 | plugins: [vue()],
6 | test: {
7 | globals: true,
8 | coverage: {
9 | provider: 'v8',
10 | reporter: ['text', 'json', 'html'],
11 | },
12 | },
13 | });
--------------------------------------------------------------------------------