From ac964ddd3f58b2080b1f775ec02b47cf23218b98 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Mon, 17 Apr 2023 02:43:36 +0200 Subject: [PATCH] Move to Typescript. --- package-lock.json | 344 ++++++++++++++++-- package.json | 7 +- src/{App.js => App.tsx} | 26 +- src/{Cell.js => Cell.tsx} | 20 +- src/{InputForm.js => InputForm.tsx} | 21 +- src/{Maze.js => Maze.tsx} | 7 +- src/{MessageBanner.js => MessageBanner.tsx} | 6 +- ...ield.js => ValidatingInputNumberField.tsx} | 0 src/index.js | 12 - src/index.tsx | 12 + src/model/Coordinates.ts | 4 + src/model/Maze.ts | 18 + src/state/action.ts | 60 +++ src/{reducer.js => state/reducer.ts} | 26 +- src/state/state.ts | 18 + .../userpathhandler.ts} | 20 +- 16 files changed, 482 insertions(+), 119 deletions(-) rename src/{App.js => App.tsx} (56%) rename src/{Cell.js => Cell.tsx} (65%) rename src/{InputForm.js => InputForm.tsx} (89%) rename src/{Maze.js => Maze.tsx} (82%) rename src/{MessageBanner.js => MessageBanner.tsx} (74%) rename src/{ValidatingInputNumberField.js => ValidatingInputNumberField.tsx} (100%) delete mode 100644 src/index.js create mode 100644 src/index.tsx create mode 100644 src/model/Coordinates.ts create mode 100644 src/model/Maze.ts create mode 100644 src/state/action.ts rename src/{reducer.js => state/reducer.ts} (61%) create mode 100644 src/state/state.ts rename src/{userpathhandler.js => state/userpathhandler.ts} (76%) diff --git a/package-lock.json b/package-lock.json index 1ab4f97..e6af3cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,13 @@ "name": "a-maze-r", "version": "0.0.0", "dependencies": { + "@types/jest": "^29.5.0", + "@types/node": "^18.15.11", + "@types/react": "^18.0.35", + "@types/react-dom": "^18.0.11", "react": "^18.0.0", - "react-dom": "^18.0.0" + "react-dom": "^18.0.0", + "typescript": "^5.0.4" }, "devDependencies": { "react-scripts": "^5.0.1" @@ -32,7 +37,6 @@ "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", - "dev": true, "dependencies": { "@babel/highlight": "^7.18.6" }, @@ -414,7 +418,6 @@ "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -461,7 +464,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -2642,6 +2644,25 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/@jest/expect-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "dependencies": { + "jest-get-type": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils/node_modules/jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jest/fake-timers": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", @@ -3753,14 +3774,12 @@ "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, "dependencies": { "@types/istanbul-lib-coverage": "*" } @@ -3769,11 +3788,246 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, "dependencies": { "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dependencies": { + "@sinclair/typebox": "^0.25.16" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "dependencies": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==" + }, + "node_modules/@types/jest/node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@types/jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@types/jest/node_modules/diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "dependencies": { + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/jest/node_modules/jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-message-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.5.0", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/@types/jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -3795,8 +4049,7 @@ "node_modules/@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "dev": true + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -3810,6 +4063,11 @@ "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", "dev": true }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, "node_modules/@types/q": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", @@ -3828,6 +4086,24 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "node_modules/@types/react": { + "version": "18.0.35", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.35.tgz", + "integrity": "sha512-6Laome31HpetaIUGFWl1VQ3mdSImwxtFZ39rh059a1MNnKGqBpC88J6NJ8n/Is3Qx7CefDGLgf/KhN/sYCf7ag==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.0.11", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz", + "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -3843,6 +4119,11 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + }, "node_modules/@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -3880,8 +4161,7 @@ "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, "node_modules/@types/trusted-types": { "version": "2.0.3", @@ -3910,8 +4190,7 @@ "node_modules/@types/yargs-parser": { "version": "21.0.0", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.58.0", @@ -4617,7 +4896,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -5314,7 +5592,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -5490,7 +5767,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -5567,7 +5843,6 @@ "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", - "dev": true, "funding": [ { "type": "github", @@ -5650,7 +5925,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -5658,8 +5932,7 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/colord": { "version": "2.9.3", @@ -6349,6 +6622,11 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, + "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==" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -6974,7 +7252,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "engines": { "node": ">=0.8.0" } @@ -8034,7 +8311,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -8684,8 +8960,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/grapheme-splitter": { "version": "1.0.4", @@ -8745,7 +9020,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "engines": { "node": ">=4" } @@ -9415,7 +9689,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -12345,7 +12618,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -13088,7 +13360,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -15879,7 +16150,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -16013,7 +16283,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -16025,7 +16294,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, "engines": { "node": ">=8" } @@ -16321,7 +16589,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -16716,7 +16983,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -16911,17 +17177,15 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "peer": true, + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=12.20" } }, "node_modules/unbox-primitive": { diff --git a/package.json b/package.json index 25f0948..532772b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,12 @@ { "dependencies": { + "@types/jest": "^29.5.0", + "@types/node": "^18.15.11", + "@types/react": "^18.0.35", + "@types/react-dom": "^18.0.11", "react": "^18.0.0", - "react-dom": "^18.0.0" + "react-dom": "^18.0.0", + "typescript": "^5.0.4" }, "main": "/index.js", "homepage": ".", diff --git a/src/App.js b/src/App.tsx similarity index 56% rename from src/App.js rename to src/App.tsx index e02172d..09ded60 100644 --- a/src/App.js +++ b/src/App.tsx @@ -1,18 +1,13 @@ -import React, {useReducer} from 'react'; -import Maze from "./Maze"; -import InputForm from "./InputForm"; -import reduce from "./reducer"; -import MessageBanner from "./MessageBanner"; +import {useReducer} from 'react'; +import Maze from "./Maze.tsx"; +import InputForm from "./InputForm.tsx"; +import reduce from "./state/reducer.ts"; +import MessageBanner from "./MessageBanner.tsx"; +import {INITIAL_STATE} from "./state/state.ts"; +import {actionToggledShowSolution} from "./state/action.ts"; export default function App() { - const [state, dispatch] = useReducer(reduce, { - maze: null, - loading: false, - errorMessage: null, - showSolution: false, - userPath: [] - }, - undefined); + const [state, dispatch] = useReducer(reduce, INITIAL_STATE); const hasValidMaze = !!state.maze; return ( <> @@ -25,10 +20,7 @@ export default function App() {

The Maze ({state.maze.width}x{state.maze.height}, ID: {state.maze.id})

{ - dispatch({ - type: 'toggled_show_solution', - value: e.target.checked - }); + dispatch(actionToggledShowSolution(e.target.checked)); }} id={"showSolution"}/> e.x === x && e.y === y); } export default function Cell({x, y, state, dispatch}) { - const cell = state.maze.grid[y][x]; + const cell: MazeCell = state.maze.grid[y][x]; let classes = "cell r" + y + " c" + x; if (cell.top) classes += " top"; if (cell.right) classes += " right"; @@ -19,19 +21,11 @@ export default function Cell({x, y, state, dispatch}) { onMouseEnter={(e) => { const leftPressed = e.buttons & 0x1; if (leftPressed) { - dispatch({ - type: 'clicked_cell', - x, - y - }); + dispatch(actionClickedCell(x, y)); } }} onClick={(e) => { - dispatch({ - type: 'clicked_cell', - x, - y - }); + dispatch(actionClickedCell(x, y)); }}> ); diff --git a/src/InputForm.js b/src/InputForm.tsx similarity index 89% rename from src/InputForm.js rename to src/InputForm.tsx index 8c69ede..d6ad12a 100644 --- a/src/InputForm.js +++ b/src/InputForm.tsx @@ -1,32 +1,25 @@ -import React, {useState} from 'react'; -import ValidatingInputNumberField from "./ValidatingInputNumberField"; +import {useState} from 'react'; +import ValidatingInputNumberField from "./ValidatingInputNumberField.tsx"; +import {actionLoadedMaze, actionLoadingFailed, actionStartedLoading} from "./state/action.ts"; export default function InputForm({state, dispatch}) { const [width, setWidth] = useState(10); const [height, setHeight] = useState(10); - const [id, setId] = useState(null); + const [id, setId] = useState(null as number); const handleSubmit = (e) => { e.preventDefault(); - dispatch({ - type: 'started_loading' - }); + dispatch(actionStartedLoading()); const url = `https://manuel.friedli.info/labyrinth/create/json?w=${width}&h=${height}&id=${id || ''}`; fetch(url) .then(response => response.json()) // .then(result => new Promise(resolve => setTimeout(resolve, 600, result))) .then(result => { - dispatch({ - type: 'loaded_maze', - maze: result - }); + dispatch(actionLoadedMaze(result)); }) .catch(reason => { console.error("Failed to fetch maze data.", reason); - dispatch({ - type: 'loading_failed', - reason - }); + dispatch(actionLoadingFailed(reason)); }); }; const validateWidthHeightInput = value => { diff --git a/src/Maze.js b/src/Maze.tsx similarity index 82% rename from src/Maze.js rename to src/Maze.tsx index 4adbf2f..698014a 100644 --- a/src/Maze.js +++ b/src/Maze.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import Cell from "./Cell"; +import Cell from "./Cell.tsx"; export default function Maze({state, dispatch}) { @@ -7,9 +6,9 @@ export default function Maze({state, dispatch}) { return
No valid maze.
} - let maze = []; + let maze: JSX.Element[] = []; for (let y = 0; y < state.maze.height; y++) { - let row = []; + let row: JSX.Element[] = []; for (let x = 0; x < state.maze.width; x++) { row.push() } diff --git a/src/MessageBanner.js b/src/MessageBanner.tsx similarity index 74% rename from src/MessageBanner.js rename to src/MessageBanner.tsx index 1e3083a..25cb261 100644 --- a/src/MessageBanner.js +++ b/src/MessageBanner.tsx @@ -1,10 +1,8 @@ -import React from "react"; +import {actionClosedMessageBanner} from "./state/action.ts"; export default function MessageBanner({state, dispatch}) { function handleClose() { - dispatch({ - type: 'closed_message_banner' - }) + dispatch(actionClosedMessageBanner()); } if (!!state.errorMessage) { diff --git a/src/ValidatingInputNumberField.js b/src/ValidatingInputNumberField.tsx similarity index 100% rename from src/ValidatingInputNumberField.js rename to src/ValidatingInputNumberField.tsx diff --git a/src/index.js b/src/index.js deleted file mode 100644 index fb8062d..0000000 --- a/src/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import React, { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import "./styles.css"; - -import App from "./App"; - -const root = createRoot(document.getElementById("root")); -root.render( - - - -); \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..6f9bbb3 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,12 @@ +import {StrictMode} from "react"; +import {createRoot, Root} from "react-dom/client"; +import "./styles.css"; +import App from "./App.tsx"; + + +const root: Root = createRoot(document.getElementById("root")); +root.render( + + + +); \ No newline at end of file diff --git a/src/model/Coordinates.ts b/src/model/Coordinates.ts new file mode 100644 index 0000000..d52c913 --- /dev/null +++ b/src/model/Coordinates.ts @@ -0,0 +1,4 @@ +export default interface Coordinates { + x: number, + y: number +} \ No newline at end of file diff --git a/src/model/Maze.ts b/src/model/Maze.ts new file mode 100644 index 0000000..57ab1fe --- /dev/null +++ b/src/model/Maze.ts @@ -0,0 +1,18 @@ +import Coordinates from "./Coordinates"; + +export default interface Maze { + id: string, + width: number, + height: number, + start: Coordinates, + end: Coordinates, + grid: MazeCell[][] +} + +export interface MazeCell { + top: boolean, + right: boolean, + bottom: boolean, + left: boolean, + solution: boolean +} \ No newline at end of file diff --git a/src/state/action.ts b/src/state/action.ts new file mode 100644 index 0000000..46fb132 --- /dev/null +++ b/src/state/action.ts @@ -0,0 +1,60 @@ +import Maze from "../model/Maze"; + +export interface Action { + type: string, + + [key: string]: any +} + +export const ID_ACTION_STARTED_LOADING = 'started_loading'; + +export function actionStartedLoading(): Action { + return { + type: ID_ACTION_STARTED_LOADING + } +} + +export const ID_ACTION_LOADED_MAZE = 'loaded_maze'; + +export function actionLoadedMaze(maze: Maze): Action { + return { + type: ID_ACTION_LOADED_MAZE, + maze + } +} + +export const ID_ACTION_LOADING_FAILED = 'loading_failed'; + +export function actionLoadingFailed(reason: string): Action { + return { + type: ID_ACTION_LOADING_FAILED, + reason + }; +} + +export const ID_ACTION_TOGGLED_SHOW_SOLUTION = 'toggled_show_solution'; + +export function actionToggledShowSolution(value: boolean): Action { + return { + type: ID_ACTION_TOGGLED_SHOW_SOLUTION, + value + } +} + +export const ID_ACTION_CLOSED_MESSAGE_BANNER = 'closed_message_banner'; + +export function actionClosedMessageBanner(): Action { + return { + type: ID_ACTION_CLOSED_MESSAGE_BANNER + } +} + +export const ID_ACTION_CLICKED_CELL = 'clicked_cell'; + +export function actionClickedCell(x: number, y: number): Action { + return { + type: ID_ACTION_CLICKED_CELL, + x, + y + } +} \ No newline at end of file diff --git a/src/reducer.js b/src/state/reducer.ts similarity index 61% rename from src/reducer.js rename to src/state/reducer.ts index 17cffce..eaddcb3 100644 --- a/src/reducer.js +++ b/src/state/reducer.ts @@ -1,8 +1,18 @@ -import handleUserClicked from "./userpathhandler"; +import handleUserClicked from "./userpathhandler.ts"; +import {State} from "./state"; +import { + Action, + ID_ACTION_CLICKED_CELL, + ID_ACTION_CLOSED_MESSAGE_BANNER, + ID_ACTION_LOADED_MAZE, + ID_ACTION_LOADING_FAILED, + ID_ACTION_STARTED_LOADING, + ID_ACTION_TOGGLED_SHOW_SOLUTION +} from "./action.ts"; -export default function reduce(state, action) { +export default function reduce(state: State, action: Action) { switch (action.type) { - case 'started_loading': { + case ID_ACTION_STARTED_LOADING: { return { ...state, maze: null, @@ -10,7 +20,7 @@ export default function reduce(state, action) { errorMessage: null } } - case 'loaded_maze': { + case ID_ACTION_LOADED_MAZE: { return { ...state, loading: false, @@ -18,26 +28,26 @@ export default function reduce(state, action) { userPath: [] } } - case 'loading_failed': { + case ID_ACTION_LOADING_FAILED: { return { ...state, loading: false, errorMessage: `Failed to load maze. Reason: ${action.reason}` } } - case 'toggled_show_solution': { + case ID_ACTION_TOGGLED_SHOW_SOLUTION: { return { ...state, showSolution: action.value } } - case 'closed_message_banner': { + case ID_ACTION_CLOSED_MESSAGE_BANNER: { return { ...state, errorMessage: null } } - case 'clicked_cell': { + case ID_ACTION_CLICKED_CELL: { // There's so much logic involved, externalize that into its own file. return handleUserClicked(state, action.x, action.y); } diff --git a/src/state/state.ts b/src/state/state.ts new file mode 100644 index 0000000..999ed91 --- /dev/null +++ b/src/state/state.ts @@ -0,0 +1,18 @@ +import Coordinates from "../model/Coordinates"; +import Maze from "../model/Maze"; + +export interface State { + errorMessage: string | null, + loading: boolean, + maze: Maze | null, + showSolution: boolean, + userPath: Coordinates[] +} + +export const INITIAL_STATE: State = { + errorMessage: null, + loading: false, + maze: null, + showSolution: false, + userPath: [] +}; \ No newline at end of file diff --git a/src/userpathhandler.js b/src/state/userpathhandler.ts similarity index 76% rename from src/userpathhandler.js rename to src/state/userpathhandler.ts index c967f8d..dfa8251 100644 --- a/src/userpathhandler.js +++ b/src/state/userpathhandler.ts @@ -1,12 +1,19 @@ -export default function handleUserClicked(state, x, y) { +import {State} from "./state"; +import Coordinates from "../model/Coordinates"; +import {MazeCell} from "../model/Maze"; + +export default function handleUserClicked(state: State, x: number, y: number): State { if (isClickAllowed(x, y, state)) { // Okay, we clicked a cell that's adjacent to the end of the userpath (or which IS the end of the userpath) // and that's not blocked by a wall. Now let's see. if (-1 === state.userPath.findIndex(step => step.x === x && step.y === y)) { // The clicked cell is not yet part of the userpath --> add it. + // If it's the end tile, also show a congratulation message + const showMessage = x === state.maze.end.x && y === state.maze.end.y; return { ...state, - userPath: [...state.userPath, {x: x, y: y}] + userPath: [...state.userPath, {x, y}], + errorMessage: showMessage ? "Congratulations! You won!" : state.errorMessage }; } else { // The clicked cell IS part of the userpath. Is it the last cell of it? @@ -15,7 +22,8 @@ export default function handleUserClicked(state, x, y) { // Yes, it's the last cell of the userpath --> remove it. return { ...state, - userPath: state.userPath.filter(step => step.x !== x || step.y !== y) + userPath: state.userPath.filter(step => step.x !== x || step.y !== y), + errorMessage: null } } } @@ -24,7 +32,7 @@ export default function handleUserClicked(state, x, y) { return state; } -function isClickAllowed(x, y, state) { +function isClickAllowed(x: number, y: number, state: State): boolean { const lastCoordsFromUserPath = getLastCoordsFromUserPath(state); if (!lastCoordsFromUserPath) { // when nothing has been marked yet, we can only toggle the starting position @@ -61,11 +69,11 @@ function isClickAllowed(x, y, state) { return false; } -function getCellAt(coords, state) { +function getCellAt(coords: Coordinates, state: State): MazeCell { return state.maze.grid[coords.y][coords.x]; } -function getLastCoordsFromUserPath(state) { +function getLastCoordsFromUserPath(state: State): Coordinates | null { if (state.userPath.length > 0) { return state.userPath[state.userPath.length - 1]; } -- 2.45.2