running-tools

A collection of tools for runners and their coaches
git clone https://git.ashermorgan.net/running-tools/
Log | Files | Refs | README

commit f3cf19ae77d63eb7f9de6a562af72f29177e5cca
parent 7a9ffcd827c2e0603c8486d49e190c0e5e222513
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date:   Sun, 19 May 2024 19:05:57 -0700

Merge pull request #8 from ashermorgan/vue-composition-api

Migrate to Vue Composition API
Diffstat:
Mpackage-lock.json | 919++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mpackage.json | 20++++++++++----------
Msrc/components/DecimalInput.vue | 171+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/components/IntegerInput.vue | 152+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/components/SimpleTargetTable.vue | 152++++++++++++++++++++++++++++++++-----------------------------------------------
Msrc/components/TargetEditor.vue | 196+++++++++++++++++++++++++++++++------------------------------------------------
Msrc/components/TargetSetSelector.vue | 225+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/components/TimeInput.vue | 227++++++++++++++++++++++++++++++++++++-------------------------------------------
Msrc/views/AboutPage.vue | 17++---------------
Msrc/views/HomePage.vue | 6------
Msrc/views/NotFoundPage.vue | 6------
Msrc/views/PaceCalculator.vue | 296++++++++++++++++++++++++++++++++++++-------------------------------------------
Msrc/views/RaceCalculator.vue | 501+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/views/SplitCalculator.vue | 253+++++++++++++++++++++++++++++++++----------------------------------------------
Msrc/views/UnitCalculator.vue | 399+++++++++++++++++++++++++++++++++++++------------------------------------------
Mtests/unit/components/TargetSetSelector.spec.js | 6+++---
Mtests/unit/components/TimeInput.spec.js | 4++--
Mtests/unit/views/PaceCalculator.spec.js | 72++++++++++++++----------------------------------------------------------
Mtests/unit/views/RaceCalculator.spec.js | 72++++++++++++++----------------------------------------------------------
Mtests/unit/views/SplitCalculator.spec.js | 448+++++++++++++++++++++++++++++--------------------------------------------------
Mtests/unit/views/UnitCalculator.spec.js | 4++--
21 files changed, 2003 insertions(+), 2143 deletions(-)

