feat: use material ui

This commit is contained in:
2025-09-30 17:21:35 +08:00
parent 431a247963
commit c1fc26df03
13 changed files with 1054 additions and 281 deletions

712
client/package-lock.json generated
View File

@@ -8,7 +8,11 @@
"name": "traveling-around-the-world-client",
"version": "0.1.0",
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@hookform/resolvers": "^3.3.4",
"@mui/icons-material": "^5.15.14",
"@mui/material": "^5.15.14",
"@tanstack/react-query": "^5.29.0",
"i18next": "^23.11.5",
"leaflet": "^1.9.4",
@@ -54,6 +58,84 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/generator": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"dependencies": {
"@babel/parser": "^7.28.3",
"@babel/types": "^7.28.2",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dependencies": {
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"dependencies": {
"@babel/types": "^7.28.4"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
@@ -62,6 +144,181 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
"integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.4",
"debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
"dependencies": {
"@emotion/memoize": "^0.9.0"
}
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
},
"node_modules/@emotion/react": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="
},
"node_modules/@emotion/styled": {
"version": "11.14.1",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/is-prop-valid": "^1.3.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2"
},
"peerDependencies": {
"@emotion/react": "^11.0.0-rc.0",
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -624,7 +881,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
@@ -634,7 +890,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -642,19 +897,244 @@
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.18.0.tgz",
"integrity": "sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.18.0.tgz",
"integrity": "sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg==",
"dependencies": {
"@babel/runtime": "^7.23.9"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^5.0.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.18.0.tgz",
"integrity": "sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/core-downloads-tracker": "^5.18.0",
"@mui/system": "^5.18.0",
"@mui/types": "~7.2.15",
"@mui/utils": "^5.17.1",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.10",
"clsx": "^2.1.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1",
"react-is": "^19.0.0",
"react-transition-group": "^4.4.5"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material/node_modules/react-is": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
"integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA=="
},
"node_modules/@mui/private-theming": {
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.17.1.tgz",
"integrity": "sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/utils": "^5.17.1",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/styled-engine": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.18.0.tgz",
"integrity": "sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.3",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
}
}
},
"node_modules/@mui/system": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.18.0.tgz",
"integrity": "sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/private-theming": "^5.17.1",
"@mui/styled-engine": "^5.18.0",
"@mui/types": "~7.2.15",
"@mui/utils": "^5.17.1",
"clsx": "^2.1.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/types": {
"version": "7.2.24",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz",
"integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==",
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils": {
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.17.1.tgz",
"integrity": "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/types": "~7.2.15",
"@types/prop-types": "^15.7.12",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^19.0.0"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/utils/node_modules/react-is": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
"integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA=="
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -700,6 +1180,15 @@
"node": ">=14"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-leaflet/core": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
@@ -1283,17 +1772,20 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"devOptional": true
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="
},
"node_modules/@types/react": {
"version": "18.3.25",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.25.tgz",
"integrity": "sha512-oSVZmGtDPmRZtVDqvdKUi/qgCsWp5IDY29wp8na8Bj4B3cc99hfNzvNhlMkVVxctkAOGUA3Km7MMpBHAnWfcIA==",
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -1308,6 +1800,14 @@
"@types/react": "^18.0.0"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
@@ -1825,6 +2325,39 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/babel-plugin-macros/node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1957,7 +2490,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"engines": {
"node": ">=6"
}
@@ -2043,6 +2575,14 @@
"node": ">= 6"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2076,6 +2616,34 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cosmiconfig/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"engines": {
"node": ">= 6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2105,8 +2673,7 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/data-view-buffer": {
"version": "1.0.2",
@@ -2163,7 +2730,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
},
@@ -2252,6 +2818,15 @@
"node": ">=6.0.0"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2284,6 +2859,14 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-abstract": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
@@ -2504,7 +3087,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -2855,6 +3437,11 @@
"node": ">=8"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -2959,7 +3546,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -3262,7 +3848,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -3270,6 +3855,14 @@
"node": ">= 0.4"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
@@ -3313,7 +3906,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -3382,6 +3974,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
},
"node_modules/is-async-function": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
@@ -3460,7 +4057,6 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"dependencies": {
"hasown": "^2.0.2"
},
@@ -3831,12 +4427,28 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -3906,8 +4518,7 @@
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/locate-path": {
"version": "6.0.0",
@@ -4005,8 +4616,7 @@
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/mz": {
"version": "2.7.0",
@@ -4070,7 +4680,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4259,7 +4868,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"dependencies": {
"callsites": "^3.0.0"
},
@@ -4267,6 +4875,23 @@
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4297,8 +4922,7 @@
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-scurry": {
"version": "1.11.1",
@@ -4320,7 +4944,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -4328,8 +4951,7 @@
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -4585,7 +5207,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -4683,8 +5304,7 @@
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-leaflet": {
"version": "4.2.1",
@@ -4729,6 +5349,21 @@
"react-dom": ">=16.8"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -4813,7 +5448,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"engines": {
"node": ">=4"
}
@@ -5140,6 +5774,14 @@
"node": ">=8"
}
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -5357,6 +5999,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
},
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -5430,7 +6077,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},

