├── .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 | ![screenshot of the app showing excerpts from the worldmap where the user is challenged to select a country](doc/screenshot.png) 4 | 5 | _do you know where the countries are?_ 6 | 7 | **[Play the free online practice game](https://map.koljapluemer.com)** 8 | 9 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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 | ![screenshots](doc/more_screenshots.png) 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 | [![ts-fsrs npm version](https://img.shields.io/npm/v/ts-fsrs.svg)](https://www.npmjs.com/package/ts-fsrs) 7 | [![Downloads](https://img.shields.io/npm/dm/ts-fsrs)](https://www.npmjs.com/package/ts-fsrs) 8 | [![codecov](https://codecov.io/gh/open-spaced-repetition/ts-fsrs/graph/badge.svg?token=E3KLLDL8QH)](https://codecov.io/gh/open-spaced-repetition/ts-fsrs) 9 | [![Build and Publish](https://github.com/open-spaced-repetition/ts-fsrs/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/open-spaced-repetition/ts-fsrs/actions/workflows/npm-publish.yml) 10 | [![Deploy](https://github.com/open-spaced-repetition/ts-fsrs/actions/workflows/deploy.yml/badge.svg)](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 | 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 | 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 | 34 | 35 | -------------------------------------------------------------------------------- /src/modules/misc-views/stats-view/per-country-stats/CountryStatsView.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | -------------------------------------------------------------------------------- /src/modules/misc-views/stats-view/per-country-stats/DistanceProgressChart.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/modules/misc-views/stats-view/per-country-stats/TimeProgressChart.vue: -------------------------------------------------------------------------------- 1 | 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 | 380 | 381 | 421 | 422 | -------------------------------------------------------------------------------- /src/modules/play/map-renderer/WorldMapGame.vue: -------------------------------------------------------------------------------- 1 | 171 | 172 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/modules/play/play-modes/challenge-play/ChallengeResults.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | -------------------------------------------------------------------------------- /src/modules/play/play-modes/challenge-play/ChallengeRulesPopup.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | -------------------------------------------------------------------------------- /src/modules/play/play-modes/challenge-play/PlayChallengeView.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | -------------------------------------------------------------------------------- /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 | 80 | -------------------------------------------------------------------------------- /src/modules/play/play-modes/custom-play/filter-modal/FilterModal.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /src/modules/play/play-modes/custom-play/filter-modal/tabs/TabCountrySelection.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/modules/play/play-modes/custom-play/filter-modal/tabs/TabQuickSelection.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | -------------------------------------------------------------------------------- /src/modules/play/play-modes/custom-play/filter-modal/tabs/TabWizardSelection.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | }); --------------------------------------------------------------------------------