diff --git a/package-lock.json b/package-lock.json @@ -8,22 +8,22 @@ "name": "running-tools", "version": "1.3.0", "dependencies": { - "feather-icons": "^4.29.0", - "vue": "^3.3.4", + "feather-icons": "^4.29.2", + "vue": "^3.4.27", "vue-feather": "^2.0.0", - "vue-router": "^4.2.2" + "vue-router": "^4.3.2" }, "devDependencies": { "@playwright/test": "^1.44.0", - "@types/node": "^20.12.11", - "@vitejs/plugin-vue": "^4.2.3", - "@vue/test-utils": "^2.4.0", - "eslint": "^8.39.0", - "eslint-plugin-vue": "^9.11.0", + "@types/node": "^20.12.12", + "@vitejs/plugin-vue": "^4.6.2", + "@vue/test-utils": "^2.4.6", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.26.0", "jsdom": "^22.1.0", "pwa-asset-generator": "^6.3.1", - "vite": "^4.3.9", - "vite-plugin-pwa": "^0.16.4", + "vite": "^4.5.3", + "vite-plugin-pwa": "^0.16.7", "vitest": "^0.32.4" } }, @@ -646,9 +646,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -2295,23 +2295,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.2", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -2327,22 +2327,22 @@ } }, "node_modules/@eslint/js": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz", - "integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -2363,11 +2363,55 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@jest/schemas": { "version": "29.6.0", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz", @@ -2487,6 +2531,22 @@ "node": ">= 8" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@playwright/test": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz", @@ -2566,9 +2626,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", - "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -2605,16 +2665,22 @@ "@types/node": "*" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@vitejs/plugin-vue": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz", - "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", "dev": true, "engines": { "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.0.0", + "vite": "^4.0.0 || ^5.0.0", "vue": "^3.2.25" } }, @@ -2714,133 +2780,108 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", - "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.27.tgz", + "integrity": "sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==", "dependencies": { - "@babel/parser": "^7.21.3", - "@vue/shared": "3.3.4", + "@babel/parser": "^7.24.4", + "@vue/shared": "3.4.27", + "entities": "^4.5.0", "estree-walker": "^2.0.2", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", - "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz", + "integrity": "sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==", "dependencies": { - "@vue/compiler-core": "3.3.4", - "@vue/shared": "3.3.4" + "@vue/compiler-core": "3.4.27", + "@vue/shared": "3.4.27" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", - "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", - "dependencies": { - "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.4", - "@vue/compiler-dom": "3.3.4", - "@vue/compiler-ssr": "3.3.4", - "@vue/reactivity-transform": "3.3.4", - "@vue/shared": "3.3.4", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz", + "integrity": "sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==", + "dependencies": { + "@babel/parser": "^7.24.4", + "@vue/compiler-core": "3.4.27", + "@vue/compiler-dom": "3.4.27", + "@vue/compiler-ssr": "3.4.27", + "@vue/shared": "3.4.27", "estree-walker": "^2.0.2", - "magic-string": "^0.30.0", - "postcss": "^8.1.10", - "source-map-js": "^1.0.2" + "magic-string": "^0.30.10", + "postcss": "^8.4.38", + "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", - "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz", + "integrity": "sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==", "dependencies": { - "@vue/compiler-dom": "3.3.4", - "@vue/shared": "3.3.4" + "@vue/compiler-dom": "3.4.27", + "@vue/shared": "3.4.27" } }, "node_modules/@vue/devtools-api": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", - "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.1.tgz", + "integrity": "sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==" }, "node_modules/@vue/reactivity": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", - "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.27.tgz", + "integrity": "sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==", "dependencies": { - "@vue/shared": "3.3.4" - } - }, - "node_modules/@vue/reactivity-transform": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", - "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", - "dependencies": { - "@babel/parser": "^7.20.15", - "@vue/compiler-core": "3.3.4", - "@vue/shared": "3.3.4", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.0" + "@vue/shared": "3.4.27" } }, "node_modules/@vue/runtime-core": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", - "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.27.tgz", + "integrity": "sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==", "dependencies": { - "@vue/reactivity": "3.3.4", - "@vue/shared": "3.3.4" + "@vue/reactivity": "3.4.27", + "@vue/shared": "3.4.27" } }, "node_modules/@vue/runtime-dom": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", - "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz", + "integrity": "sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==", "dependencies": { - "@vue/runtime-core": "3.3.4", - "@vue/shared": "3.3.4", - "csstype": "^3.1.1" + "@vue/runtime-core": "3.4.27", + "@vue/shared": "3.4.27", + "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", - "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.27.tgz", + "integrity": "sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==", "dependencies": { - "@vue/compiler-ssr": "3.3.4", - "@vue/shared": "3.3.4" + "@vue/compiler-ssr": "3.4.27", + "@vue/shared": "3.4.27" }, "peerDependencies": { - "vue": "3.3.4" + "vue": "3.4.27" } }, "node_modules/@vue/shared": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", - "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==" + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", + "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==" }, "node_modules/@vue/test-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.0.tgz", - "integrity": "sha512-BKB9aj1yky63/I3IwSr1FjUeHYsKXI7D6S9F378AHt7a5vC0dLkOBtSsFXoRGC/7BfHmiB9HRhT+i9xrUHoAKw==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", "dev": true, "dependencies": { - "js-beautify": "1.14.6", - "vue-component-type-helpers": "1.6.5" - }, - "peerDependencies": { - "@vue/compiler-dom": "^3.0.1", - "@vue/server-renderer": "^3.0.1", - "vue": "^3.0.1" - }, - "peerDependenciesMeta": { - "@vue/compiler-dom": { - "optional": true - }, - "@vue/server-renderer": { - "optional": true - } + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" } }, "node_modules/abab": { @@ -2850,10 +2891,13 @@ "dev": true }, "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/acorn": { "version": "8.9.0", @@ -3582,9 +3626,9 @@ } }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/data-urls": { "version": "4.0.0", @@ -3803,50 +3847,67 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/editorconfig": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", - "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", "dev": true, "dependencies": { - "commander": "^2.19.0", - "lru-cache": "^4.1.5", - "semver": "^5.6.0", - "sigmund": "^1.0.1" + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" }, "bin": { "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" } }, - "node_modules/editorconfig/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "balanced-match": "^1.0.0" } }, - "node_modules/editorconfig/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, - "bin": { - "semver": "bin/semver" + "engines": { + "node": ">=14" } }, - "node_modules/editorconfig/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/ejs": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", - "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "dependencies": { "jake": "^10.8.5" @@ -3864,6 +3925,12 @@ "integrity": "sha512-kKiHnbrHME7z8E6AYaw0ehyxY5+hdaRmeUbjBO22LZMdqTYCO29EvF0T1cQ3pJ1RN5fyMcHl1Lmcsdt9WWJpJQ==", "dev": true }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -3877,7 +3944,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "engines": { "node": ">=0.12" }, @@ -4033,27 +4099,28 @@ } }, "node_modules/eslint": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz", - "integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.43.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -4063,7 +4130,6 @@ "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", @@ -4073,9 +4139,8 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -4089,30 +4154,31 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.15.1.tgz", - "integrity": "sha512-CJE/oZOslvmAR9hf8SClTdQ9JLweghT6JCBQNrT2Iel1uVw0W0OLJxzvPd6CxmABKCvLrtyDnqGV37O7KQv6+A==", + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.26.0.tgz", + "integrity": "sha512-eTvlxXgd4ijE1cdur850G6KalZqk65k1JKoOI2d1kT3hr8sPD07j1q98FRFdNnpxBELGPWxZmInxeHGF/GxtqQ==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.3.0", + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", "natural-compare": "^1.4.0", - "nth-check": "^2.0.1", - "postcss-selector-parser": "^6.0.9", - "semver": "^7.3.5", - "vue-eslint-parser": "^9.3.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.0", + "vue-eslint-parser": "^9.4.2", "xml-name-validator": "^4.0.0" }, "engines": { "node": "^14.17.0 || >=16.0.0" }, "peerDependencies": { - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, "node_modules/eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -4126,9 +4192,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4138,12 +4204,12 @@ } }, "node_modules/espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" }, @@ -4240,9 +4306,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.0.tgz", - "integrity": "sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -4298,9 +4364,9 @@ } }, "node_modules/feather-icons": { - "version": "4.29.0", - "resolved": "https://registry.npmjs.org/feather-icons/-/feather-icons-4.29.0.tgz", - "integrity": "sha512-Y7VqN9FYb8KdaSF0qD1081HCkm0v4Eq/fpfQYQnubpqi0hXx14k+gF9iqtRys1SIcTEi97xDi/fmsPFZ8xo0GQ==", + "version": "4.29.2", + "resolved": "https://registry.npmjs.org/feather-icons/-/feather-icons-4.29.2.tgz", + "integrity": "sha512-0TaCFTnBTVCz6U+baY2UJNKne5ifGh7sMG4ZC2LoBWCZdIyPa+y6UiR4lEYGws1JOFWdee8KAsAIvu0VcXqiqA==", "dependencies": { "classnames": "^2.2.5", "core-js": "^3.1.3" @@ -4427,6 +4493,22 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -4627,9 +4709,9 @@ } }, "node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -4879,9 +4961,9 @@ ] }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -5083,6 +5165,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5307,6 +5398,24 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.8.7", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", @@ -5340,15 +5449,16 @@ } }, "node_modules/js-beautify": { - "version": "1.14.6", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.6.tgz", - "integrity": "sha512-GfofQY5zDp+cuHc+gsEXKPpNw2KbPddreEo35O6jT6i0RVK6LhsoYBhq5TvK4/n74wnA0QbK8gGd+jUZwTMKJw==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", "dev": true, "dependencies": { "config-chain": "^1.1.13", - "editorconfig": "^0.15.3", - "glob": "^8.0.3", - "nopt": "^6.0.0" + "editorconfig": "^1.0.4", + "glob": "^10.3.3", + "js-cookie": "^3.0.5", + "nopt": "^7.2.0" }, "bin": { "css-beautify": "js/bin/css-beautify.js", @@ -5356,7 +5466,7 @@ "js-beautify": "js/bin/js-beautify.js" }, "engines": { - "node": ">=10" + "node": ">=14" } }, "node_modules/js-beautify/node_modules/brace-expansion": { @@ -5369,34 +5479,49 @@ } }, "node_modules/js-beautify/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "version": "10.3.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz", + "integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.11.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=12" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/js-beautify/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "engines": { + "node": ">=14" } }, "node_modules/js-tokens": { @@ -5690,14 +5815,11 @@ } }, "node_modules/magic-string": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", - "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" + "@jridgewell/sourcemap-codec": "^1.4.15" } }, "node_modules/map-obj": { @@ -5840,6 +5962,15 @@ "node": ">= 6" } }, + "node_modules/minipass": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz", + "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -5936,18 +6067,18 @@ "dev": true }, "node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", "dev": true, "dependencies": { - "abbrev": "^1.0.0" + "abbrev": "^2.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/normalize-package-data": { @@ -6172,6 +6303,31 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/pathe": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", @@ -6316,9 +6472,9 @@ } }, "node_modules/postcss": { - "version": "8.4.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", - "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -6336,16 +6492,16 @@ "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -6379,9 +6535,9 @@ } }, "node_modules/pretty-bytes": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.0.tgz", - "integrity": "sha512-Rk753HI8f4uivXi4ZCIYdhmG1V+WKzvRMg/X+M42a6t7D07RcmopXJMDNk6N++7Bl75URRGsb40ruvg7Hcp2wQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", "dev": true, "engines": { "node": "^14.13.1 || >=16.0.0" @@ -6437,12 +6593,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", - "dev": true - }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -7000,13 +7150,10 @@ } }, "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -7064,11 +7211,17 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, - "node_modules/sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==", - "dev": true + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/slash": { "version": "3.0.0", @@ -7092,9 +7245,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -7204,6 +7357,71 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", @@ -7294,6 +7512,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", @@ -7787,9 +8018,9 @@ } }, "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -7865,14 +8096,14 @@ } }, "node_modules/vite-plugin-pwa": { - "version": "0.16.4", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.4.tgz", - "integrity": "sha512-lmwHFIs9zI2H9bXJld/zVTbCqCQHZ9WrpyDMqosICDV0FVnCJwniX1NMDB79HGTIZzOQkY4gSZaVTJTw6maz/Q==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.16.7.tgz", + "integrity": "sha512-4WMA5unuKlHs+koNoykeuCfTcqEGbiTRr8sVYUQMhc6tWxZpSRnv9Ojk4LKmqVhoPGHfBVCdGaMo8t9Qidkc1Q==", "dev": true, "dependencies": { "debug": "^4.3.4", - "fast-glob": "^3.2.12", - "pretty-bytes": "^6.0.0", + "fast-glob": "^3.3.1", + "pretty-bytes": "^6.1.1", "workbox-build": "^7.0.0", "workbox-window": "^7.0.0" }, @@ -7883,7 +8114,7 @@ "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "vite": "^3.1.0 || ^4.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", "workbox-build": "^7.0.0", "workbox-window": "^7.0.0" } @@ -7966,27 +8197,35 @@ } }, "node_modules/vue": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", - "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz", + "integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==", "dependencies": { - "@vue/compiler-dom": "3.3.4", - "@vue/compiler-sfc": "3.3.4", - "@vue/runtime-dom": "3.3.4", - "@vue/server-renderer": "3.3.4", - "@vue/shared": "3.3.4" + "@vue/compiler-dom": "3.4.27", + "@vue/compiler-sfc": "3.4.27", + "@vue/runtime-dom": "3.4.27", + "@vue/server-renderer": "3.4.27", + "@vue/shared": "3.4.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/vue-component-type-helpers": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-1.6.5.tgz", - "integrity": "sha512-iGdlqtajmiqed8ptURKPJ/Olz0/mwripVZszg6tygfZSIL9kYFPJTNY6+Q6OjWGznl2L06vxG5HvNvAnWrnzbg==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.19.tgz", + "integrity": "sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==", "dev": true }, "node_modules/vue-eslint-parser": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.1.tgz", - "integrity": "sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==", + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", + "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -8017,11 +8256,11 @@ } }, "node_modules/vue-router": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.2.tgz", - "integrity": "sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.2.tgz", + "integrity": "sha512-hKQJ1vDAZ5LVkKEnHhmm1f9pMiWIBNGF5AwU67PdH7TyXCj/a4hTccuUuYCAMgJK6rO/NVYtQIEN3yL8CECa7Q==", "dependencies": { - "@vue/devtools-api": "^6.5.0" + "@vue/devtools-api": "^6.5.1" }, "funding": { "url": "https://github.com/sponsors/posva" @@ -8513,6 +8752,100 @@ "workbox-core": "7.0.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json @@ -12,22 +12,22 @@ "test:e2e": "npx playwright test" }, "dependencies": { - "feather-icons": "^4.29.0", - "vue": "^3.3.4", + "feather-icons": "^4.29.2", + "vue": "^3.4.27", "vue-feather": "^2.0.0", - "vue-router": "^4.2.2" + "vue-router": "^4.3.2" }, "devDependencies": { "@playwright/test": "^1.44.0", - "@types/node": "^20.12.11", - "@vitejs/plugin-vue": "^4.2.3", - "@vue/test-utils": "^2.4.0", - "eslint": "^8.39.0", - "eslint-plugin-vue": "^9.11.0", + "@types/node": "^20.12.12", + "@vitejs/plugin-vue": "^4.6.2", + "@vue/test-utils": "^2.4.6", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.26.0", "jsdom": "^22.1.0", "pwa-asset-generator": "^6.3.1", - "vite": "^4.3.9", - "vite-plugin-pwa": "^0.16.4", + "vite": "^4.5.3", + "vite-plugin-pwa": "^0.16.7", "vitest": "^0.32.4" } } diff --git a/src/components/DecimalInput.vue b/src/components/DecimalInput.vue @@ -1,110 +1,101 @@ <template> - <input ref="input" type="number" step="any" required @blur="onblur" v-model="stringValue"> + <input ref="inputElement" type="number" step="any" required @blur="onblur" v-model="stringValue"> </template> -<script> +<script setup> +import { ref, watch } from 'vue'; import formatUtils from '@/utils/format'; -export default { - name: 'DecimalInput', +/** + * The component value + */ +const model = defineModel({ + type: Number, + default: 0, +}); - props: { - /** - * The input value - */ - modelValue: { - type: Number, - default: 0, - }, - - /** - * The number of digits to show before the decimal point - */ - padding: { - type: Number, - default: 0, - validator(value) { - return value >= 0; - }, +const props = defineProps({ + /** + * The number of digits to show before the decimal point + */ + padding: { + type: Number, + default: 0, + validator(value) { + return value >= 0; }, + }, - /** - * The number of digits to show after the decimal point - */ - digits: { - type: Number, - default: 1, - validator(value) { - return value > 0; - }, + /** + * The number of digits to show after the decimal point + */ + digits: { + type: Number, + default: 1, + validator(value) { + return value > 0; }, }, +}); - data() { - return { - /** - * The internal float value - */ - internalValue: this.modelValue, +/** + * The internal float value + */ +const internalValue = ref(model.value); - /** - * The raw string value (empty if input is currently invalid) - */ - stringValue: this.format(this.modelValue), - }; - }, +/** + * The raw string value (empty if input is currently invalid) + */ +const stringValue = ref(format(model.value)); - watch: { - /** - * Update the component value when the modelValue prop changes - * @param {Number} newValue The new prop value - */ - modelValue(newValue) { - if (newValue !== this.internalValue) { - this.internalValue = newValue; - this.stringValue = this.format(this.internalValue); - } - }, +/** + * The input element + */ +const inputElement = ref(null); - /** - * Emit the input event when the internal value changes - * @param {Number} newValue The new internal float value - */ - internalValue(newValue) { - this.$emit('update:modelValue', newValue); - }, +/* + * Update the internal value when the component value changes + */ +watch(model, (newValue) => { + if (newValue !== internalValue.value) { + internalValue.value = newValue; + stringValue.value = format(internalValue.value); + } +}); - /** - * Update the float value when the raw string value changes - * @param {Number} newValue The new raw string value - */ - stringValue(newValue) { - if (this.$refs.input.validity.valid) { - this.internalValue = Number(newValue); - } - }, - }, +/** + * Update the component value when the internal value changes + */ +watch(internalValue, (newValue) => { + model.value = newValue; +}); - methods: { - /** - * Reformat display value if not invalid - */ - onblur() { - if (this.$refs.input.validity.valid) { - this.stringValue = this.format(this.internalValue); - } - }, +/** + * Update the internal value when the raw string value changes + */ +watch(stringValue, (newValue) => { + if (inputElement.value.validity.valid) { + internalValue.value = Number(newValue); + } +}); - /** - * Format a decimal as a string - * @param {Number} value The decimal - * @returns {String} The formated string - */ - format(value) { - return formatUtils.formatNumber(value, this.padding, this.digits, true); - }, - }, -}; +/** + * Reformat display value if not invalid + */ +function onblur() { + if (inputElement.value.validity.valid) { + stringValue.value = format(internalValue.value); + } +} + +/** + * Format a decimal as a string + * @param {Number} value The decimal + * @returns {String} The formated string + */ +function format(value) { + return formatUtils.formatNumber(value, props.padding, props.digits, true); +} </script> <style scoped> diff --git a/src/components/IntegerInput.vue b/src/components/IntegerInput.vue @@ -1,97 +1,89 @@ <template> - <input ref="input" type="number" step="1" required @blur="onblur" v-model="stringValue"> + <input ref="inputElement" type="number" step="1" required @blur="onblur" v-model="stringValue"> </template> -<script> -export default { - name: 'IntegerInput', +<script setup> +import { ref, watch } from 'vue'; - props: { - /** - * The input value - */ - modelValue: { - type: Number, - default: 0, - }, +/** + * The component value + */ +const model = defineModel({ + type: Number, + default: 0, +}); - /** - * The number of digits to show before the decimal point - */ - padding: { - type: Number, - default: 0, - validator(value) { - return value >= 0; - }, +const props = defineProps({ + /** + * The number of digits to show before the decimal point + */ + padding: { + type: Number, + default: 0, + validator(value) { + return value >= 0; }, }, +}); - data() { - return { - /** - * The internal integer value - */ - internalValue: this.modelValue, +/** + * The internal integer value + */ +const internalValue = ref(model.value); - /** - * The raw string value (empty if input is currently invalid) - */ - stringValue: this.format(this.modelValue), - }; - }, +/** + * The raw string value (empty if input is currently invalid) + */ +const stringValue = ref(format(model.value)); - watch: { - /** - * Update the component value when the modelValue prop changes - * @param {Number} newValue The new prop value - */ - modelValue(newValue) { - if (newValue !== this.internalValue) { - this.internalValue = newValue; - this.stringValue = this.format(this.internalValue); - } - }, +/** + * The input element + */ +const inputElement = ref(null); - /** - * Emit the input event when the internal value changes - * @param {Number} newValue The new internal integer value - */ - internalValue(newValue) { - this.$emit('update:modelValue', newValue); - }, +/** + * Update the internal value when the component value changes + */ +watch(model, (newValue) => { + if (newValue !== internalValue.value) { + internalValue.value = newValue; + stringValue.value = format(internalValue.value); + } +}); - /** - * Update the integer value when the raw string value changes - * @param {Number} newValue The new raw string value - */ - stringValue(newValue) { - if (this.$refs.input.validity.valid) { - this.internalValue = Number(newValue); - } - }, - }, +/** + * Update the component value when the internal value changes + */ +watch(internalValue, (newValue) => { + model.value = newValue; +}); - methods: { - /** - * Reformat display value if not invalid - */ - onblur() { - if (this.$refs.input.validity.valid) { - this.stringValue = this.format(this.internalValue); - } - }, +/** + * Update the internal value when the raw string value changes + */ +watch(stringValue, (newValue) => { + if (inputElement.value.validity.valid) { + internalValue.value = Number(newValue); + } +}); - /** - * Format an integer as a string - * @param {Number} value The integer - * @returns {String} The formated string - */ - format(value) { - return value.toString().padStart(this.padding, '0'); - }, - }, -}; +/** + * Reformat display value if not invalid + */ +function onblur() { + if (inputElement.value.validity.valid) { + stringValue.value = format(internalValue.value); + } +} + +/** + * Format an integer as a string + * @param {Number} value The integer + * @returns {String} The formated string + */ +function format(value) { + return value.toString().padStart(props.padding, '0'); +} </script> <style scoped> diff --git a/src/components/SimpleTargetTable.vue b/src/components/SimpleTargetTable.vue @@ -14,17 +14,18 @@ <tbody> <tr v-for="(item, index) in results" :key="index"> <td :class="item.result === 'distance' ? 'result' : ''"> - {{ formatNumber(item.distanceValue, 0, 2, item.result === 'distance') }} - {{ distanceUnits[item.distanceUnit].symbol }} + {{ formatUtils.formatNumber(item.distanceValue, 0, 2, item.result === 'distance') }} + {{ unitUtils.DISTANCE_UNITS[item.distanceUnit].symbol }} </td> <td :class="item.result === 'time' ? 'result' : ''"> - {{ formatDuration(item.time, 3, 2, item.result === 'time') }} + {{ formatUtils.formatDuration(item.time, 3, 2, item.result === 'time') }} </td> <td v-if="showPace"> - {{ formatDuration(getPace(item), 3, 0, true) }} - / {{ distanceUnits[getDefaultDistanceUnit(defaultUnitSystem)].symbol }} + {{ formatUtils.formatDuration(getPace(item), 3, 0, true) }} + / {{ unitUtils.DISTANCE_UNITS[unitUtils.getDefaultDistanceUnit(defaultUnitSystem)] + .symbol }} </td> </tr> @@ -38,102 +39,71 @@ </div> </template> -<script> +<script setup> +import { computed } from 'vue'; import formatUtils from '@/utils/format'; import unitUtils from '@/utils/units'; -export default { - name: 'SimpleTargetTable', - - props: { - /** - * The method that generates the target table rows - */ - calculateResult: { - type: Function, - required: true, - }, - - /** - * The target set - */ - targets: { - type: Array, - default: () => [], - }, - - /** - * Whether to show result paces - */ - showPace: { - type: Boolean, - default: false, - }, - - /** - * The unit system to use when showing result paces - */ - defaultUnitSystem: { - type: String, - default: 'metric', - }, +const props = defineProps({ + /** + * The method that generates the target table rows + */ + calculateResult: { + type: Function, + required: true, }, - data() { - return { - /** - * The distance units - */ - distanceUnits: unitUtils.DISTANCE_UNITS, - - /** - * The formatDuration method - */ - formatDuration: formatUtils.formatDuration, - - /** - * The formatNumber method - */ - formatNumber: formatUtils.formatNumber, - - /** - * The getDefaultDistanceUnit method - */ - getDefaultDistanceUnit: unitUtils.getDefaultDistanceUnit, - }; + /** + * The target set + */ + targets: { + type: Array, + default: () => [], }, - computed: { - /** - * The target table results - */ - results() { - // Calculate results - const result = []; - this.targets.forEach((row) => { - // Add result - result.push(this.calculateResult(row)); - }); - - // Sort results by time - result.sort((a, b) => a.time - b.time); - - // Return results - return result; - }, + /** + * Whether to show result paces + */ + showPace: { + type: Boolean, + default: false, }, - methods: { - /** - * Get the pace of a result - * @param {Object} result The result - */ - getPace(result) { - return result.time / unitUtils.convertDistance(result.distanceValue, result.distanceUnit, - unitUtils.getDefaultDistanceUnit(this.defaultUnitSystem)); - }, + /** + * The unit system to use when showing result paces + */ + defaultUnitSystem: { + type: String, + default: 'metric', }, -}; +}); + +/** + * The target table results + */ +const results = computed(() => { + // Calculate results + const result = []; + props.targets.forEach((row) => { + // Add result + result.push(props.calculateResult(row)); + }); + + // Sort results by time + result.sort((a, b) => a.time - b.time); + + // Return results + return result; +}); + +/** + * Get the pace of a result + * @param {Object} result The result + */ +function getPace(result) { + return result.time / unitUtils.convertDistance(result.distanceValue, result.distanceUnit, + unitUtils.getDefaultDistanceUnit(props.defaultUnitSystem)); +} </script> <style scoped> diff --git a/src/components/TargetEditor.vue b/src/components/TargetEditor.vue @@ -7,13 +7,13 @@ <input v-model="internalValue.name" placeholder="Target set label" aria-label="Target set label"/> <button class="icon" :title="isCustomSet ? 'Delete target set' : 'Revert target set'" - @click="revert"> + @click="emit('revert')"> <vue-feather :type="isCustomSet ? 'trash-2' : 'rotate-ccw'" aria-hidden="true"/> </button> </th> <th> - <button class="icon" title="Close" @click="close"> + <button class="icon" title="Close" @click="emit('close')"> <vue-feather type="x" aria-hidden="true"/> </button> </th> @@ -26,7 +26,7 @@ <decimal-input v-model="item.distanceValue" aria-label="Target distance value" :min="0" :digits="2"/> <select v-model="item.distanceUnit" aria-label="Target distance unit"> - <option v-for="(value, key) in distanceUnits" :key="key" :value="key"> + <option v-for="(value, key) in unitUtils.DISTANCE_UNITS" :key="key" :value="key"> {{ value.name }} </option> </select> @@ -67,7 +67,9 @@ </table> </template> -<script> +<script setup> +import { watch, ref } from 'vue'; + import VueFeather from 'vue-feather'; import targetUtils from '@/utils/targets'; @@ -76,126 +78,82 @@ import unitUtils from '@/utils/units'; import DecimalInput from '@/components/DecimalInput.vue'; import TimeInput from '@/components/TimeInput.vue'; -export default { - name: 'TargetEditor', - - components: { - DecimalInput, - TimeInput, - VueFeather, +/** + * The component value + */ +const model = defineModel({ + type: Object, + default: JSON.parse(JSON.stringify(targetUtils.defaultTargetSet)), +}); + +const props = defineProps({ + /** + * Whether the target set is a custom or default set + */ + isCustomSet: { + type: Boolean, + default: false, }, - props: { - /** - * The targets - */ - modelValue: { - type: Object, - default: JSON.parse(JSON.stringify(targetUtils.defaultTargetSet)), - }, - - /** - * Whether the target set is a custom or default set - */ - isCustomSet: { - type: Boolean, - default: false, - }, - - /** - * The unit system to use when creating distance targets - */ - defaultUnitSystem: { - type: String, - default: 'metric', - }, - }, - - data() { - return { - /** - * The internal value - */ - internalValue: this.modelValue, - - /** - * The distance units - */ - distanceUnits: unitUtils.DISTANCE_UNITS, - }; + /** + * The unit system to use when creating distance targets + */ + defaultUnitSystem: { + type: String, + default: 'metric', }, +}); + +// Declare emitted events +const emit = defineEmits(['revert', 'close']); + +/** + * The internal value + */ +const internalValue = ref(model.value); + +/** + * Update the internal value when the component value changes + */ +watch(model, (newValue) => { + internalValue.value = newValue; +}, { deep: true }); + +/** + * Update the component value when the internal value changes + */ +watch(internalValue, (newValue) => { + model.value = newValue; +}, { deep: true }); + +/** + * Add a new distance based target + */ +function addDistanceTarget() { + internalValue.value.targets.push({ + result: 'time', + distanceValue: 1, + distanceUnit: unitUtils.getDefaultDistanceUnit(props.defaultUnitSystem), + }); +} - watch: { - /** - * Update the component value when the modelValue prop changes - * @param {Number} newValue The new prop value - */ - modelValue: { - deep: true, - handler(newValue) { - this.internalValue = newValue; - }, - }, - - /** - * Emit the input event when the component value changes - * @param {Number} newValue The new component value - */ - internalValue: { - deep: true, - handler(newValue) { - this.$emit('update:modelValue', newValue); - }, - }, - }, +/** + * Add a new time based target + */ +function addTimeTarget() { + internalValue.value.targets.push({ + result: 'distance', + time: 600, + }); +} - methods: { - /** - * Revert the target set - */ - revert() { - // Emit revert event - this.$emit('revert'); - }, - - /** - * Close the target editor - */ - close() { - // Emit close event - this.$emit('close'); - }, - - /** - * Add a new distance based target - */ - addDistanceTarget() { - this.internalValue.targets.push({ - result: 'time', - distanceValue: 1, - distanceUnit: unitUtils.getDefaultDistanceUnit(this.defaultUnitSystem), - }); - }, - - /** - * Add a new time based target - */ - addTimeTarget() { - this.internalValue.targets.push({ - result: 'distance', - time: 600, - }); - }, - - /** - * Remove a target - * @param {Number} index The index of the target - */ - removeTarget(index) { - this.internalValue.targets.splice(index, 1); - }, - }, -}; +/** + * Remove a target + * @param {Number} index The index of the target + */ +function removeTarget(index) { + internalValue.value.targets.splice(index, 1); +} </script> <style scoped> diff --git a/src/components/TargetSetSelector.vue b/src/components/TargetSetSelector.vue @@ -8,19 +8,21 @@ </select> <button class="icon" title="Edit target set" - @click="reloadTargetSets(); sortTargetSet(); $refs.dialog.showModal()"> + @click="reloadTargetSets(); sortTargetSet(); dialogElement.showModal()"> <vue-feather type="edit" aria-hidden="true"/> </button> - <dialog ref="dialog" class="target-set-editor-dialog" aria-label="Edit target set"> - <target-editor @close="sortTargetSet(); $refs.dialog.close()" v-model="targetSets[internalValue]" + <dialog ref="dialogElement" class="target-set-editor-dialog" aria-label="Edit target set"> + <target-editor @close="sortTargetSet(); dialogElement.close()" v-model="targetSets[internalValue]" @revert="revertTargetSet" :default-unit-system="defaultUnitSystem" :isCustomSet="!internalValue.startsWith('_')"/> </dialog> </span> </template> -<script> +<script setup> +import { onActivated, ref, watch } from 'vue'; + import VueFeather from 'vue-feather'; import storage from '@/utils/localStorage'; @@ -28,126 +30,111 @@ import targetUtils from '@/utils/targets'; import TargetEditor from '@/components/TargetEditor.vue'; -export default { - name: 'TargetSetSelector', - - components: { - TargetEditor, - VueFeather, - }, - - props: { - /** - * The selected target set - */ - modelValue: { - type: String, - default: '_new', - }, - - /** - * The unit system to use when creating distance targets - */ - defaultUnitSystem: { - type: String, - default: 'metric', - }, +/** + * The selected target set + */ +const model = defineModel({ + type: String, + default: '_new', +}); + +const props = defineProps({ + /** + * The unit system to use when creating distance targets + */ + defaultUnitSystem: { + type: String, + default: 'metric', }, - - data() { - return { - /** - * The internal value - */ - internalValue: this.modelValue, - - /** - * The target sets - */ - targetSets: storage.get('target-sets', targetUtils.defaultTargetSets), +}); + +// Declare emitted events +const emit = defineEmits(['targets-updated']); + +/** + * The internal value + */ +const internalValue = ref(model.value); + +/** + * The target sets + */ +const targetSets = ref(storage.get('target-sets', targetUtils.defaultTargetSets)); + +/** + * The dialog element + */ +const dialogElement = ref(null); + +/** + * Update the internal value when the component value changes + */ +watch(model, (newValue) => { + if (newValue !== internalValue.value) { + internalValue.value = newValue; + } +}); + +/** + * Update the component vvalue when the internal value changes and create a new set if necessary + */ +watch(internalValue, (newValue) => { + if (newValue == '_new') { + reloadTargetSets(); + let key = Date.now().toString(); + targetSets.value[key] = { + name: 'New target set', + targets: [], }; - }, - - watch: { - /** - * Update the component value when the modelValue prop changes - */ - modelValue(newValue) { - if (newValue !== this.internalValue) { - this.internalValue = newValue; - } - }, - - /** - * Emit the input event when the component value changes and create a new set if necessary - */ - internalValue: { - immediate: true, - handler(newValue) { - if (newValue == '_new') { - this.reloadTargetSets(); - let key = Date.now().toString(); - this.targetSets[key] = { - name: 'New target set', - targets: [], - }; - this.internalValue = key; - } else { - this.$emit('update:modelValue', newValue); - } - }, - }, - - /** - * Save target sets - */ - targetSets: { - deep: true, - handler(newValue) { - storage.set('target-sets', newValue); - this.$emit('targets-updated'); - }, - }, - }, + internalValue.value = key; + } else { + model.value = newValue; + } +}, { immediate: true }); + +/** + * Save target sets + */ +watch(targetSets, (newValue) => { + storage.set('target-sets', newValue); + emit('targets-updated'); +}, { deep: true }); + +/** + * Revert or remove the current target set + */ +function revertTargetSet() { + if (internalValue.value.startsWith('_')) { + // Revert default set + targetSets.value[internalValue.value] = + JSON.parse(JSON.stringify(targetUtils.defaultTargetSets[internalValue.value])); + sortTargetSet(); + } else { + // Remove custom set + delete targetSets.value[internalValue.value]; + internalValue.value = [...Object.keys(targetSets.value), '_new'][0]; + if (dialogElement.value.close) dialogElement.value.close(); + } +}; - methods: { - /** - * Revert or remove the current target set - */ - revertTargetSet() { - if (this.internalValue.startsWith('_')) { - // Revert default set - this.targetSets[this.internalValue] = - JSON.parse(JSON.stringify(targetUtils.defaultTargetSets[this.internalValue])); - this.sortTargetSet(); - } else { - // Remove custom set - delete this.targetSets[this.internalValue]; - this.internalValue = [...Object.keys(this.targetSets), '_new'][0]; - if (this.$refs.dialog.close) this.$refs.dialog.close(); - } - }, - - /** - * Sort the current target set - */ - sortTargetSet() { - this.targetSets[this.internalValue].targets = - targetUtils.sort(this.targetSets[this.internalValue].targets); - }, - - /** - * Reload the target sets - */ - reloadTargetSets() { - this.targetSets = storage.get('target-sets', targetUtils.defaultTargetSets); - }, - }, +/** + * Sort the current target set + */ +function sortTargetSet() { + targetSets.value[internalValue.value].targets = + targetUtils.sort(targetSets.value[internalValue.value].targets); +}; - activated() { - this.reloadTargetSets(); - }, +/** + * Reload the target sets + */ +function reloadTargetSets() { + targetSets.value = storage.get('target-sets', targetUtils.defaultTargetSets); }; + +onActivated(() => { + reloadTargetSets(); +}); </script> <style scoped> diff --git a/src/components/TimeInput.vue b/src/components/TimeInput.vue @@ -13,145 +13,126 @@ </div> </template> -<script> +<script setup> +import { computed, ref, watch } from 'vue'; + import IntegerInput from '@/components/IntegerInput.vue'; import DecimalInput from '@/components/DecimalInput.vue'; -export default { - name: 'TimeInput', - - components: { - IntegerInput, - DecimalInput, +/** + * The component value + */ +const model = defineModel({ + type: Number, + default: 0, + validator(value) { + return value >= 0 && value <= 359999.99; }, +}); - props: { - /** - * The input value - */ - modelValue: { - type: Number, - default: 0, - validator(value) { - return value >= 0 && value <= 359999.99; - }, - }, - - /** - * Whether to show the hour field - */ - showHours: { - type: Boolean, - default: true, - }, - - /** - * The prefix for each field's aria-label - */ - label: { - type: String, - default: '', - }, +const props = defineProps({ + /** + * Whether to show the hour field + */ + showHours: { + type: Boolean, + default: true, }, - data() { - return { - /** - * The internal value - */ - internalValue: this.modelValue, - }; + /** + * The prefix for each field's aria-label + */ + label: { + type: String, + default: '', }, +}); - computed: { - /** - * The maximum value - */ - max() { - return this.showHours ? 359999.99 : 3599.99; - }, +/** + * The internal value + */ +const internalValue = ref(model.value); - /** - * The value of the hours field - */ - hours: { - get() { - return Math.floor(this.modelValue / 3600); - }, - set(newValue) { - this.internalValue = (newValue * 3600) + (this.minutes * 60) + this.seconds; - }, - }, +/** + * The maximum value + */ +const max = computed(() => { + return props.showHours ? 359999.99 : 3599.99; +}); - /** - * The value of the minutes field - */ - minutes: { - get() { - return Math.floor((this.modelValue % 3600) / 60); - }, - set(newValue) { - this.internalValue = (this.hours * 3600) + (newValue * 60) + this.seconds; - }, - }, - - /** - * The value of the seconds field - */ - seconds: { - get() { - return this.modelValue % 60; - }, - set(newValue) { - this.internalValue = (this.hours * 3600) + (this.minutes * 60) + newValue; - }, - }, +/** + * The value of the hours field + */ +const hours = computed({ + get() { + return Math.floor(model.value / 3600); }, + set(newValue) { + internalValue.value = (newValue * 3600) + (minutes.value * 60) + seconds.value; + }, +}); - watch: { - /** - * Update the component value when the modelValue prop changes - * @param {Number} newValue The new prop value - */ - modelValue(newValue) { - if (newValue !== this.internalValue) { - this.internalValue = newValue; - } - }, - - /** - * Emit the input event when the component value changes - * @param {Number} newValue The new component value - */ - internalValue(newValue) { - this.$emit('update:modelValue', newValue); - }, +/** + * The value of the minutes field + */ +const minutes = computed({ + get() { + return Math.floor((model.value % 3600) / 60); }, + set(newValue) { + internalValue.value = (hours.value * 3600) + (newValue * 60) + seconds.value; + }, +}); - methods: { - /** - * Process up and down arrow presses - * @param {Object} e The keydown event args - */ - onkeydown(e, step = 1) { - if (e.key === 'ArrowUp') { - if (Math.floor(this.internalValue) + step > this.max) { - this.internalValue = this.max; - } else { - this.internalValue = Math.floor(this.internalValue) + step; - } - e.preventDefault(); - } else if (e.key === 'ArrowDown') { - if (Math.ceil(this.internalValue) - step < 0) { - this.internalValue = 0; - } else { - this.internalValue = Math.ceil(this.internalValue) - step; - } - e.preventDefault(); - } - }, +/** + * The value of the seconds field + */ +const seconds = computed({ + get() { + return model.value % 60; + }, + set(newValue) { + internalValue.value = (hours.value * 3600) + (minutes.value * 60) + newValue; }, -}; +}); + +/** + * Update the internal value when the component value changes + */ +watch(model, (newValue) => { + if (newValue !== internalValue.value) { + internalValue.value = newValue; + } +}); + +/** + * Update the component value when the internal value changes + */ +watch(internalValue, (newValue) => { + model.value = newValue; +}); + +/** + * Process up and down arrow presses + * @param {Object} e The keydown event args + */ +function onkeydown(e, step = 1) { + if (e.key === 'ArrowUp') { + if (Math.floor(internalValue.value) + step > max.value) { + internalValue.value = max.value; + } else { + internalValue.value = Math.floor(internalValue.value) + step; + } + e.preventDefault(); + } else if (e.key === 'ArrowDown') { + if (Math.ceil(internalValue.value) - step < 0) { + internalValue.value = 0; + } else { + internalValue.value = Math.ceil(internalValue.value) - step; + } + e.preventDefault(); + } +} </script> <style scoped> diff --git a/src/views/AboutPage.vue b/src/views/AboutPage.vue @@ -123,25 +123,12 @@ </div> </template> -<script> +<script setup> import { version } from '/package.json'; import VueFeather from 'vue-feather'; -export default { - name: 'AboutPage', - - components: { - VueFeather - }, - - data() { - return { - version, - development: process.env.NODE_ENV === 'development', - }; - }, -}; +const development = process.env.NODE_ENV === 'development'; </script> <style scoped> diff --git a/src/views/HomePage.vue b/src/views/HomePage.vue @@ -33,12 +33,6 @@ </div> </template> -<script> -export default { - name: 'HomePage', -}; -</script> - <style scoped> .home-page { text-align: center; diff --git a/src/views/NotFoundPage.vue b/src/views/NotFoundPage.vue @@ -5,12 +5,6 @@ </div> </template> -<script> -export default { - name: 'NotFoundPage', -}; -</script> - <style scoped> h1 { font-size: 1.5em; diff --git a/src/views/PaceCalculator.vue b/src/views/PaceCalculator.vue @@ -7,7 +7,7 @@ <decimal-input v-model="inputDistance" aria-label="Input distance value" :min="0" :digits="2"/> <select v-model="inputUnit" aria-label="Input distance unit"> - <option v-for="(value, key) in distanceUnits" :key="key" :value="key"> + <option v-for="(value, key) in unitUtils.DISTANCE_UNITS" :key="key" :value="key"> {{ value.name }} </option> </select> @@ -42,7 +42,9 @@ </div> </template> -<script> +<script setup> +import { computed, onActivated, ref, watch } from 'vue'; + import paceUtils from '@/utils/paces'; import storage from '@/utils/localStorage'; import targetUtils from '@/utils/targets'; @@ -53,164 +55,138 @@ import SimpleTargetTable from '@/components/SimpleTargetTable.vue'; import TargetSetSelector from '@/components/TargetSetSelector.vue'; import TimeInput from '@/components/TimeInput.vue'; -export default { - name: 'PaceCalculator', - - components: { - DecimalInput, - SimpleTargetTable, - TargetSetSelector, - TimeInput, - }, - - data() { - return { - /** - * The input distance value - */ - inputDistance: storage.get('pace-calculator-input-distance', 5), - - /** - * The input distance unit - */ - inputUnit: storage.get('pace-calculator-input-unit', 'kilometers'), - - /** - * The input time value - */ - inputTime: storage.get('pace-calculator-input-time', 20 * 60), - - /** - * The default unit system - * - * Loaded in activate() method - */ - defaultUnitSystem: null, - - /** - * The names of the distance units - */ - distanceUnits: unitUtils.DISTANCE_UNITS, - - /** - * The current selected target set - */ - selectedTargetSet: storage.get('pace-calculator-target-set', '_pace_targets'), - - /** - * The target sets - * - * Loaded in activate() method - */ - targetSets: {}, - }; - }, - - watch: { - /** - * Save input distance value - */ - inputDistance(newValue) { - storage.set('pace-calculator-input-distance', newValue); - }, - - /** - * Save input distance unit - */ - inputUnit(newValue) { - storage.set('pace-calculator-input-unit', newValue); - }, - - /** - * Save input time value - */ - inputTime(newValue) { - storage.set('pace-calculator-input-time', newValue); - }, - - /** - * Save default unit system - */ - defaultUnitSystem(newValue) { - storage.set('default-unit-system', newValue); - }, - - /** - * Save the current selected target set - */ - selectedTargetSet(newValue) { - storage.set('pace-calculator-target-set', newValue); - }, - }, - - computed: { - /** - * The input pace (in seconds per meter) - */ - pace() { - const distance = unitUtils.convertDistance(this.inputDistance, this.inputUnit, 'meters'); - return paceUtils.getPace(distance, this.inputTime); - }, - }, - - methods: { - /** - * Reload the target sets - */ - reloadTargets() { - this.targetSets = storage.get('target-sets', targetUtils.defaultTargetSets); - }, - - /** - * Calculate paces from a target - * @param {Object} target The target - * @returns {Object} The result - */ - calculatePace(target) { - // Initialize result - const result = { - distanceValue: target.distanceValue, - distanceUnit: target.distanceUnit, - time: target.time, - result: target.result, - }; - - // Add missing value to result - if (target.result === 'time') { - // Convert target distance into meters - const d2 = unitUtils.convertDistance(target.distanceValue, target.distanceUnit, 'meters'); - - // Calculate time to travel distance at input pace - const time = paceUtils.getTime(this.pace, d2); - - // Update result - result.time = time; - } else { - // Calculate distance traveled in time at input pace - let distance = paceUtils.getDistance(this.pace, target.time); - - // Convert output distance into default distance unit - distance = unitUtils.convertDistance(distance, 'meters', - unitUtils.getDefaultDistanceUnit(this.defaultUnitSystem)); - - // Update result - result.distanceValue = distance; - result.distanceUnit = unitUtils.getDefaultDistanceUnit(this.defaultUnitSystem); - } - - // Return result - return result; - }, - }, - - /** - * (Re)load settings used in multiple calculators - */ - activated() { - this.targetSets = storage.get('target-sets', targetUtils.defaultTargetSets); - this.defaultUnitSystem = storage.get('default-unit-system', unitUtils.detectDefaultUnitSystem()); - }, -}; +/** + * The input distance value + */ +const inputDistance = ref(storage.get('pace-calculator-input-distance', 5)); + +/** + * The input distance unit + */ +const inputUnit = ref(storage.get('pace-calculator-input-unit', 'kilometers')); + +/** + * The input time value + */ +const inputTime = ref(storage.get('pace-calculator-input-time', 20 * 60)); + +/** + * The default unit system + * + * Loaded in onActivated() hook + */ +const defaultUnitSystem = ref(null); + +/** + * The current selected target set + */ +const selectedTargetSet = ref(storage.get('pace-calculator-target-set', '_pace_targets')); + +/** + * The target sets + * + * Loaded in onActivated() hook + */ +const targetSets = ref({}); + +/** + * Save input distance value + */ +watch(inputDistance, (newValue) => { + storage.set('pace-calculator-input-distance', newValue); +}); + +/** + * Save input distance unit + */ +watch(inputUnit, (newValue) => { + storage.set('pace-calculator-input-unit', newValue); +}); + +/** + * Save input time value + */ +watch(inputTime, (newValue) => { + storage.set('pace-calculator-input-time', newValue); +}); + +/** + * Save default unit system + */ +watch(defaultUnitSystem, (newValue) => { + storage.set('default-unit-system', newValue); +}); + +/** + * Save the current selected target set + */ +watch(selectedTargetSet, (newValue) => { + storage.set('pace-calculator-target-set', newValue); +}); + +/** + * The input pace (in seconds per meter) + */ +const pace = computed(() => { + const distance = unitUtils.convertDistance(inputDistance.value, inputUnit.value, 'meters'); + return paceUtils.getPace(distance, inputTime.value); +}); + +/** + * Reload the target sets + */ +function reloadTargets() { + targetSets.value = storage.get('target-sets', targetUtils.defaultTargetSets); +} + +/** + * Calculate paces from a target + * @param {Object} target The target + * @returns {Object} The result + */ +function calculatePace(target) { + // Initialize result + const result = { + distanceValue: target.distanceValue, + distanceUnit: target.distanceUnit, + time: target.time, + result: target.result, + }; + + // Add missing value to result + if (target.result === 'time') { + // Convert target distance into meters + const d2 = unitUtils.convertDistance(target.distanceValue, target.distanceUnit, 'meters'); + + // Calculate time to travel distance at input pace + const time = paceUtils.getTime(pace.value, d2); + + // Update result + result.time = time; + } else { + // Calculate distance traveled in time at input pace + let distance = paceUtils.getDistance(pace.value, target.time); + + // Convert output distance into default distance unit + distance = unitUtils.convertDistance(distance, 'meters', + unitUtils.getDefaultDistanceUnit(defaultUnitSystem.value)); + + // Update result + result.distanceValue = distance; + result.distanceUnit = unitUtils.getDefaultDistanceUnit(defaultUnitSystem.value); + } + + // Return result + return result; +} + +/** + * (Re)load settings used in multiple calculators + */ +onActivated(() => { + targetSets.value = storage.get('target-sets', targetUtils.defaultTargetSets); + defaultUnitSystem.value = storage.get('default-unit-system', unitUtils.detectDefaultUnitSystem()); +}); </script> <style scoped> diff --git a/src/views/RaceCalculator.vue b/src/views/RaceCalculator.vue @@ -6,7 +6,7 @@ Distance: <decimal-input v-model="inputDistance" aria-label="Input distance value" :min="0" :digits="2"/> <select v-model="inputUnit" aria-label="Input distance unit"> - <option v-for="(value, key) in distanceUnits" :key="key" :value="key"> + <option v-for="(value, key) in unitUtils.DISTANCE_UNITS" :key="key" :value="key"> {{ value.name }} </option> </select> @@ -22,14 +22,14 @@ <h2>Race Statistics</h2> </summary> <div> - Purdy Points: <b>{{ formatNumber(purdyPoints, 0, 1, true) }}</b> + Purdy Points: <b>{{ formatUtils.formatNumber(purdyPoints, 0, 1, true) }}</b> </div> <div> - V&#775;O&#8322;: <b>{{ formatNumber(vo2, 0, 1, true) }}</b> ml/kg/min - (<b>{{ formatNumber(vo2Percentage, 0, 1, true) }}%</b> of max) + V&#775;O&#8322;: <b>{{ formatUtils.formatNumber(vo2, 0, 1, true) }}</b> ml/kg/min + (<b>{{ formatUtils.formatNumber(vo2Percentage, 0, 1, true) }}%</b> of max) </div> <div> - V&#775;O&#8322; Max: <b>{{ formatNumber(vo2Max, 0, 1, true) }}</b> ml/kg/min + V&#775;O&#8322; Max: <b>{{ formatUtils.formatNumber(vo2Max, 0, 1, true) }}</b> ml/kg/min </div> </details> @@ -73,7 +73,9 @@ </div> </template> -<script> +<script setup> +import { computed, onActivated, ref, watch } from 'vue'; + import formatUtils from '@/utils/format'; import raceUtils from '@/utils/races'; import storage from '@/utils/localStorage'; @@ -85,265 +87,234 @@ import SimpleTargetTable from '@/components/SimpleTargetTable.vue'; import TargetSetSelector from '@/components/TargetSetSelector.vue'; import TimeInput from '@/components/TimeInput.vue'; -export default { - name: 'RaceCalculator', - - components: { - DecimalInput, - SimpleTargetTable, - TargetSetSelector, - TimeInput, - }, - - data() { - return { - /** - * The input distance value - */ - inputDistance: storage.get('race-calculator-input-distance', 5), - - /** - * The input distance unit - */ - inputUnit: storage.get('race-calculator-input-unit', 'kilometers'), - - /** - * The input time value - */ - inputTime: storage.get('race-calculator-input-time', 20 * 60), - - /** - * The default unit system - * - * Loaded in activate() method - */ - defaultUnitSystem: null, - - /** - * The race prediction model - */ - model: storage.get('race-calculator-model', 'AverageModel'), - - /** - * The value of the exponent in Riegel's Model - */ - riegelExponent: storage.get('race-calculator-riegel-exponent', 1.06), - - /** - * The names of the distance units - */ - distanceUnits: unitUtils.DISTANCE_UNITS, - - /** - * The formatNumber method - */ - formatNumber: formatUtils.formatNumber, - - /** - * The current selected target set - */ - selectedTargetSet: storage.get('race-calculator-target-set', '_race_targets'), - - /** - * The target sets - * - * Loaded in activate() method - */ - targetSets: {}, - }; - }, - - methods: { - /** - * Reload the target sets - */ - reloadTargets() { - this.targetSets = storage.get('target-sets', targetUtils.defaultTargetSets); - }, - - /** - * Predict race results from a target - * @param {Object} target The target - * @returns {Object} The result - */ - predictResult(target) { - // Initialize result - const result = { - distanceValue: target.distanceValue, - distanceUnit: target.distanceUnit, - time: target.time, - result: target.result, - }; - - // Add missing value to result - if (target.result === 'time') { - // Convert target distance into meters - const d2 = unitUtils.convertDistance(target.distanceValue, target.distanceUnit, 'meters'); - - // Get prediction - let time; - switch (this.model) { - default: - case 'AverageModel': - time = raceUtils.AverageModel.predictTime(this.d1, this.inputTime, d2, - this.riegelExponent); - break; - case 'PurdyPointsModel': - time = raceUtils.PurdyPointsModel.predictTime(this.d1, this.inputTime, d2); - break; - case 'VO2MaxModel': - time = raceUtils.VO2MaxModel.predictTime(this.d1, this.inputTime, d2); - break; - case 'RiegelModel': - time = raceUtils.RiegelModel.predictTime(this.d1, this.inputTime, d2, - this.riegelExponent); - break; - case 'CameronModel': - time = raceUtils.CameronModel.predictTime(this.d1, this.inputTime, d2); - break; - } - - // Update result - result.time = time; - } else { - // Get prediction - let distance; - switch (this.model) { - default: - case 'AverageModel': - distance = raceUtils.AverageModel.predictDistance(this.inputTime, this.d1, target.time, - this.riegelExponent); - break; - case 'PurdyPointsModel': - distance = raceUtils.PurdyPointsModel.predictDistance(this.inputTime, this.d1, - target.time); - break; - case 'VO2MaxModel': - distance = raceUtils.VO2MaxModel.predictDistance(this.inputTime, this.d1, target.time); - break; - case 'RiegelModel': - distance = raceUtils.RiegelModel.predictDistance(this.inputTime, this.d1, target.time, - this.riegelExponent); - break; - case 'CameronModel': - distance = raceUtils.CameronModel.predictDistance(this.inputTime, this.d1, target.time); - break; - } - - // Convert output distance into default distance unit - distance = unitUtils.convertDistance(distance, 'meters', - unitUtils.getDefaultDistanceUnit(this.defaultUnitSystem)); - - // Update result - result.distanceValue = distance; - result.distanceUnit = unitUtils.getDefaultDistanceUnit(this.defaultUnitSystem); - } - - // Return result - return result; - }, - }, - - computed: { - /** - * The input distance in meters - */ - d1() { - return unitUtils.convertDistance(this.inputDistance, this.inputUnit, 'meters'); - }, - - /** - * The Purdy Points for the input race - */ - purdyPoints() { - const result = raceUtils.PurdyPointsModel.getPurdyPoints(this.d1, this.inputTime); - return result; - }, - - /** - * The VO2 Max calculated from the input race - */ - vo2Max() { - const result = raceUtils.VO2MaxModel.getVO2Max(this.d1, this.inputTime); - return result; - }, - - /** - * The VO2 calculated from the input race - */ - vo2() { - const result = raceUtils.VO2MaxModel.getVO2(this.d1, this.inputTime); - return result; - }, - - /** - * The percentage of VO2 Max calculated from the input race - */ - vo2Percentage() { - const result = raceUtils.VO2MaxModel.getVO2Percentage(this.inputTime) * 100; - return result; - }, - }, - - watch: { - /** - * Save input distance value - */ - inputDistance(newValue) { - storage.set('race-calculator-input-distance', newValue); - }, - - /** - * Save input distance unit - */ - inputUnit(newValue) { - storage.set('race-calculator-input-unit', newValue); - }, - - /** - * Save input time value - */ - inputTime(newValue) { - storage.set('race-calculator-input-time', newValue); - }, - - /** - * Save default unit system - */ - defaultUnitSystem(newValue) { - storage.set('default-unit-system', newValue); - }, - - /** - * Save prediction model - */ - model(newValue) { - storage.set('race-calculator-model', newValue); - }, - - /** - * Save Riegel Model exponent - */ - riegelExponent(newValue) { - storage.set('race-calculator-riegel-exponent', newValue); - }, - - /** - * Save the current selected target set - */ - selectedTargetSet(newValue) { - storage.set('race-calculator-target-set', newValue); - }, - }, - - /** - * (Re)load settings used in multiple calculators - */ - activated() { - this.targetSets = storage.get('target-sets', targetUtils.defaultTargetSets); - this.defaultUnitSystem = storage.get('default-unit-system', unitUtils.detectDefaultUnitSystem()); - }, -}; +/** + * The input distance value + */ +const inputDistance = ref(storage.get('race-calculator-input-distance', 5)); + +/** + * The input distance unit + */ +const inputUnit = ref(storage.get('race-calculator-input-unit', 'kilometers')); + +/** + * The input time value + */ +const inputTime = ref(storage.get('race-calculator-input-time', 20 * 60)); + +/** + * The default unit system + * + * Loaded in onActivated() hook + */ +const defaultUnitSystem = ref(null); + +/** +* The race prediction model +*/ +const model = ref(storage.get('race-calculator-model', 'AverageModel')); + +/** +* The value of the exponent in Riegel's Model +*/ +const riegelExponent = ref(storage.get('race-calculator-riegel-exponent', 1.06)); + +/** + * The current selected target set + */ +const selectedTargetSet = ref(storage.get('race-calculator-target-set', '_race_targets')); + +/** + * The target sets + * + * Loaded in onActivated() hook + */ +let targetSets = ref({}); + +/** + * Reload the target sets + */ +function reloadTargets() { + targetSets.value = storage.get('target-sets', targetUtils.defaultTargetSets); +} + +/** + * Predict race results from a target + * @param {Object} target The target + * @returns {Object} The result + */ +function predictResult(target) { + // Initialize result + const result = { + distanceValue: target.distanceValue, + distanceUnit: target.distanceUnit, + time: target.time, + result: target.result, + }; + + // Add missing value to result + if (target.result === 'time') { + // Convert target distance into meters + const d2 = unitUtils.convertDistance(target.distanceValue, target.distanceUnit, 'meters'); + + // Get prediction + let time; + switch (model.value) { + default: + case 'AverageModel': + time = raceUtils.AverageModel.predictTime(d1.value, inputTime.value, d2, + riegelExponent.value); + break; + case 'PurdyPointsModel': + time = raceUtils.PurdyPointsModel.predictTime(d1.value, inputTime.value, d2); + break; + case 'VO2MaxModel': + time = raceUtils.VO2MaxModel.predictTime(d1.value, inputTime.value, d2); + break; + case 'RiegelModel': + time = raceUtils.RiegelModel.predictTime(d1.value, inputTime.value, d2, + riegelExponent.value); + break; + case 'CameronModel': + time = raceUtils.CameronModel.predictTime(d1.value, inputTime.value, d2); + break; + } + + // Update result + result.time = time; + } else { + // Get prediction + let distance; + switch (model.value) { + default: + case 'AverageModel': + distance = raceUtils.AverageModel.predictDistance(inputTime.value, d1.value, target.time, + riegelExponent.value); + break; + case 'PurdyPointsModel': + distance = raceUtils.PurdyPointsModel.predictDistance(inputTime.value, d1.value, + target.time); + break; + case 'VO2MaxModel': + distance = raceUtils.VO2MaxModel.predictDistance(inputTime.value, d1.value, target.time); + break; + case 'RiegelModel': + distance = raceUtils.RiegelModel.predictDistance(inputTime.value, d1.value, target.time, + riegelExponent.value); + break; + case 'CameronModel': + distance = raceUtils.CameronModel.predictDistance(inputTime.value, d1.value, target.time); + break; + } + + // Convert output distance into default distance unit + distance = unitUtils.convertDistance(distance, 'meters', + unitUtils.getDefaultDistanceUnit(defaultUnitSystem.value)); + + // Update result + result.distanceValue = distance; + result.distanceUnit = unitUtils.getDefaultDistanceUnit(defaultUnitSystem.value); + } + + // Return result + return result; +} + +/** + * The input distance in meters + */ +const d1 = computed(() => { + return unitUtils.convertDistance(inputDistance.value, inputUnit.value, 'meters'); +}); + +/** + * The Purdy Points for the input race + */ +const purdyPoints = computed(() => { + const result = raceUtils.PurdyPointsModel.getPurdyPoints(d1.value, inputTime.value); + return result; +}); + +/** + * The VO2 Max calculated from the input race + */ +const vo2Max = computed(() => { + const result = raceUtils.VO2MaxModel.getVO2Max(d1.value, inputTime.value); + return result; +}); + +/** + * The VO2 calculated from the input race + */ +const vo2 = computed(() => { + const result = raceUtils.VO2MaxModel.getVO2(d1.value, inputTime.value); + return result; +}); + +/** + * The percentage of VO2 Max calculated from the input race + */ +const vo2Percentage = computed(() => { + const result = raceUtils.VO2MaxModel.getVO2Percentage(inputTime.value) * 100; + return result; +}); + +/** + * Save input distance value + */ +watch(inputDistance, (newValue) => { + storage.set('race-calculator-input-distance', newValue); +}); + +/** + * Save input distance unit + */ +watch(inputUnit, (newValue) => { + storage.set('race-calculator-input-unit', newValue); +}); + +/** + * Save input time value + */ +watch(inputTime, (newValue) => { + storage.set('race-calculator-input-time', newValue); +}); + +/** + * Save default unit system + */ +watch(defaultUnitSystem, (newValue) => { + storage.set('default-unit-system', newValue); +}); + +/** + * Save prediction model + */ +watch(model, (newValue) => { + storage.set('race-calculator-model', newValue); +}); + +/** + * Save Riegel Model exponent + */ +watch(riegelExponent, (newValue) => { + storage.set('race-calculator-riegel-exponent', newValue); +}); + +/** + * Save the current selected target set + */ +watch(selectedTargetSet, (newValue) => { + storage.set('race-calculator-target-set', newValue); +}); + +/** +* (Re)load settings used in multiple calculators +*/ +onActivated(() => { + targetSets.value = storage.get('target-sets', targetUtils.defaultTargetSets); + defaultUnitSystem.value = storage.get('default-unit-system', unitUtils.detectDefaultUnitSystem()); +}); </script> <style scoped> diff --git a/src/views/SplitCalculator.vue b/src/views/SplitCalculator.vue @@ -34,12 +34,12 @@ <tbody> <tr v-for="(item, index) in results" :key="index"> <td> - {{ formatNumber(item.distanceValue, 0, 2, false) }} - {{ distanceUnits[item.distanceUnit].symbol }} + {{ formatUtils.formatNumber(item.distanceValue, 0, 2, false) }} + {{ unitUtils.DISTANCE_UNITS[item.distanceUnit].symbol }} </td> <td> - {{ formatDuration(item.totalTime, 3, 2, true) }} + {{ formatUtils.formatDuration(item.totalTime, 3, 2, true) }} </td> <td v-if="targetSets[selectedTargetSet]"> @@ -48,8 +48,9 @@ </td> <td> - {{ formatDuration(item.pace, 3, 0, true) }} - / {{ distanceUnits[getDefaultDistanceUnit(defaultUnitSystem)].symbol }} + {{ formatUtils.formatDuration(item.pace, 3, 0, true) }} + / {{ unitUtils.DISTANCE_UNITS[unitUtils.getDefaultDistanceUnit(defaultUnitSystem)] + .symbol }} </td> </tr> @@ -64,7 +65,9 @@ </div> </template> -<script> +<script setup> +import { computed, onActivated, ref, watch } from 'vue'; + import formatUtils from '@/utils/format'; import storage from '@/utils/localStorage'; import targetUtils from '@/utils/targets'; @@ -73,146 +76,104 @@ import unitUtils from '@/utils/units'; import TargetSetSelector from '@/components/TargetSetSelector.vue'; import TimeInput from '@/components/TimeInput.vue'; -export default { - name: 'SplitCalculator', - - components: { - TargetSetSelector, - TimeInput, - }, - - data() { - return { - /** - * The default unit system - * - * Loaded in activate() method - */ - defaultUnitSystem: null, - - /** - * The distance units - */ - distanceUnits: unitUtils.DISTANCE_UNITS, - - /** - * The formatDuration method - */ - formatDuration: formatUtils.formatDuration, - - /** - * The formatNumber method - */ - formatNumber: formatUtils.formatNumber, - - /** - * The getDefaultDistanceUnit method - */ - getDefaultDistanceUnit: unitUtils.getDefaultDistanceUnit, - - /** - * The current selected target set - */ - selectedTargetSet: storage.get('split-calculator-target-set', '_split_targets'), - - /** - * The default output targets - * - * Loaded in activate() method - */ - targetSets: {}, - }; - }, - - watch: { - /** - * Save default unit system - */ - defaultUnitSystem(newValue) { - storage.set('default-unit-system', newValue); - }, - - /** - * Save the current selected target set - */ - selectedTargetSet(newValue) { - storage.set('split-calculator-target-set', newValue); - }, - - /** - * Save target sets - */ - targetSets: { - deep: true, - handler(newValue) { - storage.set('target-sets', newValue); - }, - }, - }, - - computed: { - /** - * The target table results - */ - results() { - // Initialize results array - const results = []; - - // Check for missing target set - if (!this.targetSets[this.selectedTargetSet]) return []; - - let targets = targetUtils.sort(this.targetSets[this.selectedTargetSet].targets.filter(x => - x.result === 'time')); - - for (let i = 0; i < targets.length; i += 1) { - // Calculate split and total times - const splitTime = targets[i].split || 0; - const totalTime = i === 0 ? splitTime : results[i - 1].totalTime + splitTime; - - // Calculate split and total distances - const totalDistance = unitUtils.convertDistance( - targets[i].distanceValue, - targets[i].distanceUnit, 'meters', - ); - const splitDistance = i === 0 ? totalDistance : totalDistance - results[i - 1].distance; - - // Calculate pace - const pace = splitTime / unitUtils.convertDistance(splitDistance, 'meters', - unitUtils.getDefaultDistanceUnit(this.defaultUnitSystem)); - - // Add row to results array - results.push({ - distance: totalDistance, - distanceValue: targets[i].distanceValue, - distanceUnit: targets[i].distanceUnit, - totalTime, - splitTime, - pace, - }); - } - - // Return results array - return results; - }, - }, - - methods: { - /** - * Reload the target sets - */ - reloadTargets() { - this.targetSets = storage.get('target-sets', targetUtils.defaultTargetSets); - }, - }, - - /** - * (Re)load settings used in multiple calculators - */ - activated() { - this.targetSets = storage.get('target-sets', targetUtils.defaultTargetSets); - this.defaultUnitSystem = storage.get('default-unit-system', unitUtils.detectDefaultUnitSystem()); - }, -}; +/** + * The default unit system + * + * Loaded in onActivated() hook + */ +const defaultUnitSystem = ref(null); + +/** + * The current selected target set + */ +const selectedTargetSet = ref(storage.get('split-calculator-target-set', '_split_targets')); + +/** + * The default output targets + * + * Loaded in onActivated() hook + */ +const targetSets = ref({}); + +/** + * Save default unit system + */ +watch(defaultUnitSystem, (newValue) => { + storage.set('default-unit-system', newValue); +}); + +/** + * Save the current selected target set + */ +watch(selectedTargetSet, (newValue) => { + storage.set('split-calculator-target-set', newValue); +}); + +/** + * Save target sets + */ +watch(targetSets, (newValue) => { + storage.set('target-sets', newValue); +}, { deep: true }); + +/** + * The target table results + */ +const results = computed(() => { + // Initialize results array + const results = []; + + // Check for missing target set + if (!targetSets.value[selectedTargetSet.value]) return []; + + let targets = targetUtils.sort(targetSets.value[selectedTargetSet.value].targets.filter(x => + x.result === 'time')); + + for (let i = 0; i < targets.length; i += 1) { + // Calculate split and total times + const splitTime = targets[i].split || 0; + const totalTime = i === 0 ? splitTime : results[i - 1].totalTime + splitTime; + + // Calculate split and total distances + const totalDistance = unitUtils.convertDistance( + targets[i].distanceValue, + targets[i].distanceUnit, 'meters', + ); + const splitDistance = i === 0 ? totalDistance : totalDistance - results[i - 1].distance; + + // Calculate pace + const pace = splitTime / unitUtils.convertDistance(splitDistance, 'meters', + unitUtils.getDefaultDistanceUnit(defaultUnitSystem.value)); + + // Add row to results array + results.push({ + distance: totalDistance, + distanceValue: targets[i].distanceValue, + distanceUnit: targets[i].distanceUnit, + totalTime, + splitTime, + pace, + }); + } + + // Return results array + return results; +}); + +/** + * Reload the target sets + */ +function reloadTargets() { + targetSets.value = storage.get('target-sets', targetUtils.defaultTargetSets); +} + +/** + * (Re)load settings used in multiple calculators + */ +onActivated(() => { + targetSets.value = storage.get('target-sets', targetUtils.defaultTargetSets); + defaultUnitSystem.value = storage.get('default-unit-system', unitUtils.detectDefaultUnitSystem()); +}); </script> <style scoped> diff --git a/src/views/UnitCalculator.vue b/src/views/UnitCalculator.vue @@ -20,10 +20,10 @@ <span class="equals"> = </span> <span v-if="getUnitType(outputUnit) === 'time'" class="output-value" aria-label="Output value"> - {{ formatDuration(outputValue, 6, 3, true) }} + {{ formatUtils.formatDuration(outputValue, 6, 3, true) }} </span> <span v-else class="output-value" aria-label="Output value"> - {{ formatNumber(outputValue, 0, 3, true) }} + {{ formatUtils.formatNumber(outputValue, 0, 3, true) }} </span> <select v-model="outputUnit" class="output-units" aria-label="Output units"> @@ -34,7 +34,9 @@ </div> </template> -<script> +<script setup> + import { computed, ref, watch } from 'vue'; + import formatUtils from '@/utils/format'; import storage from '@/utils/localStorage'; import unitUtils from '@/utils/units'; @@ -42,227 +44,198 @@ import unitUtils from '@/utils/units'; import DecimalInput from '@/components/DecimalInput.vue'; import TimeInput from '@/components/TimeInput.vue'; -export default { - name: 'UnitCalculator', - - components: { - DecimalInput, - TimeInput, - }, - - data() { - return { - /** - * The input value - */ - inputValue: storage.get('unit-calculator-distance-input-value', 1.0), - - /** - * The unit of the input - */ - inputUnit: storage.get('unit-calculator-distance-input-unit', 'miles'), +/** + * The input value + */ +const inputValue = ref(storage.get('unit-calculator-distance-input-value', 1.0)); - /** - * The unit of the output - */ - outputUnit: storage.get('unit-calculator-distance-output-unit', 'kilometers'), +/** + * The unit of the input + */ +const inputUnit = ref(storage.get('unit-calculator-distance-input-unit', 'miles')); - /** - * The unit category - */ - category: 'distance', +/** + * The unit of the output + */ +const outputUnit = ref(storage.get('unit-calculator-distance-output-unit', 'kilometers')); - /** - * The formatDuration method - */ - formatDuration: formatUtils.formatDuration, +/** + * The unit category + */ +const category = ref('distance'); - /** - * The formatNumber method - */ - formatNumber: formatUtils.formatNumber, - }; - }, - - computed: { - /** - * The names of the units in the current category - */ - units() { - switch (this.category) { - case 'distance': { - return unitUtils.DISTANCE_UNITS; - } - case 'time': { - return { - ...unitUtils.TIME_UNITS, - 'hh:mm:ss': { - name: 'hh:mm:ss', - symbol: '', - value: null, - }, - }; - } - case 'speed_and_pace': { - return { ...unitUtils.PACE_UNITS, ...unitUtils.SPEED_UNITS }; - } - default: { - return {}; - } - } - }, +/** + * The names of the units in the current category + */ +const units = computed(() => { + switch (category.value) { + case 'distance': { + return unitUtils.DISTANCE_UNITS; + } + case 'time': { + return { + ...unitUtils.TIME_UNITS, + 'hh:mm:ss': { + name: 'hh:mm:ss', + symbol: '', + value: null, + }, + }; + } + case 'speed_and_pace': { + return { ...unitUtils.PACE_UNITS, ...unitUtils.SPEED_UNITS }; + } + default: { + return {}; + } + } +}); - /** - * The output value - */ - outputValue() { - switch (this.category) { - case 'distance': { - return unitUtils.convertDistance(this.inputValue, this.inputUnit, this.outputUnit); - } - case 'time': { - // Correct input and output units for 'hh:mm:ss' unit - const realInput = this.inputUnit === 'hh:mm:ss' ? 'seconds' : this.inputUnit; - const realOutput = this.outputUnit === 'hh:mm:ss' ? 'seconds' : this.outputUnit; +/** + * The output value + */ +const outputValue = computed(() => { + switch (category.value) { + case 'distance': { + return unitUtils.convertDistance(inputValue.value, inputUnit.value, outputUnit.value); + } + case 'time': { + // Correct input and output units for 'hh:mm:ss' unit + const realInput = inputUnit.value === 'hh:mm:ss' ? 'seconds' : inputUnit.value; + const realOutput = outputUnit.value === 'hh:mm:ss' ? 'seconds' : outputUnit.value; - // Calculate conversion - return unitUtils.convertTime(this.inputValue, realInput, realOutput); - } - case 'speed_and_pace': { - return unitUtils.convertSpeedPace(this.inputValue, this.inputUnit, this.outputUnit); - } - default: { - return null; - } - } - }, - }, + // Calculate conversion + return unitUtils.convertTime(inputValue.value, realInput, realOutput); + } + case 'speed_and_pace': { + return unitUtils.convertSpeedPace(inputValue.value, inputUnit.value, outputUnit.value); + } + default: { + return null; + } + } +}); - watch: { - /** - * Reset inputValue, inputUnit, and outputUnit - */ - category(newValue) { - switch (newValue) { - case 'distance': { - this.inputValue = storage.get('unit-calculator-distance-input-value', 1); - this.inputUnit = storage.get('unit-calculator-distance-input-unit', 'miles'); - this.outputUnit = storage.get('unit-calculator-distance-output-unit', 'kilometers'); - break; - } - case 'time': { - this.inputValue = storage.get('unit-calculator-time-input-value', 1); - this.inputUnit = storage.get('unit-calculator-time-input-unit', 'seconds'); - this.outputUnit = storage.get('unit-calculator-time-output-unit', 'hh:mm:ss'); - break; - } - case 'speed_and_pace': { - this.inputValue = storage.get('unit-calculator-speed-input-value', 600); - this.inputUnit = storage.get('unit-calculator-speed-input-unit', - 'seconds_per_mile'); - this.outputUnit = storage.get('unit-calculator-speed-output-unit', - 'miles_per_hour'); - break; - } - default: { - break; - } - } - }, +/** + * Reset inputValue, inputUnit, and outputUnit + */ +watch(category, (newValue) => { + switch (newValue) { + case 'distance': { + inputValue.value = storage.get('unit-calculator-distance-input-value', 1); + inputUnit.value = storage.get('unit-calculator-distance-input-unit', 'miles'); + outputUnit.value = storage.get('unit-calculator-distance-output-unit', 'kilometers'); + break; + } + case 'time': { + inputValue.value = storage.get('unit-calculator-time-input-value', 1); + inputUnit.value = storage.get('unit-calculator-time-input-unit', 'seconds'); + outputUnit.value = storage.get('unit-calculator-time-output-unit', 'hh:mm:ss'); + break; + } + case 'speed_and_pace': { + inputValue.value = storage.get('unit-calculator-speed-input-value', 600); + inputUnit.value = storage.get('unit-calculator-speed-input-unit', + 'seconds_per_mile'); + outputUnit.value = storage.get('unit-calculator-speed-output-unit', + 'miles_per_hour'); + break; + } + default: { + break; + } + } +}); - /** - * Save input value - */ - inputValue(newValue) { - switch (this.category) { - case 'distance': { - storage.set('unit-calculator-distance-input-value', newValue); - break; - } - case 'time': { - storage.set('unit-calculator-time-input-value', newValue); - break; - } - case 'speed_and_pace': { - storage.set('unit-calculator-speed-input-value', newValue); - break; - } - default: { - break; - } - } - }, +/** + * Save input value + */ +watch(inputValue, (newValue) => { + switch (category.value) { + case 'distance': { + storage.set('unit-calculator-distance-input-value', newValue); + break; + } + case 'time': { + storage.set('unit-calculator-time-input-value', newValue); + break; + } + case 'speed_and_pace': { + storage.set('unit-calculator-speed-input-value', newValue); + break; + } + default: { + break; + } + } +}); - /** - * Save input unit - */ - inputUnit(newValue) { - switch (this.category) { - case 'distance': { - storage.set('unit-calculator-distance-input-unit', newValue); - break; - } - case 'time': { - storage.set('unit-calculator-time-input-unit', newValue); - break; - } - case 'speed_and_pace': { - storage.set('unit-calculator-speed-input-unit', newValue); - break; - } - default: { - break; - } - } - }, +/** + * Save input unit + */ +watch(inputUnit, (newValue) => { + switch (category.value) { + case 'distance': { + storage.set('unit-calculator-distance-input-unit', newValue); + break; + } + case 'time': { + storage.set('unit-calculator-time-input-unit', newValue); + break; + } + case 'speed_and_pace': { + storage.set('unit-calculator-speed-input-unit', newValue); + break; + } + default: { + break; + } + } +}); - /** - * Save output unit - */ - outputUnit(newValue) { - switch (this.category) { - case 'distance': { - storage.set('unit-calculator-distance-output-unit', newValue); - break; - } - case 'time': { - storage.set('unit-calculator-time-output-unit', newValue); - break; - } - case 'speed_and_pace': { - storage.set('unit-calculator-speed-output-unit', newValue); - break; - } - default: { - break; - } - } - }, - }, +/** + * Save output unit + */ +watch(outputUnit, (newValue) => { + switch (category.value) { + case 'distance': { + storage.set('unit-calculator-distance-output-unit', newValue); + break; + } + case 'time': { + storage.set('unit-calculator-time-output-unit', newValue); + break; + } + case 'speed_and_pace': { + storage.set('unit-calculator-speed-output-unit', newValue); + break; + } + default: { + break; + } + } +}); - methods: { - /** - * Get the type of a unit - * @param {String} unit The unit - * @returns {String} The type ('decimal' or 'time') - */ - getUnitType(unit) { - if (unit in unitUtils.DISTANCE_UNITS) { - return 'decimal'; - } - if (unit in unitUtils.TIME_UNITS) { - return 'decimal'; - } - if (unit === 'hh:mm:ss') { - return 'time'; - } - if (['seconds_per_kilometer', 'seconds_per_mile'].includes(unit)) { - return 'time'; - } - return 'decimal'; - }, - }, -}; +/** + * Get the type of a unit + * @param {String} unit The unit + * @returns {String} The type ('decimal' or 'time') + */ +function getUnitType(unit) { + if (unit in unitUtils.DISTANCE_UNITS) { + return 'decimal'; + } + if (unit in unitUtils.TIME_UNITS) { + return 'decimal'; + } + if (unit === 'hh:mm:ss') { + return 'time'; + } + if (['seconds_per_kilometer', 'seconds_per_mile'].includes(unit)) { + return 'time'; + } + return 'decimal'; +} </script> <style scoped> diff --git a/tests/unit/components/TargetSetSelector.spec.js b/tests/unit/components/TargetSetSelector.spec.js @@ -230,7 +230,7 @@ test('edit button should open target editor with the correct props for default s }); // Mock showModal function - wrapper.vm.$refs.dialog.showModal = vi.fn(); + wrapper.vm.dialogElement.showModal = vi.fn(); // Click edit button await wrapper.find('button').trigger('click'); @@ -265,7 +265,7 @@ test('edit button should open target editor with the correct props for custom se }); // Mock showModal function - wrapper.vm.$refs.dialog.showModal = vi.fn(); + wrapper.vm.dialogElement.showModal = vi.fn(); // Click edit button await wrapper.find('button').trigger('click'); @@ -305,7 +305,7 @@ test('should reload and sort target set before target editor is opened', async ( localStorage.setItem('running-tools.target-sets', JSON.stringify(targetSets)); // Mock showModal function - wrapper.vm.$refs.dialog.showModal = vi.fn(); + wrapper.vm.dialogElement.showModal = vi.fn(); // Click edit button await wrapper.find('button').trigger('click'); diff --git a/tests/unit/components/TimeInput.spec.js b/tests/unit/components/TimeInput.spec.js @@ -42,13 +42,13 @@ test('should emit input event when value changes', async () => { const wrapper = shallowMount(TimeInput); // Change value to 1:00:00.00 - await wrapper.setData({ internalValue: 3600 }); + await wrapper.findAllComponents({ name: 'integer-input' })[0].setValue(1); // Assert input event was emitted expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[3600.00]]); // Change value to 1:00:01.50 - await wrapper.setData({ internalValue: 3601.5 }); + await wrapper.findComponent({ name: 'decimal-input' }).setValue(1.5); // Assert another input event was emitted expect(wrapper.emitted()['update:modelValue']).to.deep.equal([[3600.00], [3601.50]]); diff --git a/tests/unit/views/PaceCalculator.spec.js b/tests/unit/views/PaceCalculator.spec.js @@ -1,6 +1,7 @@ import { beforeEach, test, expect } from 'vitest'; import { shallowMount } from '@vue/test-utils'; import PaceCalculator from '@/views/PaceCalculator.vue'; +import targetUtils from '@/utils/targets'; beforeEach(() => { localStorage.clear(); @@ -34,19 +35,16 @@ test('should correctly calculate time results', async () => { test('should correctly calculate distance results according to default units setting', async () => { // Initialize component - const wrapper = shallowMount(PaceCalculator, { - data() { - return { - defaultUnitSystem: 'metric', - }; - }, - }); + const wrapper = shallowMount(PaceCalculator); // Enter input pace data await wrapper.findComponent({ name: 'decimal-input' }).setValue(2); await wrapper.find('select[aria-label="Input distance unit"]').setValue('miles'); await wrapper.findComponent({ name: 'time-input' }).setValue(1200); + // Set default units + await wrapper.find('select[aria-label="Default units"]').setValue('metric'); + // Get calculate result function const calculateResult = wrapper.findComponent({ name: 'simple-target-table' }).vm.calculateResult; @@ -76,27 +74,11 @@ test('should not show paces in results table', async () => { test('should correctly handle null target set', async () => { // Initialize component - const paceTargets = [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, - ]; - const wrapper = shallowMount(PaceCalculator, { - data() { - return { - targetSets: { - '_pace_targets': { - name: 'Common pace targets', - targets: paceTargets, - }, - '_race_targets': null, - }, - }; - }, - }); + const wrapper = shallowMount(PaceCalculator); + await wrapper.vm.reloadTargets(); // onActivated method not called in tests // Switch to invalid target set - await wrapper.findComponent({ name: 'target-set-selector' }).setValue('_race_targets'); + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('does_not_exist'); // Assert empty array passed to SimpleTargetTable component expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.targets).to.deep.equal([]); @@ -105,6 +87,7 @@ test('should correctly handle null target set', async () => { await wrapper.findComponent({ name: 'target-set-selector' }).setValue('_pace_targets'); // Assert valid targets passed to SimpleTargetTable component + const paceTargets = targetUtils.defaultTargetSets._pace_targets.targets; expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.targets).to.deep.equal(paceTargets); }); @@ -143,34 +126,12 @@ test('should load selected target set from localStorage', async () => { localStorage.setItem('running-tools.pace-calculator-target-set', '"_race_targets"'); // Initialize component - const raceTargets = [ - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 6, distanceUnit: 'kilometers' }, - ]; - const wrapper = shallowMount(PaceCalculator, { - data() { - return { - targetSets: { - '_pace_targets': { - name: 'Common pace targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, - ], - }, - '_race_targets': { - name: 'Common race targets', - targets: raceTargets, - }, - }, - }; - }, - }); + const wrapper = shallowMount(PaceCalculator); + await wrapper.vm.reloadTargets(); // Assert selection is loaded expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.modelValue).to.equal('_race_targets'); + const raceTargets = targetUtils.defaultTargetSets._race_targets.targets; expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.targets).to.deep.equal(raceTargets); }); @@ -187,15 +148,10 @@ test('should save selected target set to localStorage when modified', async () = test('should save default units setting to localStorage when modified', async () => { // Initialize component - const wrapper = shallowMount(PaceCalculator, { - data() { - return { - defaultUnitSystem: 'metric', - }; - }, - }); + const wrapper = shallowMount(PaceCalculator); // Change default units + await wrapper.find('select[aria-label="Default units"]').setValue('metric'); await wrapper.find('select[aria-label="Default units"]').setValue('imperial'); // New default units should be saved to localStorage diff --git a/tests/unit/views/RaceCalculator.spec.js b/tests/unit/views/RaceCalculator.spec.js @@ -1,6 +1,7 @@ import { beforeEach, test, expect } from 'vitest'; import { shallowMount } from '@vue/test-utils'; import RaceCalculator from '@/views/RaceCalculator.vue'; +import targetUtils from '@/utils/targets'; beforeEach(() => { localStorage.clear(); @@ -32,19 +33,16 @@ test('should correctly predict race times', async () => { test('should correctly calculate distance results according to default units setting', async () => { // Initialize component - const wrapper = shallowMount(RaceCalculator, { - data() { - return { - defaultUnitSystem: 'metric', - }; - }, - }); + const wrapper = shallowMount(RaceCalculator); // Enter input pace data await wrapper.findComponent({ name: 'decimal-input' }).setValue(5); await wrapper.find('select[aria-label="Input distance unit"]').setValue('kilometers'); await wrapper.findComponent({ name: 'time-input' }).setValue(1200); + // Set default units + await wrapper.find('select[aria-label="Default units"]').setValue('metric'); + // Get calculate result function const calculateResult = wrapper.findComponent({ name: 'simple-target-table' }).vm.calculateResult; @@ -76,27 +74,11 @@ test('should show paces in results table', async () => { test('should correctly handle null target set', async () => { // Initialize component - const raceTargets = [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, - ]; - const wrapper = shallowMount(RaceCalculator, { - data() { - return { - targetSets: { - '_pace_targets': null, - '_race_targets': { - name: 'Common race targets', - targets: raceTargets, - }, - }, - }; - }, - }); + const wrapper = shallowMount(RaceCalculator); + await wrapper.vm.reloadTargets(); // onActivated method not called in tests // Switch to invalid target set - await wrapper.findComponent({ name: 'target-set-selector' }).setValue('_pace_targets'); + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('does_not_exist'); // Assert empty array passed to SimpleTargetTable component expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.targets).to.deep.equal([]); @@ -105,6 +87,7 @@ test('should correctly handle null target set', async () => { await wrapper.findComponent({ name: 'target-set-selector' }).setValue('_race_targets'); // Assert valid targets passed to SimpleTargetTable component + const raceTargets = targetUtils.defaultTargetSets._race_targets.targets; expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.targets).to.deep.equal(raceTargets); }); @@ -202,34 +185,12 @@ test('should load selected target set from localStorage', async () => { localStorage.setItem('running-tools.race-calculator-target-set', '"_pace_targets"'); // Initialize component - const paceTargets = [ - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 3, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 6, distanceUnit: 'kilometers' }, - ]; - const wrapper = shallowMount(RaceCalculator, { - data() { - return { - targetSets: { - '_pace_targets': { - name: 'Common pace targets', - targets: paceTargets, - }, - '_race_targets': { - name: 'Common race targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, - ], - }, - }, - }; - }, - }); + const wrapper = shallowMount(RaceCalculator); + await wrapper.vm.reloadTargets(); // Assert selection is loaded expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.modelValue).to.equal('_pace_targets'); + const paceTargets = targetUtils.defaultTargetSets._pace_targets.targets; expect(wrapper.findComponent({ name: 'simple-target-table' }).vm.targets).to.deep.equal(paceTargets); }); @@ -246,15 +207,10 @@ test('should save selected target set to localStorage when modified', async () = test('should save default units setting to localStorage when modified', async () => { // Initialize component - const wrapper = shallowMount(RaceCalculator, { - data() { - return { - defaultUnitSystem: 'metric', - }; - }, - }); + const wrapper = shallowMount(RaceCalculator); // Change default units + await wrapper.find('select[aria-label="Default units"]').setValue('metric'); await wrapper.find('select[aria-label="Default units"]').setValue('imperial'); // New default units should be saved to localStorage diff --git a/tests/unit/views/SplitCalculator.spec.js b/tests/unit/views/SplitCalculator.spec.js @@ -8,105 +8,73 @@ beforeEach(() => { test('should initialize undefined splits to 0:00.00', async () => { // Initialize component - const wrapper = shallowMount(SplitCalculator, { - data() { - return { - targetSets: { - '_split_targets': { - name: 'Split targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'kilometers' }, - { result: 'time', distanceValue: 2, distanceUnit: 'kilometers' }, - { result: 'time', distanceValue: 3000, distanceUnit: 'meters' }, - ], - }, - }, - defaultUnitSystem: 'metric', - }; - }, - }); + const wrapper = shallowMount(SplitCalculator); + await wrapper.vm.reloadTargets(); // onActivated method not called in tests // Assert results are correct const rows = wrapper.findAll('tbody tr'); - expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 km'); + expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 mi'); expect(rows[0].findAll('td')[1].element.textContent).to.equal('0:00.00'); expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('0:00 / km'); + expect(rows[0].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); expect(rows[0].findAll('td').length).to.equal(4); - expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 km'); + expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 mi'); expect(rows[1].findAll('td')[1].element.textContent).to.equal('0:00.00'); expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('0:00 / km'); + expect(rows[1].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); expect(rows[1].findAll('td').length).to.equal(4); - expect(rows[2].findAll('td')[0].element.textContent).to.equal('3000 m'); + expect(rows[2].findAll('td')[0].element.textContent).to.equal('5 km'); expect(rows[2].findAll('td')[1].element.textContent).to.equal('0:00.00'); expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('0:00 / km'); + expect(rows[2].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); expect(rows[2].findAll('td').length).to.equal(4); expect(rows.length).to.equal(3); }); test('should correctly load split times from split targets', async () => { - // Initialize component - const wrapper = shallowMount(SplitCalculator, { - data() { - return { - targetSets: { - '_split_targets': { - name: 'Split targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, - { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, - { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, - ], - }, - }, - defaultUnitSystem: 'metric', - }; + // Initialize localStorage + localStorage.setItem('running-tools.target-sets', JSON.stringify({ + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, + ], }, - }); + })); + + // Initialize component + const wrapper = shallowMount(SplitCalculator); + await wrapper.vm.reloadTargets(); // Assert results are correct const rows = wrapper.findAll('tbody tr'); expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 km'); expect(rows[0].findAll('td')[1].element.textContent).to.equal('3:00.00'); expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(180); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('3:00 / km'); + expect(rows[0].findAll('td')[3].element.textContent).to.equal('4:50 / mi'); expect(rows[0].findAll('td').length).to.equal(4); expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 km'); expect(rows[1].findAll('td')[1].element.textContent).to.equal('6:10.00'); expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(190); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('3:10 / km'); + expect(rows[1].findAll('td')[3].element.textContent).to.equal('5:06 / mi'); expect(rows[1].findAll('td').length).to.equal(4); expect(rows[2].findAll('td')[0].element.textContent).to.equal('3000 m'); expect(rows[2].findAll('td')[1].element.textContent).to.equal('9:30.00'); expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(200); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('3:20 / km'); + expect(rows[2].findAll('td')[3].element.textContent).to.equal('5:22 / mi'); expect(rows[2].findAll('td').length).to.equal(4); expect(rows.length).to.equal(3); }); test('should correctly handle null target set', async () => { + // Initialize localStorage + localStorage.setItem('running-tools.split-calculator-target-set', '"does_not_exist"'); + // Initialize component - const wrapper = shallowMount(SplitCalculator, { - data() { - return { - targetSets: { - '_split_targets': { - name: 'Split targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, - { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, - { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, - ], - }, - 'B': null, - }, - selectedTargetSet: 'B', - defaultUnitSystem: 'metric', - }; - }, - }); + const wrapper = shallowMount(SplitCalculator); + await wrapper.vm.reloadTargets(); // Assert results are empty let rows = wrapper.findAll('tbody tr'); @@ -119,168 +87,133 @@ test('should correctly handle null target set', async () => { // Assert results are correct rows = wrapper.findAll('tbody tr'); - expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 km'); - expect(rows[0].findAll('td')[1].element.textContent).to.equal('3:00.00'); - expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(180); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('3:00 / km'); - expect(rows[0].findAll('td').length).to.equal(4); - expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 km'); - expect(rows[1].findAll('td')[1].element.textContent).to.equal('6:10.00'); - expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(190); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('3:10 / km'); - expect(rows[1].findAll('td').length).to.equal(4); - expect(rows[2].findAll('td')[0].element.textContent).to.equal('3000 m'); - expect(rows[2].findAll('td')[1].element.textContent).to.equal('9:30.00'); - expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(200); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('3:20 / km'); - expect(rows[2].findAll('td').length).to.equal(4); + expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 mi'); + expect(rows[0].findAll('td')[1].element.textContent).to.equal('0:00.00'); + expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); + expect(rows[0].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); expect(rows.length).to.equal(3); }); -test('should correctly calculate paces and cululative times from entered split times', async () => { +test('should correctly calculate paces and cumulative times from entered split times', async () => { // Initialize component - const wrapper = shallowMount(SplitCalculator, { - data() { - return { - targetSets: { - '_split_targets': { - name: 'Split targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, - { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 180 }, - { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 180 }, - ], - }, - }, - defaultUnitSystem: 'metric', - }; - }, - }); + const wrapper = shallowMount(SplitCalculator); + await wrapper.vm.reloadTargets(); // Update split times - await wrapper.findAllComponents({ name: 'time-input' })[1].setValue(190); - await wrapper.findAllComponents({ name: 'time-input' })[2].setValue(200); + await wrapper.findAllComponents({ name: 'time-input' })[0].setValue(420); + await wrapper.findAllComponents({ name: 'time-input' })[1].setValue(390); + await wrapper.findAllComponents({ name: 'time-input' })[2].setValue(390); // Assert results are correct const rows = wrapper.findAll('tbody tr'); - expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 km'); - expect(rows[0].findAll('td')[1].element.textContent).to.equal('3:00.00'); - expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(180); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('3:00 / km'); + expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 mi'); + expect(rows[0].findAll('td')[1].element.textContent).to.equal('7:00.00'); + expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(420); + expect(rows[0].findAll('td')[3].element.textContent).to.equal('7:00 / mi'); expect(rows[0].findAll('td').length).to.equal(4); - expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 km'); - expect(rows[1].findAll('td')[1].element.textContent).to.equal('6:10.00'); - expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(190); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('3:10 / km'); + expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 mi'); + expect(rows[1].findAll('td')[1].element.textContent).to.equal('13:30.00'); + expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(390); + expect(rows[1].findAll('td')[3].element.textContent).to.equal('6:30 / mi'); expect(rows[1].findAll('td').length).to.equal(4); - expect(rows[2].findAll('td')[0].element.textContent).to.equal('3000 m'); - expect(rows[2].findAll('td')[1].element.textContent).to.equal('9:30.00'); - expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(200); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('3:20 / km'); + expect(rows[2].findAll('td')[0].element.textContent).to.equal('5 km'); + expect(rows[2].findAll('td')[1].element.textContent).to.equal('20:00.00'); + expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(390); + expect(rows[2].findAll('td')[3].element.textContent).to.equal('5:52 / mi'); expect(rows[2].findAll('td').length).to.equal(4); expect(rows.length).to.equal(3); }); test('should correctly sort split targets', async () => { - // Initialize component - const wrapper = shallowMount(SplitCalculator, { - data() { - return { - targetSets: { - '_split_targets': { - name: 'Split targets', - targets: [ - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 1, distanceUnit: 'kilometers' }, - { result: 'time', distanceValue: 2, distanceUnit: 'kilometers' }, - ], - }, - }, - defaultUnitSystem: 'metric', - }; + // Initialize localStorage (targets are mis-ordered) + localStorage.setItem('running-tools.target-sets', JSON.stringify({ + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers' }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers' }, + ], }, - }); + })); + + // Initialize component + const wrapper = shallowMount(SplitCalculator) + await wrapper.vm.reloadTargets(); // Assert results are correct const rows = wrapper.findAll('tbody tr'); expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 km'); expect(rows[0].findAll('td')[1].element.textContent).to.equal('0:00.00'); expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('0:00 / km'); + expect(rows[0].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); expect(rows[0].findAll('td').length).to.equal(4); expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 km'); expect(rows[1].findAll('td')[1].element.textContent).to.equal('0:00.00'); expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('0:00 / km'); + expect(rows[1].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); expect(rows[1].findAll('td').length).to.equal(4); expect(rows[2].findAll('td')[0].element.textContent).to.equal('2 mi'); expect(rows[2].findAll('td')[1].element.textContent).to.equal('0:00.00'); expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('0:00 / km'); + expect(rows[2].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); expect(rows[2].findAll('td').length).to.equal(4); expect(rows.length).to.equal(3); }); test('should ignore time based targets', async () => { - // Initialize component - const wrapper = shallowMount(SplitCalculator, { - data() { - return { - targetSets: { - '_split_targets': { - name: 'Split targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'kilometers' }, - { result: 'distance', time: 600 }, - { result: 'time', distanceValue: 2, distanceUnit: 'kilometers' }, - { result: 'time', distanceValue: 3000, distanceUnit: 'meters' }, - ], - }, - }, - defaultUnitSystem: 'metric', - }; + // Initialize localStorage + localStorage.setItem('running-tools.target-sets', JSON.stringify({ + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers' }, + { result: 'distance', time: 600 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers' }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters' }, + ], }, - }); + })); + // Initialize component + const wrapper = shallowMount(SplitCalculator); + await wrapper.vm.reloadTargets(); // Assert results are correct const rows = wrapper.findAll('tbody tr'); expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 km'); expect(rows[0].findAll('td')[1].element.textContent).to.equal('0:00.00'); expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('0:00 / km'); + expect(rows[0].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); expect(rows[0].findAll('td').length).to.equal(4); expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 km'); expect(rows[1].findAll('td')[1].element.textContent).to.equal('0:00.00'); expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('0:00 / km'); + expect(rows[1].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); expect(rows[1].findAll('td').length).to.equal(4); expect(rows[2].findAll('td')[0].element.textContent).to.equal('3000 m'); expect(rows[2].findAll('td')[1].element.textContent).to.equal('0:00.00'); expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(0); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('0:00 / km'); + expect(rows[2].findAll('td')[3].element.textContent).to.equal('0:00 / mi'); expect(rows[2].findAll('td').length).to.equal(4); expect(rows.length).to.equal(3); }); test('should correctly save split times with split targets in localStorage', async () => { - // Initialize component - const wrapper = shallowMount(SplitCalculator, { - data() { - return { - targetSets: { - '_split_targets': { - name: 'Split targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, - { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 180 }, - { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 180 }, - ], - }, - }, - defaultUnitSystem: 'metric', - }; + // Initialize localStorage + localStorage.setItem('running-tools.target-sets', JSON.stringify({ + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 180 }, + ], }, - }); + })); + + // Initialize component + const wrapper = shallowMount(SplitCalculator); + await wrapper.vm.reloadTargets(); // Update split times await wrapper.findAllComponents({ name: 'time-input' })[1].setValue(190); @@ -300,32 +233,29 @@ test('should correctly save split times with split targets in localStorage', asy }); test('should update results when a new target set is selected', async () => { - // Initialize component - const wrapper = shallowMount(SplitCalculator, { - data() { - return { - targetSets: { - '_split_targets': { - name: 'Split targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, - ], - }, - 'B': { - name: 'Split targets #2', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, - { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, - { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, - ], - }, - }, - defaultUnitSystem: 'metric', - }; + // Initialize localStorage + localStorage.setItem('running-tools.target-sets', JSON.stringify({ + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], }, - }); + 'B': { + name: 'Split targets #2', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, + ], + }, + })); + + // Initialize component + const wrapper = shallowMount(SplitCalculator); + await wrapper.vm.reloadTargets(); // Assert default split targets are initially loaded expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.modelValue).to.equal('_split_targets'); @@ -339,17 +269,17 @@ test('should update results when a new target set is selected', async () => { expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 km'); expect(rows[0].findAll('td')[1].element.textContent).to.equal('3:00.00'); expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(180); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('3:00 / km'); + expect(rows[0].findAll('td')[3].element.textContent).to.equal('4:50 / mi'); expect(rows[0].findAll('td').length).to.equal(4); expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 km'); expect(rows[1].findAll('td')[1].element.textContent).to.equal('6:10.00'); expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(190); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('3:10 / km'); + expect(rows[1].findAll('td')[3].element.textContent).to.equal('5:06 / mi'); expect(rows[1].findAll('td').length).to.equal(4); expect(rows[2].findAll('td')[0].element.textContent).to.equal('3000 m'); expect(rows[2].findAll('td')[1].element.textContent).to.equal('9:30.00'); expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(200); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('3:20 / km'); + expect(rows[2].findAll('td')[3].element.textContent).to.equal('5:22 / mi'); expect(rows[2].findAll('td').length).to.equal(4); expect(rows.length).to.equal(3); }); @@ -357,33 +287,29 @@ test('should update results when a new target set is selected', async () => { test('should load selected target set from localStorage', async () => { // Initialize localStorage localStorage.setItem('running-tools.split-calculator-target-set', '"B"'); + localStorage.setItem('running-tools.target-sets', JSON.stringify({ + '_split_targets': { + name: 'Split targets', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, + { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, + ], + }, + 'B': { + name: 'Split targets #2', + targets: [ + { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, + { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, + { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, + ], + }, + })); + // Initialize component - const wrapper = shallowMount(SplitCalculator, { - data() { - return { - targetSets: { - '_split_targets': { - name: 'Split targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, - ], - }, - 'B': { - name: 'Split targets #2', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, - { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, - { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, - ], - }, - }, - defaultUnitSystem: 'metric', - }; - }, - }); + const wrapper = shallowMount(SplitCalculator); + await wrapper.vm.reloadTargets(); // Assert selection is loaded expect(wrapper.findComponent({ name: 'target-set-selector' }).vm.modelValue).to.equal('B'); @@ -393,75 +319,45 @@ test('should load selected target set from localStorage', async () => { expect(rows[0].findAll('td')[0].element.textContent).to.equal('1 km'); expect(rows[0].findAll('td')[1].element.textContent).to.equal('3:00.00'); expect(rows[0].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(180); - expect(rows[0].findAll('td')[3].element.textContent).to.equal('3:00 / km'); + expect(rows[0].findAll('td')[3].element.textContent).to.equal('4:50 / mi'); expect(rows[0].findAll('td').length).to.equal(4); expect(rows[1].findAll('td')[0].element.textContent).to.equal('2 km'); expect(rows[1].findAll('td')[1].element.textContent).to.equal('6:10.00'); expect(rows[1].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(190); - expect(rows[1].findAll('td')[3].element.textContent).to.equal('3:10 / km'); + expect(rows[1].findAll('td')[3].element.textContent).to.equal('5:06 / mi'); expect(rows[1].findAll('td').length).to.equal(4); expect(rows[2].findAll('td')[0].element.textContent).to.equal('3000 m'); expect(rows[2].findAll('td')[1].element.textContent).to.equal('9:30.00'); expect(rows[2].findAll('td')[2].findComponent({ name: 'time-input' }).vm.modelValue).to.equal(200); - expect(rows[2].findAll('td')[3].element.textContent).to.equal('3:20 / km'); + expect(rows[2].findAll('td')[3].element.textContent).to.equal('5:22 / mi'); expect(rows[2].findAll('td').length).to.equal(4); expect(rows.length).to.equal(3); }); test('should save selected target set to localStorage when modified', async () => { // Initialize component - const wrapper = shallowMount(SplitCalculator, { - data() { - return { - targetSets: { - '_split_targets': { - name: 'Split targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles' }, - { result: 'time', distanceValue: 5, distanceUnit: 'kilometers' }, - ], - }, - 'B': { - name: 'Split targets #2', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'kilometers', split: 180 }, - { result: 'time', distanceValue: 2, distanceUnit: 'kilometers', split: 190 }, - { result: 'time', distanceValue: 3000, distanceUnit: 'meters', split: 200 }, - ], - }, - }, - defaultUnitSystem: 'metric', - }; - }, - }); + const wrapper = shallowMount(SplitCalculator); + await wrapper.vm.reloadTargets(); // Select a new target set - await wrapper.findComponent({ name: 'target-set-selector' }).setValue('B'); + await wrapper.findComponent({ name: 'target-set-selector' }).setValue('_race_targets'); // New selected target set should be saved to localStorage - expect(localStorage.getItem('running-tools.split-calculator-target-set')).to.equal('"B"'); + expect(localStorage.getItem('running-tools.split-calculator-target-set')).to.equal('"_race_targets"'); }); test('should update paces according to default units setting', async () => { // Initialize component - const wrapper = shallowMount(SplitCalculator, { - data() { - return { - targetSets: { - '_split_targets': { - name: 'Split targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles', split: 300 }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles', split: 300 }, - { result: 'time', distanceValue: 5, distanceUnit: 'kilometers', split: 330 }, - ], - }, - }, - defaultUnitSystem: 'metric', - }; - }, - }); + const wrapper = shallowMount(SplitCalculator); + await wrapper.vm.reloadTargets(); + + // Enter split times + await wrapper.findAllComponents({ name: 'time-input' })[0].setValue(300); + await wrapper.findAllComponents({ name: 'time-input' })[1].setValue(300); + await wrapper.findAllComponents({ name: 'time-input' })[2].setValue(330); + + // Set default units setting + await wrapper.find('select', { name: 'Default units' }).setValue('metric'); // Assert paces are correct let rows = wrapper.findAll('tbody tr'); @@ -481,23 +377,7 @@ test('should update paces according to default units setting', async () => { test('should save default units setting to localStorage when modified', async () => { // Initialize component - const wrapper = shallowMount(SplitCalculator, { - data() { - return { - targetSets: { - '_split_targets': { - name: 'Split targets', - targets: [ - { result: 'time', distanceValue: 1, distanceUnit: 'miles', split: 300 }, - { result: 'time', distanceValue: 2, distanceUnit: 'miles', split: 300 }, - { result: 'time', distanceValue: 5, distanceUnit: 'kilometers', split: 330 }, - ], - }, - }, - defaultUnitSystem: 'metric', - }; - }, - }); + const wrapper = shallowMount(SplitCalculator); // Change default units await wrapper.find('select').setValue('imperial'); diff --git a/tests/unit/views/UnitCalculator.spec.js b/tests/unit/views/UnitCalculator.spec.js @@ -27,7 +27,7 @@ test('should correctly update controls when category changes', async () => { expect(wrapper.find('select[aria-label="Output units"]').element.value).to.equal('miles_per_hour'); // Change category - await wrapper.setData({ category: 'distance' }); + await wrapper.find('select[aria-label="Selected unit category"]').setValue('distance'); // Assert controls are correct expect(wrapper.findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(1); @@ -121,7 +121,7 @@ test('should correctly load saved inputs', async () => { expect(wrapper.find('select[aria-label="Output units"]').element.value).to.equal('seconds_per_mile'); // Change category - await wrapper.setData({ category: 'distance' }); + await wrapper.find('select[aria-label="Selected unit category"]').setValue('distance'); // Assert inputs are correct expect(wrapper.findComponent({ name: 'decimal-input' }).vm.modelValue).to.equal(5);