View File

@@ -22,7 +22,11 @@
"nanoid": "^5.0.7",
"@hookform/resolvers": "^3.3.4",
"i18next": "^23.11.5",
"react-i18next": "^14.1.1"
"react-i18next": "^14.1.1",
"@mui/material": "^5.15.14",
"@mui/icons-material": "^5.15.14",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0"
},
"devDependencies": {
"@types/leaflet": "^1.9.12",

View File

@@ -1,11 +1,13 @@
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { VisitProvider } from '../features/visits/context/VisitProvider';
import { useCallback, useState } from 'react';
import { ThemeProvider, CssBaseline } from '@mui/material';
import { PrimaryLayout } from '../components/layout/PrimaryLayout';
import { VisitSidebar } from '../components/layout/VisitSidebar';
import { TravelMap } from '../features/map/TravelMap';
import { VisitModal } from '../components/overlay/VisitModal';
import { useVisitModalStore } from '../state/useVisitModalStore';
import { appTheme } from '../styles/theme';
function HomePage() {
const openForCreate = useVisitModalStore((state) => state.openForCreate);
@@ -32,13 +34,16 @@ function HomePage() {
export function App() {
return (
<VisitProvider mode="local">
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
</Routes>
</BrowserRouter>
<VisitModal />
</VisitProvider>
<ThemeProvider theme={appTheme}>
<CssBaseline />
<VisitProvider mode="local">
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
</Routes>
</BrowserRouter>
<VisitModal />
</VisitProvider>
</ThemeProvider>
);
}

View File

@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next';
import { FormControl, InputLabel, MenuItem, Select } from '@mui/material';
import { SUPPORTED_LANGUAGES, type SupportedLanguage } from '../../i18n';
const LABEL_LOOKUP: Record<SupportedLanguage, string> = {
@@ -13,22 +14,24 @@ export function LanguageSwitcher() {
SUPPORTED_LANGUAGES.find((language) => i18n.language.startsWith(language)) ?? 'zh-Hant';
return (
<label className="flex items-center gap-2 text-sm text-slate-500">
<span>{t('language.label')}</span>
<select
className="rounded border border-slate-300 bg-white px-2 py-1 text-sm"
<FormControl size="small">
<InputLabel id="language-switcher-label">{t('language.label')}</InputLabel>
<Select
labelId="language-switcher-label"
label={t('language.label')}
value={currentLanguage}
onChange={(event) => {
const selected = event.target.value as SupportedLanguage;
void i18n.changeLanguage(selected);
}}
sx={{ minWidth: 140 }}
>
{SUPPORTED_LANGUAGES.map((language) => (
<option key={language} value={language}>
<MenuItem key={language} value={language}>
{t(LABEL_LOOKUP[language])}
</option>
</MenuItem>
))}
</select>
</label>
</Select>
</FormControl>
);
}

View File

@@ -1,5 +1,20 @@
import { useEffect, useState, type ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import {
AppBar,
Box,
Divider,
Drawer,
IconButton,
Link,
Stack,
Toolbar,
Typography,
useMediaQuery
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import { useTheme } from '@mui/material/styles';
import { LanguageSwitcher } from './LanguageSwitcher';
interface PrimaryLayoutProps {
@@ -8,61 +23,99 @@ interface PrimaryLayoutProps {
onSidebarToggle?: (isOpen: boolean) => void;
}
const SIDEBAR_WIDTH = 320;
export function PrimaryLayout({ sidebar, content, onSidebarToggle }: PrimaryLayoutProps) {
const { t } = useTranslation();
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const theme = useTheme();
const isDesktop = useMediaQuery(theme.breakpoints.up('lg'));
const [isSidebarOpen, setIsSidebarOpen] = useState(isDesktop);
useEffect(() => {
setIsSidebarOpen(isDesktop);
}, [isDesktop]);
useEffect(() => {
onSidebarToggle?.(isSidebarOpen);
}, [isSidebarOpen, onSidebarToggle]);
const toggleSidebar = () => setIsSidebarOpen((prev) => !prev);
const toolbarOffset = { xs: '56px', sm: '64px' } as const;
return (
<div className="flex h-screen flex-col">
<header className="border-b border-slate-200 bg-white">
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
<div className="flex items-center gap-3">
<button
type="button"
<Box sx={{ display: 'flex', minHeight: '100vh', flexDirection: 'column', bgcolor: 'background.default' }}>
<AppBar position="fixed" color="transparent" elevation={0} sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Toolbar sx={{ maxWidth: 1200, mx: 'auto', width: '100%' }}>
<Stack direction="row" alignItems="center" spacing={2} sx={{ flexGrow: 1 }}>
<IconButton
size="large"
edge="start"
color="primary"
aria-label={t(isSidebarOpen ? 'sidebar.toggleClose' : 'sidebar.toggleOpen')}
aria-controls="visit-sidebar"
aria-expanded={isSidebarOpen}
className="rounded bg-slate-100 p-2 text-slate-600 transition hover:bg-slate-200 focus:outline-none focus:ring-2 focus:ring-primary"
onClick={() => setIsSidebarOpen((prev) => !prev)}
onClick={toggleSidebar}
sx={{ mr: 1 }}
>
<span className="block text-xl leading-none">{isSidebarOpen ? '⟨' : '☰'}</span>
</button>
<div>
<h1 className="text-2xl font-bold">{t('common.appName')}</h1>
<p className="text-sm text-slate-500">{t('common.tagline')}</p>
</div>
</div>
<div className="flex items-center gap-4">
<nav className="flex items-center gap-3 text-sm text-slate-500">
<a href="https://www.openstreetmap.org/" target="_blank" rel="noreferrer" className="hover:text-primary">
{isSidebarOpen ? <ChevronLeftIcon /> : <MenuIcon />}
</IconButton>
<Box>
<Typography variant="h5" fontWeight={700} color="text.primary">
{t('common.appName')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('common.tagline')}
</Typography>
</Box>
</Stack>
<Stack direction="row" spacing={2} alignItems="center">
<Stack direction="row" spacing={2} sx={{ display: { xs: 'none', sm: 'flex' } }}>
<Link href="https://www.openstreetmap.org/" target="_blank" rel="noreferrer" underline="hover" color="text.secondary">
{t('nav.openStreetMap')}
</a>
<a href="https://react-leaflet.js.org/" target="_blank" rel="noreferrer" className="hover:text-primary">
</Link>
<Link href="https://react-leaflet.js.org/" target="_blank" rel="noreferrer" underline="hover" color="text.secondary">
{t('nav.reactLeaflet')}
</a>
</nav>
</Link>
</Stack>
<LanguageSwitcher />
</div>
</div>
</header>
<main className="flex flex-1 overflow-hidden">
<aside
id="visit-sidebar"
aria-hidden={!isSidebarOpen}
className={`flex-shrink-0 min-w-0 overflow-hidden transition-[flex-basis,max-width] duration-300 ease-in-out ${
isSidebarOpen
? 'basis-[25%] max-w-[25%] border-r border-slate-200'
: 'pointer-events-none basis-0 max-w-0'
}`}
</Stack>
</Toolbar>
</AppBar>
<Box sx={{ display: 'flex', flexGrow: 1, minHeight: 0, pt: toolbarOffset }}>
<Drawer
variant={isDesktop ? 'persistent' : 'temporary'}
anchor="left"
open={isSidebarOpen}
onClose={toggleSidebar}
ModalProps={{ keepMounted: true }}
sx={{
width: { lg: SIDEBAR_WIDTH },
flexShrink: 0,
'& .MuiDrawer-paper': {
width: { xs: 280, sm: SIDEBAR_WIDTH },
boxSizing: 'border-box',
borderRight: 1,
borderColor: 'divider',
bgcolor: 'background.default',
top: toolbarOffset,
height: {
xs: `calc(100% - ${toolbarOffset.xs})`,
sm: `calc(100% - ${toolbarOffset.sm})`
}
}
}}
>
{isSidebarOpen ? <div className="h-full">{sidebar}</div> : null}
</aside>
<section className="flex-1">{content}</section>
</main>
</div>
<Box sx={{ p: 2, height: '100%', overflowY: 'auto' }}>
{sidebar}
</Box>
</Drawer>
{isDesktop && (
<Divider orientation="vertical" flexItem sx={{ display: isSidebarOpen ? 'block' : 'none' }} />
)}
<Box component="main" sx={{ flexGrow: 1, minHeight: 0 }}>{content}</Box>
</Box>
</Box>
);
}

View File

@@ -1,4 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Button, Card, CardContent, Stack, Typography } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import { VisitList } from '../../features/visits/components/VisitList';
import { VisitSummary } from '../../features/visits/components/VisitSummary';
import { useVisitModalStore } from '../../state/useVisitModalStore';
@@ -9,26 +11,33 @@ export function VisitSidebar() {
const openForEdit = useVisitModalStore((state) => state.openForEdit);
return (
<aside className="flex h-full w-full flex-col gap-4 overflow-y-auto bg-slate-50 p-4">
<header className="flex items-center justify-between">
<Stack spacing={3} sx={{ height: '100%' }}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<div>
<h2 className="text-xl font-semibold">{t('sidebar.title')}</h2>
<p className="text-sm text-slate-500">{t('sidebar.subtitle')}</p>
<Typography variant="h6" fontWeight={600} color="text.primary">
{t('sidebar.title')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('sidebar.subtitle')}
</Typography>
</div>
<button
type="button"
className="rounded bg-primary px-3 py-1.5 text-sm font-semibold text-white hover:bg-primary/90"
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => openForCreate()}
sx={{ whiteSpace: 'nowrap' }}
>
{t('sidebar.add')}
</button>
</header>
</Button>
</Stack>
<VisitSummary />
<section className="flex-1">
<VisitList onEdit={openForEdit} />
</section>
</aside>
<Card sx={{ flex: 1, overflow: 'hidden', display: 'flex' }}>
<CardContent sx={{ flex: 1, overflowY: 'auto', p: 0 }}>
<VisitList onEdit={openForEdit} />
</CardContent>
</Card>
</Stack>
);
}

View File

@@ -1,4 +1,12 @@
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogContent,
DialogTitle,
IconButton,
Typography
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { useVisitModalStore } from '../../state/useVisitModalStore';
import { VisitForm } from '../../features/visits/components/VisitForm';
@@ -6,29 +14,24 @@ export function VisitModal() {
const { isOpen, close, editingVisit, initialLocation } = useVisitModalStore();
const { t } = useTranslation();
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/60 px-4 py-10">
<div className="relative w-full max-w-lg rounded-2xl bg-white p-6 shadow-2xl">
<header className="mb-4 flex items-start justify-between">
<div>
<h2 className="text-xl font-semibold">
{editingVisit ? t('sidebar.drawerTitleEdit') : t('sidebar.drawerTitleCreate')}
</h2>
<p className="text-sm text-slate-500">{t('sidebar.subtitle')}</p>
</div>
<button
type="button"
aria-label={t('sidebar.close')}
className="rounded bg-slate-100 px-2 py-1 text-sm text-slate-500 hover:bg-slate-200"
onClick={close}
>
X
</button>
</header>
<Dialog open={isOpen} onClose={close} maxWidth="sm" fullWidth>
<DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<Typography variant="h6" component="span">
{editingVisit ? t('sidebar.drawerTitleEdit') : t('sidebar.drawerTitleCreate')}
</Typography>
<Typography variant="body2" color="text.secondary" display="block">
{t('sidebar.subtitle')}
</Typography>
</div>
<IconButton aria-label={t('sidebar.close')} onClick={close} edge="end">
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent dividers>
<VisitForm editingVisit={editingVisit} initialLocation={initialLocation} onCompleted={close} onCancel={close} />
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -8,6 +8,8 @@ import { useVisitsQuery } from '../visits/hooks/useVisitQueries';
import { useTranslation } from 'react-i18next';
import { useResizeMap } from '../visits/hooks/useResizeMap';
import type { Map as LeafletMap } from 'leaflet';
import { Box, Fab, Tooltip } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
const INITIAL_POSITION: [number, number] = [20, 0];
const INITIAL_ZOOM = 2;
@@ -43,7 +45,7 @@ export function TravelMap({ onTriggerCreate, sidebarOpen = true }: TravelMapProp
useResizeMap(mapRef, [sidebarOpen]);
return (
<div className="relative h-full w-full">
<Box sx={{ position: 'relative', height: '100%', width: '100%' }}>
<MapContainer
whenCreated={(mapInstance) => {
mapRef.current = mapInstance;
@@ -74,14 +76,15 @@ export function TravelMap({ onTriggerCreate, sidebarOpen = true }: TravelMapProp
</Marker>
))}
</MapContainer>
<button
type="button"
aria-label={t('sidebar.add')}
className="absolute bottom-5 right-5 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-primary text-2xl font-bold text-white shadow-lg transition hover:bg-primary/90"
onClick={() => (onTriggerCreate ? onTriggerCreate() : openForCreate())}
>
+
</button>
</div>
<Tooltip title={t('sidebar.add')} placement="left">
<Fab
color="primary"
sx={{ position: 'absolute', bottom: 24, right: 24, zIndex: 1000 }}
onClick={() => (onTriggerCreate ? onTriggerCreate() : openForCreate())}
>
<AddIcon />
</Fab>
</Tooltip>
</Box>
);
}

View File

@@ -2,6 +2,15 @@ import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Alert,
Box,
Button,
Grid,
Stack,
TextField,
Typography
} from '@mui/material';
import type { Visit, VisitCreateInput } from '../types';
import { useCreateVisitMutation, useUpdateVisitMutation } from '../hooks/useVisitQueries';
import { useTranslation } from 'react-i18next';
@@ -171,114 +180,107 @@ export function VisitForm({ editingVisit, onCompleted, onCancel, initialLocation
}
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid gap-2">
<label className="text-sm font-medium" htmlFor="country">
{t('form.country')}
</label>
<input
id="country"
className="rounded border border-slate-300 px-3 py-2"
<Box component="form" onSubmit={handleSubmit(onSubmit)} noValidate>
<Stack spacing={3}>
<TextField
label={t('form.country')}
placeholder={t('form.countryPlaceholder')}
{...register('country')}
error={Boolean(errors.country)}
helperText={errors.country ? t(errors.country.message ?? '') : ''}
fullWidth
/>
{errors.country ? (
<p className="text-sm text-rose-500">{t(errors.country.message ?? '')}</p>
) : null}
</div>
<div className="grid gap-2">
<label className="text-sm font-medium" htmlFor="city">
{t('form.city')}
</label>
<input
id="city"
className="rounded border border-slate-300 px-3 py-2"
<TextField
label={t('form.city')}
placeholder={t('form.cityPlaceholder')}
{...register('city')}
error={Boolean(errors.city)}
helperText={errors.city ? t(errors.city.message ?? '') : ''}
fullWidth
/>
{errors.city ? <p className="text-sm text-rose-500">{t(errors.city.message ?? '')}</p> : null}
</div>
<div className="grid gap-2">
<label className="text-sm font-medium" htmlFor="start">
{t('form.startDate')}
</label>
<input id="start" type="date" className="rounded border border-slate-300 px-3 py-2" {...register('start')} />
{errors.start ? <p className="text-sm text-rose-500">{t(errors.start.message ?? '')}</p> : null}
</div>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<TextField
label={t('form.startDate')}
type="date"
InputLabelProps={{ shrink: true }}
{...register('start')}
error={Boolean(errors.start)}
helperText={errors.start ? t(errors.start.message ?? '') : ''}
fullWidth
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
label={t('form.endDate')}
type="date"
InputLabelProps={{ shrink: true }}
{...register('end')}
error={Boolean(errors.end)}
helperText={errors.end ? t(errors.end.message ?? '') : ''}
fullWidth
/>
</Grid>
</Grid>
<div className="grid gap-2">
<label className="text-sm font-medium" htmlFor="end">
{t('form.endDate')}
</label>
<input id="end" type="date" className="rounded border border-slate-300 px-3 py-2" {...register('end')} />
{errors.end ? <p className="text-sm text-rose-500">{t(errors.end.message ?? '')}</p> : null}
</div>
<div className="grid gap-2">
<label className="text-sm font-medium" htmlFor="notes">
{t('form.notes')}
</label>
<textarea
id="notes"
className="min-h-[96px] rounded border border-slate-300 px-3 py-2"
<TextField
label={t('form.notes')}
placeholder={t('form.notesPlaceholder')}
{...register('notes')}
error={Boolean(errors.notes)}
helperText={errors.notes ? t(errors.notes.message ?? '') : ''}
fullWidth
multiline
minRows={3}
/>
{errors.notes ? <p className="text-sm text-rose-500">{t(errors.notes.message ?? '')}</p> : null}
</div>
<div className="grid gap-2 md:grid-cols-2">
<div>
<label className="text-sm font-medium" htmlFor="lat">
{t('form.latitude')}
</label>
<input
id="lat"
type="number"
step="any"
className="w-full rounded border border-slate-300 px-3 py-2"
{...register('lat', { valueAsNumber: true })}
/>
{errors.lat ? <p className="text-sm text-rose-500">{t(errors.lat.message ?? '')}</p> : null}
</div>
<div>
<label className="text-sm font-medium" htmlFor="lng">
{t('form.longitude')}
</label>
<input
id="lng"
type="number"
step="any"
className="w-full rounded border border-slate-300 px-3 py-2"
{...register('lng', { valueAsNumber: true })}
/>
{errors.lng ? <p className="text-sm text-rose-500">{t(errors.lng.message ?? '')}</p> : null}
</div>
</div>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<TextField
label={t('form.latitude')}
type="number"
inputProps={{ step: 'any' }}
{...register('lat', { valueAsNumber: true })}
error={Boolean(errors.lat)}
helperText={errors.lat ? t(errors.lat.message ?? '') : ''}
fullWidth
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
label={t('form.longitude')}
type="number"
inputProps={{ step: 'any' }}
{...register('lng', { valueAsNumber: true })}
error={Boolean(errors.lng)}
helperText={errors.lng ? t(errors.lng.message ?? '') : ''}
fullWidth
/>
</Grid>
</Grid>
{isGeocoding ? <p className="text-xs text-slate-500">{t('geocoding.detecting')}</p> : null}
{!isGeocoding && geocodeFailed ? (
<p className="text-xs text-rose-500">{t('geocoding.failed')}</p>
) : null}
{isGeocoding ? (
<Typography variant="caption" color="text.secondary">
{t('geocoding.detecting')}
</Typography>
) : null}
{!isGeocoding && geocodeFailed ? (
<Alert severity="warning" variant="outlined">
{t('geocoding.failed')}
</Alert>
) : null}
<div className="flex justify-end gap-2">
<button
type="button"
className="rounded border border-slate-300 px-4 py-2"
onClick={() => onCancel?.()}
>
{t('form.cancel')}
</button>
<button
type="submit"
disabled={isSubmitting}
className="rounded bg-primary px-4 py-2 font-semibold text-white hover:bg-primary/90 disabled:opacity-60"
>
{editingVisit ? t('form.update') : t('form.create')}
</button>
</div>
</form>
<Stack direction="row" justifyContent="flex-end" spacing={2}>
<Button variant="outlined" onClick={() => onCancel?.()}>
{t('form.cancel')}
</Button>
<Button type="submit" variant="contained" disabled={isSubmitting}>
{editingVisit ? t('form.update') : t('form.create')}
</Button>
</Stack>
</Stack>
</Box>
);
}

View File

@@ -1,4 +1,7 @@
import { useTranslation } from 'react-i18next';
import { Button, Card, CardActions, CardContent, CircularProgress, Stack, Typography } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import { useDeleteVisitMutation, useVisitsQuery } from '../hooks/useVisitQueries';
import type { Visit } from '../types';
@@ -12,48 +15,58 @@ export function VisitList({ onEdit }: VisitListProps) {
const { t } = useTranslation();
if (isLoading) {
return <p className="text-sm text-slate-500">{t('list.loading')}</p>;
return (
<Stack direction="row" spacing={1} alignItems="center" justifyContent="center" sx={{ py: 4 }}>
<CircularProgress size={20} />
<Typography variant="body2" color="text.secondary">
{t('list.loading')}
</Typography>
</Stack>
);
}
if (!visits.length) {
return <p className="text-sm text-slate-500">{t('list.empty')}</p>;
return (
<Typography variant="body2" color="text.secondary" sx={{ p: 3 }}>
{t('list.empty')}
</Typography>
);
}
return (
<ul className="space-y-3">
<Stack spacing={2} sx={{ p: 2 }}>
{visits.map((visit) => (
<li key={visit.id} className="rounded-lg bg-white p-3 shadow-sm">
<div className="flex justify-between gap-3">
<div className="space-y-1">
<h3 className="text-lg font-semibold">
{visit.location.country}
{visit.location.city ? ` · ${visit.location.city}` : null}
</h3>
<p className="text-sm text-slate-500">
{visit.date.start}
{visit.date.end ? `${visit.date.end}` : null}
</p>
{visit.notes ? <p className="text-sm">{visit.notes}</p> : null}
</div>
<div className="flex shrink-0 flex-col items-end gap-2 text-sm">
<button
type="button"
className="rounded bg-slate-100 px-2 py-1 hover:bg-slate-200"
onClick={() => onEdit(visit)}
>
{t('list.edit')}
</button>
<button
type="button"
className="rounded bg-rose-100 px-2 py-1 text-rose-600 hover:bg-rose-200"
onClick={() => deleteMutation.mutate(visit.id)}
>
{t('list.delete')}
</button>
</div>
</div>
</li>
<Card key={visit.id} variant="outlined">
<CardContent>
<Typography variant="h6" fontWeight={600} gutterBottom>
{visit.location.country}
{visit.location.city ? ` · ${visit.location.city}` : ''}
</Typography>
<Typography variant="body2" color="text.secondary">
{visit.date.start}
{visit.date.end ? `${visit.date.end}` : ''}
</Typography>
{visit.notes ? (
<Typography variant="body2" color="text.primary" sx={{ mt: 1 }}>
{visit.notes}
</Typography>
) : null}
</CardContent>
<CardActions sx={{ justifyContent: 'flex-end', gap: 1, px: 2, pb: 2 }}>
<Button startIcon={<EditIcon />} size="small" onClick={() => onEdit(visit)}>
{t('list.edit')}
</Button>
<Button
startIcon={<DeleteIcon />}
size="small"
color="error"
onClick={() => deleteMutation.mutate(visit.id)}
>
{t('list.delete')}
</Button>
</CardActions>
</Card>
))}
</ul>
</Stack>
);
}

View File

@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, Grid, Paper, Typography } from '@mui/material';
import { useVisitsQuery } from '../hooks/useVisitQueries';
export function VisitSummary() {
@@ -15,19 +16,32 @@ export function VisitSummary() {
};
}, [visits]);
const summaryItems = [
{ label: t('summary.totalVisits'), value: stats.totalVisits },
{ label: t('summary.countries'), value: stats.uniqueCountries }
];
return (
<div className="rounded-lg bg-white p-4 shadow-sm">
<h3 className="text-sm font-semibold text-slate-500">{t('summary.title')}</h3>
<dl className="mt-3 grid grid-cols-2 gap-3 text-center">
<div className="rounded bg-slate-100 p-3">
<dt className="text-xs uppercase tracking-wide text-slate-500">{t('summary.totalVisits')}</dt>
<dd className="text-xl font-bold">{stats.totalVisits}</dd>
</div>
<div className="rounded bg-slate-100 p-3">
<dt className="text-xs uppercase tracking-wide text-slate-500">{t('summary.countries')}</dt>
<dd className="text-xl font-bold">{stats.uniqueCountries}</dd>
</div>
</dl>
</div>
<Card variant="outlined">
<CardContent>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
{t('summary.title')}
</Typography>
<Grid container spacing={2}>
{summaryItems.map((item) => (
<Grid item xs={6} key={item.label}>
<Paper elevation={0} sx={{ p: 2, bgcolor: 'grey.100', textAlign: 'center' }}>
<Typography variant="overline" display="block" color="text.secondary">
{item.label}
</Typography>
<Typography variant="h5" fontWeight={700} color="text.primary">
{item.value}
</Typography>
</Paper>
</Grid>
))}
</Grid>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,18 @@
import { createTheme } from '@mui/material/styles';
export const appTheme = createTheme({
palette: {
primary: {
main: '#0b7285'
},
secondary: {
main: '#51cf66'
},
background: {
default: '#f1f5f9'
}
},
shape: {
borderRadius: 12
}
});