base commit
This commit is contained in:
Generated
+535
-1
@@ -16,9 +16,11 @@
|
|||||||
"axios": "^1.15.2",
|
"axios": "^1.15.2",
|
||||||
"effector": "^23.4.4",
|
"effector": "^23.4.4",
|
||||||
"effector-react": "^23.3.0",
|
"effector-react": "^23.3.0",
|
||||||
|
"maplibre-gl": "^5.24.0",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
"react-hook-form": "^7.74.0",
|
"react-hook-form": "^7.74.0",
|
||||||
|
"react-map-gl": "^8.1.1",
|
||||||
"react-router-dom": "^7.14.2",
|
"react-router-dom": "^7.14.2",
|
||||||
"swiper": "^12.1.3"
|
"swiper": "^12.1.3"
|
||||||
},
|
},
|
||||||
@@ -728,6 +730,111 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/point-geometry": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/tiny-sdf": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/unitbezier": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/vector-tile": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/point-geometry": "~1.1.0",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"pbf": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mapbox/whoots-js": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@maplibre/geojson-vt": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"kdbush": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||||
|
"version": "24.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.8.1.tgz",
|
||||||
|
"integrity": "sha512-zxa92qF96ZNojLxeAjnaRpjVCy+swoUNJvDhtpC90k7u5F0TMr4GmvNqMKvYrMoPB8d7gRSXbMG1hBbmgESIsw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||||
|
"@mapbox/unitbezier": "^0.0.1",
|
||||||
|
"json-stringify-pretty-compact": "^4.0.0",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"quickselect": "^3.0.0",
|
||||||
|
"rw": "^1.3.3",
|
||||||
|
"tinyqueue": "^3.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"gl-style-format": "dist/gl-style-format.mjs",
|
||||||
|
"gl-style-migrate": "dist/gl-style-migrate.mjs",
|
||||||
|
"gl-style-validate": "dist/gl-style-validate.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@maplibre/mlt": {
|
||||||
|
"version": "1.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.9.tgz",
|
||||||
|
"integrity": "sha512-g/tD8EYJB97udq33ipuJ9a4Q7fcbZnTEnUrgnEc/tLMmEL+zaCbR+X5fkDBO2dgpaAMsLH179qE3UXg2N0Nc/g==",
|
||||||
|
"license": "(MIT OR Apache-2.0)",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/point-geometry": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@maplibre/vt-pbf": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/point-geometry": "^1.1.0",
|
||||||
|
"@mapbox/vector-tile": "^2.0.4",
|
||||||
|
"@maplibre/geojson-vt": "^5.0.4",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"@types/supercluster": "^7.1.3",
|
||||||
|
"pbf": "^4.0.1",
|
||||||
|
"supercluster": "^8.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@maplibre/vt-pbf/node_modules/@maplibre/geojson-vt": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/@mui/core-downloads-tracker": {
|
"node_modules/@mui/core-downloads-tracker": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.0.0.tgz",
|
||||||
@@ -1335,6 +1442,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@@ -1393,6 +1506,15 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/supercluster": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.59.1",
|
"version": "8.59.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
|
||||||
@@ -1918,6 +2040,66 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@vis.gl/react-mapbox": {
|
||||||
|
"version": "8.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vis.gl/react-mapbox/-/react-mapbox-8.1.1.tgz",
|
||||||
|
"integrity": "sha512-KMDTjtWESXxHS4uqWxjsvgQUHvuL3Z6SdKe68o7Nxma2qUfuyH3x4TCkIqGn3FQTrFvZLWvTnSAbGvtm+Kd13A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"mapbox-gl": ">=3.5.0",
|
||||||
|
"react": ">=16.3.0",
|
||||||
|
"react-dom": ">=16.3.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"mapbox-gl": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vis.gl/react-maplibre": {
|
||||||
|
"version": "8.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vis.gl/react-maplibre/-/react-maplibre-8.1.1.tgz",
|
||||||
|
"integrity": "sha512-iUOfzJAhFAJwEZp1644tQb7LOTFgi5/GzdaztkhzNgFVuoF2Ez7guvwZjQAKB9CN2TlHTgNuYH8UW85kO7cVhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@maplibre/maplibre-gl-style-spec": "^19.2.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"maplibre-gl": ">=4.0.0",
|
||||||
|
"react": ">=16.3.0",
|
||||||
|
"react-dom": ">=16.3.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"maplibre-gl": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vis.gl/react-maplibre/node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||||
|
"version": "19.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz",
|
||||||
|
"integrity": "sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||||
|
"@mapbox/unitbezier": "^0.0.1",
|
||||||
|
"json-stringify-pretty-compact": "^3.0.0",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"rw": "^1.3.3",
|
||||||
|
"sort-object": "^3.0.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"gl-style-format": "dist/gl-style-format.mjs",
|
||||||
|
"gl-style-migrate": "dist/gl-style-migrate.mjs",
|
||||||
|
"gl-style-validate": "dist/gl-style-validate.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vis.gl/react-maplibre/node_modules/json-stringify-pretty-compact": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
|
||||||
@@ -2010,6 +2192,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/arr-union": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/array-buffer-byte-length": {
|
"node_modules/array-buffer-byte-length": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
|
||||||
@@ -2148,6 +2339,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/assign-symbols": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ast-types-flow": {
|
"node_modules/ast-types-flow": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
||||||
@@ -2316,6 +2516,25 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bytewise": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytewise-core": "^1.2.2",
|
||||||
|
"typewise": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bytewise-core": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytewise-core/-/bytewise-core-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"typewise-core": "^1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
|
||||||
@@ -2712,6 +2931,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/earcut": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/effector": {
|
"node_modules/effector": {
|
||||||
"version": "23.4.4",
|
"version": "23.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/effector/-/effector-23.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/effector/-/effector-23.4.4.tgz",
|
||||||
@@ -3535,6 +3760,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/extend-shallow": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-extendable": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -3846,6 +4083,21 @@
|
|||||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-value": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gl-matrix": {
|
||||||
|
"version": "3.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||||
|
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -4270,6 +4522,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-extendable": {
|
||||||
|
"version": "0.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -4382,6 +4643,18 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-plain-object": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"isobject": "^3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-regex": {
|
"node_modules/is-regex": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||||
@@ -4541,6 +4814,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/isobject": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iterator.prototype": {
|
"node_modules/iterator.prototype": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
|
||||||
@@ -4604,6 +4886,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/json-stringify-pretty-compact": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
@@ -4633,6 +4921,12 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kdbush": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -4982,6 +5276,40 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/maplibre-gl": {
|
||||||
|
"version": "5.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz",
|
||||||
|
"integrity": "sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||||
|
"@mapbox/point-geometry": "^1.1.0",
|
||||||
|
"@mapbox/tiny-sdf": "^2.1.0",
|
||||||
|
"@mapbox/unitbezier": "^0.0.1",
|
||||||
|
"@mapbox/vector-tile": "^2.0.4",
|
||||||
|
"@mapbox/whoots-js": "^3.1.0",
|
||||||
|
"@maplibre/geojson-vt": "^6.1.0",
|
||||||
|
"@maplibre/maplibre-gl-style-spec": "^24.8.1",
|
||||||
|
"@maplibre/mlt": "^1.1.8",
|
||||||
|
"@maplibre/vt-pbf": "^4.3.0",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
|
"earcut": "^3.0.2",
|
||||||
|
"gl-matrix": "^3.4.4",
|
||||||
|
"kdbush": "^4.0.2",
|
||||||
|
"murmurhash-js": "^1.0.0",
|
||||||
|
"pbf": "^4.0.1",
|
||||||
|
"potpack": "^2.1.0",
|
||||||
|
"quickselect": "^3.0.0",
|
||||||
|
"tinyqueue": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.14.0",
|
||||||
|
"npm": ">=8.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -5059,7 +5387,6 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@@ -5071,6 +5398,12 @@
|
|||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/murmurhash-js": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
@@ -5386,6 +5719,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pbf": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-protobuf-schema": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pbf": "bin/pbf"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -5444,6 +5789,12 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/potpack": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -5500,6 +5851,12 @@
|
|||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/protocol-buffers-schema": {
|
||||||
|
"version": "3.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
|
||||||
|
"integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
@@ -5519,6 +5876,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/quickselect": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.5",
|
"version": "19.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
||||||
@@ -5562,6 +5925,30 @@
|
|||||||
"integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==",
|
"integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-map-gl": {
|
||||||
|
"version": "8.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-8.1.1.tgz",
|
||||||
|
"integrity": "sha512-aSqFAFoxvY7wxbGI93Dz0E41171mkAb3GcNbnkFIotmu88OFw495os6mIDZSi7irYNT/PZEIOEHUxhun4ToGuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vis.gl/react-mapbox": "8.1.1",
|
||||||
|
"@vis.gl/react-maplibre": "8.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"mapbox-gl": ">=1.13.0",
|
||||||
|
"maplibre-gl": ">=1.13.0",
|
||||||
|
"react": ">=16.3.0",
|
||||||
|
"react-dom": ">=16.3.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"mapbox-gl": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"maplibre-gl": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.14.2",
|
"version": "7.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",
|
||||||
@@ -5700,6 +6087,15 @@
|
|||||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resolve-protobuf-schema": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"protocol-buffers-schema": "^3.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.0-rc.17",
|
"version": "1.0.0-rc.17",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
||||||
@@ -5741,6 +6137,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/rw": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/safe-array-concat": {
|
"node_modules/safe-array-concat": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
|
||||||
@@ -5867,6 +6269,21 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-value": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"extend-shallow": "^2.0.1",
|
||||||
|
"is-extendable": "^0.1.1",
|
||||||
|
"is-plain-object": "^2.0.3",
|
||||||
|
"split-string": "^3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@@ -5966,6 +6383,41 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sort-asc": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sort-desc": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sort-object": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sort-object/-/sort-object-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytewise": "^1.1.0",
|
||||||
|
"get-value": "^2.0.2",
|
||||||
|
"is-extendable": "^0.1.1",
|
||||||
|
"sort-asc": "^0.2.0",
|
||||||
|
"sort-desc": "^0.2.0",
|
||||||
|
"union-value": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.5.7",
|
"version": "0.5.7",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||||
@@ -5985,6 +6437,43 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/split-string": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"extend-shallow": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/split-string/node_modules/extend-shallow": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"assign-symbols": "^1.0.0",
|
||||||
|
"is-extendable": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/split-string/node_modules/is-extendable": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-plain-object": "^2.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stable-hash-x": {
|
"node_modules/stable-hash-x": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz",
|
||||||
@@ -6128,6 +6617,15 @@
|
|||||||
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
|
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/supercluster": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"kdbush": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
@@ -6205,6 +6703,12 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyqueue": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -6368,6 +6872,21 @@
|
|||||||
"typescript": ">=4.8.4 <6.1.0"
|
"typescript": ">=4.8.4 <6.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typewise": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"typewise-core": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typewise-core": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/uglify-js": {
|
"node_modules/uglify-js": {
|
||||||
"version": "3.19.3",
|
"version": "3.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||||
@@ -6408,6 +6927,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/union-value": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"arr-union": "^3.1.0",
|
||||||
|
"get-value": "^2.0.6",
|
||||||
|
"is-extendable": "^0.1.1",
|
||||||
|
"set-value": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unrs-resolver": {
|
"node_modules/unrs-resolver": {
|
||||||
"version": "1.11.1",
|
"version": "1.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
|
||||||
|
|||||||
@@ -21,9 +21,11 @@
|
|||||||
"axios": "^1.15.2",
|
"axios": "^1.15.2",
|
||||||
"effector": "^23.4.4",
|
"effector": "^23.4.4",
|
||||||
"effector-react": "^23.3.0",
|
"effector-react": "^23.3.0",
|
||||||
|
"maplibre-gl": "^5.24.0",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
"react-hook-form": "^7.74.0",
|
"react-hook-form": "^7.74.0",
|
||||||
|
"react-map-gl": "^8.1.1",
|
||||||
"react-router-dom": "^7.14.2",
|
"react-router-dom": "^7.14.2",
|
||||||
"swiper": "^12.1.3"
|
"swiper": "^12.1.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { AdminPage } from '@/pages/admin'
|
|||||||
import { AdminUsersPage } from '@/pages/admin-users'
|
import { AdminUsersPage } from '@/pages/admin-users'
|
||||||
import { AuthPage } from '@/pages/auth'
|
import { AuthPage } from '@/pages/auth'
|
||||||
import { HomePage } from '@/pages/home'
|
import { HomePage } from '@/pages/home'
|
||||||
import { MePage } from '@/pages/me/ui/MePage'
|
import { MeLayoutPage } from '@/pages/me'
|
||||||
import { ProductPage } from '@/pages/product'
|
import { ProductPage } from '@/pages/product'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
@@ -18,7 +18,7 @@ export function App() {
|
|||||||
<Route path="/admin" element={<AdminPage />} />
|
<Route path="/admin" element={<AdminPage />} />
|
||||||
<Route path="/admin/users" element={<AdminUsersPage />} />
|
<Route path="/admin/users" element={<AdminUsersPage />} />
|
||||||
<Route path="/auth" element={<AuthPage />} />
|
<Route path="/auth" element={<AuthPage />} />
|
||||||
<Route path="/me" element={<MePage />} />
|
<Route path="/me/*" element={<MeLayoutPage />} />
|
||||||
<Route path="/products/:id" element={<ProductPage />} />
|
<Route path="/products/:id" element={<ProductPage />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import type { ShippingAddress } from '@/entities/user/model/types'
|
||||||
|
import { apiClient } from '@/shared/api/client'
|
||||||
|
|
||||||
|
export type AddressesListResponse = { items: ShippingAddress[] }
|
||||||
|
|
||||||
|
export async function fetchMyAddresses(): Promise<AddressesListResponse> {
|
||||||
|
const { data } = await apiClient.get<AddressesListResponse>('me/addresses')
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMyAddress(body: {
|
||||||
|
label?: string | null
|
||||||
|
recipientName: string
|
||||||
|
recipientPhone: string
|
||||||
|
addressLine: string
|
||||||
|
comment?: string | null
|
||||||
|
lat: number
|
||||||
|
lng: number
|
||||||
|
isDefault?: boolean
|
||||||
|
}): Promise<{ item: ShippingAddress }> {
|
||||||
|
const { data } = await apiClient.post<{ item: ShippingAddress }>('me/addresses', body)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateMyAddress(
|
||||||
|
id: string,
|
||||||
|
body: Partial<{
|
||||||
|
label: string | null
|
||||||
|
recipientName: string
|
||||||
|
recipientPhone: string
|
||||||
|
addressLine: string
|
||||||
|
comment: string | null
|
||||||
|
lat: number
|
||||||
|
lng: number
|
||||||
|
isDefault: boolean
|
||||||
|
}>,
|
||||||
|
): Promise<{ item: ShippingAddress }> {
|
||||||
|
const { data } = await apiClient.patch<{ item: ShippingAddress }>(`me/addresses/${id}`, body)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteMyAddress(id: string): Promise<void> {
|
||||||
|
await apiClient.delete(`me/addresses/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setMyAddressDefault(id: string): Promise<{ item: ShippingAddress }> {
|
||||||
|
const { data } = await apiClient.post<{ item: ShippingAddress }>(`me/addresses/${id}/default`)
|
||||||
|
return data
|
||||||
|
}
|
||||||
@@ -6,3 +6,17 @@ export type AdminUser = {
|
|||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ShippingAddress = {
|
||||||
|
id: string
|
||||||
|
label: string | null
|
||||||
|
recipientName: string
|
||||||
|
recipientPhone: string
|
||||||
|
addressLine: string
|
||||||
|
comment: string | null
|
||||||
|
lat: number
|
||||||
|
lng: number
|
||||||
|
isDefault: boolean
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress'
|
||||||
|
import List from '@mui/material/List'
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton'
|
||||||
|
import ListItemText from '@mui/material/ListItemText'
|
||||||
|
import Stack from '@mui/material/Stack'
|
||||||
|
import TextField from '@mui/material/TextField'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import * as maplibregl from 'maplibre-gl'
|
||||||
|
import Map, { Marker } from 'react-map-gl/maplibre'
|
||||||
|
import type { MapMouseEvent } from 'react-map-gl/maplibre'
|
||||||
|
|
||||||
|
type NominatimItem = { display_name: string; lat: string; lon: string }
|
||||||
|
|
||||||
|
async function reverseGeocode(pos: { lat: number; lng: number }): Promise<string | null> {
|
||||||
|
const url = new URL('https://nominatim.openstreetmap.org/reverse')
|
||||||
|
url.searchParams.set('format', 'jsonv2')
|
||||||
|
url.searchParams.set('lat', String(pos.lat))
|
||||||
|
url.searchParams.set('lon', String(pos.lng))
|
||||||
|
url.searchParams.set('accept-language', 'ru')
|
||||||
|
const res = await fetch(url.toString(), { headers: { 'User-Agent': 'craftshop-demo' } })
|
||||||
|
if (!res.ok) return null
|
||||||
|
const data = (await res.json()) as { display_name?: string }
|
||||||
|
return data.display_name ? String(data.display_name) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
type LatLng = { lat: number; lng: number }
|
||||||
|
|
||||||
|
async function searchPlaces(q: string, signal?: AbortSignal): Promise<NominatimItem[]> {
|
||||||
|
const url = new URL('https://nominatim.openstreetmap.org/search')
|
||||||
|
url.searchParams.set('format', 'jsonv2')
|
||||||
|
url.searchParams.set('q', q)
|
||||||
|
url.searchParams.set('accept-language', 'ru')
|
||||||
|
url.searchParams.set('limit', '5')
|
||||||
|
const res = await fetch(url.toString(), {
|
||||||
|
headers: { 'User-Agent': 'craftshop-demo' },
|
||||||
|
signal,
|
||||||
|
})
|
||||||
|
if (!res.ok) return []
|
||||||
|
const data = (await res.json()) as NominatimItem[]
|
||||||
|
return Array.isArray(data) ? data : []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddressMapPicker(props: {
|
||||||
|
value: { lat: number; lng: number } | null
|
||||||
|
onChange: (v: { lat: number; lng: number; addressLine?: string | null }) => void
|
||||||
|
}) {
|
||||||
|
const { value, onChange } = props
|
||||||
|
const [q, setQ] = useState('')
|
||||||
|
const [searching, setSearching] = useState(false)
|
||||||
|
const [results, setResults] = useState<NominatimItem[]>([])
|
||||||
|
const [hint, setHint] = useState<string | null>(null)
|
||||||
|
const abortRef = useRef<AbortController | null>(null)
|
||||||
|
const lastQueryRef = useRef<string>('')
|
||||||
|
const lastRequestAtRef = useRef<number>(0)
|
||||||
|
|
||||||
|
const qTrimmed = q.trim()
|
||||||
|
const visibleResults = qTrimmed.length >= 3 ? results : []
|
||||||
|
|
||||||
|
const center = useMemo(() => {
|
||||||
|
if (value) return { lat: value.lat, lng: value.lng }
|
||||||
|
return { lat: 55.751244, lng: 37.618423 } // Москва (fallback)
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
const pick = async (pos: LatLng) => {
|
||||||
|
setHint(null)
|
||||||
|
onChange({ lat: pos.lat, lng: pos.lng })
|
||||||
|
try {
|
||||||
|
const addr = await reverseGeocode(pos)
|
||||||
|
if (addr) {
|
||||||
|
setHint(addr)
|
||||||
|
onChange({ lat: pos.lat, lng: pos.lng, addressLine: addr })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const s = qTrimmed
|
||||||
|
if (s.length < 3) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = window.setTimeout(async () => {
|
||||||
|
// throttle: не чаще 1 запроса в 900ms
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastRequestAtRef.current < 900) return
|
||||||
|
if (s === lastQueryRef.current) return
|
||||||
|
|
||||||
|
lastQueryRef.current = s
|
||||||
|
lastRequestAtRef.current = now
|
||||||
|
|
||||||
|
abortRef.current?.abort()
|
||||||
|
const ac = new AbortController()
|
||||||
|
abortRef.current = ac
|
||||||
|
|
||||||
|
setSearching(true)
|
||||||
|
try {
|
||||||
|
setResults(await searchPlaces(s, ac.signal))
|
||||||
|
} catch (e) {
|
||||||
|
if ((e as { name?: string })?.name !== 'AbortError') {
|
||||||
|
setResults([])
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setSearching(false)
|
||||||
|
}
|
||||||
|
}, 450)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(t)
|
||||||
|
}
|
||||||
|
}, [qTrimmed])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
<Typography variant="subtitle2">Выбор на карте</Typography>
|
||||||
|
|
||||||
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5}>
|
||||||
|
<TextField size="small" label="Найти адрес" value={q} onChange={(e) => setQ(e.target.value)} fullWidth />
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={async () => {
|
||||||
|
const s = q.trim()
|
||||||
|
if (!s) return
|
||||||
|
abortRef.current?.abort()
|
||||||
|
const ac = new AbortController()
|
||||||
|
abortRef.current = ac
|
||||||
|
setSearching(true)
|
||||||
|
try {
|
||||||
|
lastQueryRef.current = s
|
||||||
|
lastRequestAtRef.current = Date.now()
|
||||||
|
setResults(await searchPlaces(s, ac.signal))
|
||||||
|
} finally {
|
||||||
|
setSearching(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={searching || !q.trim()}
|
||||||
|
sx={{ minWidth: 160 }}
|
||||||
|
>
|
||||||
|
{searching ? <CircularProgress size={18} /> : 'Найти'}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{visibleResults.length > 0 && (
|
||||||
|
<List dense sx={{ border: 1, borderColor: 'divider', borderRadius: 2 }}>
|
||||||
|
{visibleResults.map((r) => (
|
||||||
|
<ListItemButton
|
||||||
|
key={`${r.lat}:${r.lon}:${r.display_name}`}
|
||||||
|
onClick={() => {
|
||||||
|
const lat = Number(r.lat)
|
||||||
|
const lng = Number(r.lon)
|
||||||
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return
|
||||||
|
void pick({ lat, lng })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemText primary={r.display_name} />
|
||||||
|
</ListItemButton>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: 280,
|
||||||
|
borderRadius: 2,
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Map
|
||||||
|
mapLib={maplibregl}
|
||||||
|
initialViewState={{ latitude: center.lat, longitude: center.lng, zoom: 12 }}
|
||||||
|
style={{ width: '100%', height: 280 }}
|
||||||
|
mapStyle={{
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
osm: {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [{ id: 'osm', type: 'raster', source: 'osm' }],
|
||||||
|
}}
|
||||||
|
onClick={(e: MapMouseEvent) => {
|
||||||
|
const { lng, lat } = e.lngLat
|
||||||
|
void pick({ lat, lng })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value && (
|
||||||
|
<Marker
|
||||||
|
longitude={value.lng}
|
||||||
|
latitude={value.lat}
|
||||||
|
draggable
|
||||||
|
onDragEnd={(e) => {
|
||||||
|
const { lng, lat } = e.lngLat
|
||||||
|
void pick({ lat, lng })
|
||||||
|
}}
|
||||||
|
anchor="bottom"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
borderRadius: '50%',
|
||||||
|
border: 2,
|
||||||
|
borderColor: 'background.paper',
|
||||||
|
boxShadow: 3,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Marker>
|
||||||
|
)}
|
||||||
|
</Map>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ minHeight: 18 }}>
|
||||||
|
{hint && (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Подсказка адреса: {hint}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { StrictMode } from 'react'
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { App } from '@/app/App'
|
import { App } from '@/app/App'
|
||||||
import '@/app/styles/global.css'
|
import '@/app/styles/global.css'
|
||||||
|
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||||
import { readStoredToken, tokenSet } from '@/shared/model/auth'
|
import { readStoredToken, tokenSet } from '@/shared/model/auth'
|
||||||
|
|
||||||
tokenSet(readStoredToken())
|
tokenSet(readStoredToken())
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
@@ -8,10 +8,12 @@ import TextField from '@mui/material/TextField'
|
|||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useMutation } from '@tanstack/react-query'
|
import { useMutation } from '@tanstack/react-query'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { useUnit } from 'effector-react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { apiClient } from '@/shared/api/client'
|
import { apiClient } from '@/shared/api/client'
|
||||||
import { tokenSet } from '@/shared/model/auth'
|
import { $user, tokenSet } from '@/shared/model/auth'
|
||||||
|
|
||||||
type AuthResponse = { token: string; user: { id: string; email: string } }
|
type AuthResponse = { token: string; user: { id: string; email: string; name?: string | null; phone?: string | null } }
|
||||||
|
|
||||||
function getApiErrorMessage(err: unknown): string | null {
|
function getApiErrorMessage(err: unknown): string | null {
|
||||||
if (!err || typeof err !== 'object') return null
|
if (!err || typeof err !== 'object') return null
|
||||||
@@ -24,6 +26,8 @@ function getApiErrorMessage(err: unknown): string | null {
|
|||||||
|
|
||||||
export function AuthPage() {
|
export function AuthPage() {
|
||||||
const [message, setMessage] = useState<string | null>(null)
|
const [message, setMessage] = useState<string | null>(null)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const user = useUnit($user)
|
||||||
const { register, watch } = useForm<{
|
const { register, watch } = useForm<{
|
||||||
email: string
|
email: string
|
||||||
code: string
|
code: string
|
||||||
@@ -37,6 +41,10 @@ export function AuthPage() {
|
|||||||
const code = watch('code')
|
const code = watch('code')
|
||||||
const password = watch('password')
|
const password = watch('password')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) navigate('/', { replace: true })
|
||||||
|
}, [navigate, user])
|
||||||
|
|
||||||
const requestCode = useMutation({
|
const requestCode = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
await apiClient.post('auth/request-code', { email })
|
await apiClient.post('auth/request-code', { email })
|
||||||
@@ -49,6 +57,7 @@ export function AuthPage() {
|
|||||||
const { data } = await apiClient.post<AuthResponse>('auth/verify-code', { email, code })
|
const { data } = await apiClient.post<AuthResponse>('auth/verify-code', { email, code })
|
||||||
tokenSet(data.token)
|
tokenSet(data.token)
|
||||||
setMessage(`Вход выполнен: ${data.user.email}`)
|
setMessage(`Вход выполнен: ${data.user.email}`)
|
||||||
|
navigate('/', { replace: true })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -57,6 +66,7 @@ export function AuthPage() {
|
|||||||
const { data } = await apiClient.post<AuthResponse>('auth/register', { email, password })
|
const { data } = await apiClient.post<AuthResponse>('auth/register', { email, password })
|
||||||
tokenSet(data.token)
|
tokenSet(data.token)
|
||||||
setMessage(`Регистрация выполнена: ${data.user.email}`)
|
setMessage(`Регистрация выполнена: ${data.user.email}`)
|
||||||
|
navigate('/', { replace: true })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -65,6 +75,7 @@ export function AuthPage() {
|
|||||||
const { data } = await apiClient.post<AuthResponse>('auth/login', { email, password })
|
const { data } = await apiClient.post<AuthResponse>('auth/login', { email, password })
|
||||||
tokenSet(data.token)
|
tokenSet(data.token)
|
||||||
setMessage(`Вход выполнен: ${data.user.email}`)
|
setMessage(`Вход выполнен: ${data.user.email}`)
|
||||||
|
navigate('/', { replace: true })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import Alert from '@mui/material/Alert'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Collapse from '@mui/material/Collapse'
|
import Collapse from '@mui/material/Collapse'
|
||||||
|
import Divider from '@mui/material/Divider'
|
||||||
import FormControl from '@mui/material/FormControl'
|
import FormControl from '@mui/material/FormControl'
|
||||||
import Grid from '@mui/material/Grid'
|
import Grid from '@mui/material/Grid'
|
||||||
import InputLabel from '@mui/material/InputLabel'
|
import InputLabel from '@mui/material/InputLabel'
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
import Pagination from '@mui/material/Pagination'
|
import Pagination from '@mui/material/Pagination'
|
||||||
|
import Paper from '@mui/material/Paper'
|
||||||
import Select from '@mui/material/Select'
|
import Select from '@mui/material/Select'
|
||||||
import type { SelectChangeEvent } from '@mui/material/Select'
|
import type { SelectChangeEvent } from '@mui/material/Select'
|
||||||
import Skeleton from '@mui/material/Skeleton'
|
import Skeleton from '@mui/material/Skeleton'
|
||||||
@@ -235,26 +237,58 @@ export function HomePage() {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<Box sx={{ flexGrow: 1, minWidth: { xs: '100%', md: 260 } }}>
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
|
|
||||||
Масштаб карточек
|
|
||||||
</Typography>
|
|
||||||
<ToggleButtonGroup
|
|
||||||
exclusive
|
|
||||||
size="small"
|
|
||||||
value={cardScale}
|
|
||||||
onChange={(_, v) => {
|
|
||||||
if (v === 70 || v === 90 || v === 110 || v === 130) setCardScale(v)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ToggleButton value={70}>S</ToggleButton>
|
|
||||||
<ToggleButton value={90}>M</ToggleButton>
|
|
||||||
<ToggleButton value={110}>L</ToggleButton>
|
|
||||||
<ToggleButton value={130}>XL</ToggleButton>
|
|
||||||
</ToggleButtonGroup>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 2 }} />
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
p: 1.5,
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
|
gap: 1.5,
|
||||||
|
alignItems: { sm: 'center' },
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2">Масштаб карточек</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Выберите размер карточек в каталоге
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ToggleButtonGroup
|
||||||
|
exclusive
|
||||||
|
size="small"
|
||||||
|
value={cardScale}
|
||||||
|
onChange={(_, v) => {
|
||||||
|
if (v === 70 || v === 90 || v === 110 || v === 130) setCardScale(v)
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
alignSelf: { xs: 'flex-start', sm: 'auto' },
|
||||||
|
'& .MuiToggleButton-root': {
|
||||||
|
px: 2,
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
textTransform: 'none',
|
||||||
|
},
|
||||||
|
'& .MuiToggleButton-root.Mui-selected': {
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
color: 'primary.contrastText',
|
||||||
|
'&:hover': { bgcolor: 'primary.dark' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value={70}>S</ToggleButton>
|
||||||
|
<ToggleButton value={90}>M</ToggleButton>
|
||||||
|
<ToggleButton value={110}>L</ToggleButton>
|
||||||
|
<ToggleButton value={130}>XL</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Paper>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { MePage } from './ui/MePage'
|
export { MeLayoutPage } from './ui/MeLayoutPage'
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import ChatOutlinedIcon from '@mui/icons-material/ChatOutlined'
|
||||||
|
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'
|
||||||
|
import MenuOutlinedIcon from '@mui/icons-material/MenuOutlined'
|
||||||
|
import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined'
|
||||||
|
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'
|
||||||
|
import Alert from '@mui/material/Alert'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Divider from '@mui/material/Divider'
|
||||||
|
import Drawer from '@mui/material/Drawer'
|
||||||
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
import List from '@mui/material/List'
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton'
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||||
|
import ListItemText from '@mui/material/ListItemText'
|
||||||
|
import Stack from '@mui/material/Stack'
|
||||||
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import useMediaQuery from '@mui/material/useMediaQuery'
|
||||||
|
import { useUnit } from 'effector-react'
|
||||||
|
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
import { AddressesPage } from '@/pages/me/ui/sections/AddressesPage'
|
||||||
|
import { MessagesPage } from '@/pages/me/ui/sections/MessagesPage'
|
||||||
|
import { OrdersPage } from '@/pages/me/ui/sections/OrdersPage'
|
||||||
|
import { SettingsPage } from '@/pages/me/ui/sections/SettingsPage'
|
||||||
|
import { $user } from '@/shared/model/auth'
|
||||||
|
|
||||||
|
type NavItem = {
|
||||||
|
to: string
|
||||||
|
label: string
|
||||||
|
icon: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MeLayoutPage() {
|
||||||
|
const user = useUnit($user)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
const theme = useTheme()
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
||||||
|
const [mobileOpen, setMobileOpen] = useState(false)
|
||||||
|
|
||||||
|
const navItems: NavItem[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{ to: '/me/orders', label: 'Заказы', icon: <LocalShippingOutlinedIcon /> },
|
||||||
|
{ to: '/me/messages', label: 'Сообщения', icon: <ChatOutlinedIcon /> },
|
||||||
|
{ to: '/me/settings', label: 'Настройки', icon: <SettingsOutlinedIcon /> },
|
||||||
|
{ to: '/me/addresses', label: 'Адреса доставки', icon: <PlaceOutlinedIcon /> },
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <Alert severity="info">Нужно войти. Перейдите на страницу «Вход».</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTo =
|
||||||
|
navItems.find((x) => location.pathname === x.to)?.to ??
|
||||||
|
navItems.find((x) => location.pathname.startsWith(`${x.to}/`))?.to ??
|
||||||
|
null
|
||||||
|
|
||||||
|
const nav = (
|
||||||
|
<Box sx={{ width: 280, maxWidth: '85vw' }}>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||||
|
Кабинет
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{user.name?.trim() || user.email}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<List disablePadding>
|
||||||
|
{navItems.map((i) => (
|
||||||
|
<ListItemButton
|
||||||
|
key={i.to}
|
||||||
|
selected={activeTo === i.to}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(i.to)
|
||||||
|
setMobileOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon>{i.icon}</ListItemIcon>
|
||||||
|
<ListItemText primary={i.label} />
|
||||||
|
</ListItemButton>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction={{ xs: 'column', md: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||||||
|
{isMobile ? (
|
||||||
|
<>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center" sx={{ width: '100%' }}>
|
||||||
|
<IconButton onClick={() => setMobileOpen(true)} aria-label="Открыть меню профиля">
|
||||||
|
<MenuOutlinedIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||||
|
Профиль
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Drawer open={mobileOpen} onClose={() => setMobileOpen(false)} ModalProps={{ keepMounted: true }}>
|
||||||
|
{nav}
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'sticky',
|
||||||
|
top: 88,
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{nav}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ flexGrow: 1, minWidth: 0, width: '100%' }}>
|
||||||
|
<Routes>
|
||||||
|
<Route index element={<Navigate to="/me/settings" replace />} />
|
||||||
|
<Route path="orders" element={<OrdersPage />} />
|
||||||
|
<Route path="messages" element={<MessagesPage />} />
|
||||||
|
<Route path="settings" element={<SettingsPage />} />
|
||||||
|
<Route path="addresses" element={<AddressesPage />} />
|
||||||
|
<Route path="*" element={<Navigate to="/me/settings" replace />} />
|
||||||
|
</Routes>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import Alert from '@mui/material/Alert'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import Chip from '@mui/material/Chip'
|
||||||
|
import Dialog from '@mui/material/Dialog'
|
||||||
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
|
import DialogContent from '@mui/material/DialogContent'
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
import Stack from '@mui/material/Stack'
|
||||||
|
import Switch from '@mui/material/Switch'
|
||||||
|
import TextField from '@mui/material/TextField'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { Controller, useForm } from 'react-hook-form'
|
||||||
|
import {
|
||||||
|
createMyAddress,
|
||||||
|
deleteMyAddress,
|
||||||
|
fetchMyAddresses,
|
||||||
|
setMyAddressDefault,
|
||||||
|
updateMyAddress,
|
||||||
|
} from '@/entities/user/api/address-api'
|
||||||
|
import type { ShippingAddress } from '@/entities/user/model/types'
|
||||||
|
import { AddressMapPicker } from '@/features/address-map-picker/ui/AddressMapPicker'
|
||||||
|
|
||||||
|
export function AddressesPage() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [editing, setEditing] = useState<ShippingAddress | null>(null)
|
||||||
|
|
||||||
|
const addressesQuery = useQuery({
|
||||||
|
queryKey: ['me', 'addresses'],
|
||||||
|
queryFn: fetchMyAddresses,
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = useForm<{
|
||||||
|
label: string
|
||||||
|
recipientName: string
|
||||||
|
recipientPhone: string
|
||||||
|
addressLine: string
|
||||||
|
comment: string
|
||||||
|
lat: number | null
|
||||||
|
lng: number | null
|
||||||
|
isDefault: boolean
|
||||||
|
}>({
|
||||||
|
defaultValues: {
|
||||||
|
label: '',
|
||||||
|
recipientName: '',
|
||||||
|
recipientPhone: '',
|
||||||
|
addressLine: '',
|
||||||
|
comment: '',
|
||||||
|
lat: null,
|
||||||
|
lng: null,
|
||||||
|
isDefault: false,
|
||||||
|
},
|
||||||
|
mode: 'onChange',
|
||||||
|
})
|
||||||
|
|
||||||
|
const createMut = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const v = form.getValues()
|
||||||
|
if (v.lat === null || v.lng === null) throw new Error('Выберите точку на карте')
|
||||||
|
await createMyAddress({
|
||||||
|
label: v.label.trim() || null,
|
||||||
|
recipientName: v.recipientName.trim(),
|
||||||
|
recipientPhone: v.recipientPhone.trim(),
|
||||||
|
addressLine: v.addressLine.trim(),
|
||||||
|
comment: v.comment.trim() || null,
|
||||||
|
lat: v.lat,
|
||||||
|
lng: v.lng,
|
||||||
|
isDefault: v.isDefault,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['me', 'addresses'] })
|
||||||
|
setOpen(false)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateMut = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const v = form.getValues()
|
||||||
|
if (v.lat === null || v.lng === null) throw new Error('Выберите точку на карте')
|
||||||
|
await updateMyAddress(editing!.id, {
|
||||||
|
label: v.label.trim() || null,
|
||||||
|
recipientName: v.recipientName.trim(),
|
||||||
|
recipientPhone: v.recipientPhone.trim(),
|
||||||
|
addressLine: v.addressLine.trim(),
|
||||||
|
comment: v.comment.trim() || null,
|
||||||
|
lat: v.lat,
|
||||||
|
lng: v.lng,
|
||||||
|
isDefault: v.isDefault,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['me', 'addresses'] })
|
||||||
|
setOpen(false)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteMut = useMutation({
|
||||||
|
mutationFn: (id: string) => deleteMyAddress(id),
|
||||||
|
onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['me', 'addresses'] }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const defaultMut = useMutation({
|
||||||
|
mutationFn: (id: string) => setMyAddressDefault(id),
|
||||||
|
onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['me', 'addresses'] }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const error = createMut.error ?? updateMut.error ?? deleteMut.error ?? defaultMut.error
|
||||||
|
|
||||||
|
const items = addressesQuery.data?.items ?? []
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
setEditing(null)
|
||||||
|
form.reset({
|
||||||
|
label: '',
|
||||||
|
recipientName: '',
|
||||||
|
recipientPhone: '',
|
||||||
|
addressLine: '',
|
||||||
|
comment: '',
|
||||||
|
lat: null,
|
||||||
|
lng: null,
|
||||||
|
isDefault: items.length === 0,
|
||||||
|
})
|
||||||
|
setOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEdit = (a: ShippingAddress) => {
|
||||||
|
setEditing(a)
|
||||||
|
form.reset({
|
||||||
|
label: a.label ?? '',
|
||||||
|
recipientName: a.recipientName,
|
||||||
|
recipientPhone: a.recipientPhone,
|
||||||
|
addressLine: a.addressLine,
|
||||||
|
comment: a.comment ?? '',
|
||||||
|
lat: a.lat,
|
||||||
|
lng: a.lng,
|
||||||
|
isDefault: a.isDefault,
|
||||||
|
})
|
||||||
|
setOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Адреса доставки
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
Можно добавить несколько адресов. Для каждого адреса задаются ФИО и телефон получателя отдельно.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
|
||||||
|
<Button variant="contained" onClick={openCreate}>
|
||||||
|
Добавить адрес
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{addressesQuery.isError && (
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
Не удалось загрузить адреса. Проверьте, что вы вошли в аккаунт и сервер запущен.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
{(error as Error).message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack spacing={2}>
|
||||||
|
{items.map((a) => (
|
||||||
|
<Box
|
||||||
|
key={a.id}
|
||||||
|
sx={{
|
||||||
|
border: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
borderRadius: 2,
|
||||||
|
p: 2,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} alignItems={{ sm: 'center' }}>
|
||||||
|
<Typography sx={{ fontWeight: 700, flexGrow: 1 }}>{a.label?.trim() ? a.label : 'Адрес'}</Typography>
|
||||||
|
{a.isDefault && <Chip label="По умолчанию" color="primary" size="small" />}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Typography sx={{ mt: 1 }}>{a.addressLine}</Typography>
|
||||||
|
<Typography color="text.secondary" variant="body2" sx={{ mt: 0.5 }}>
|
||||||
|
Получатель: {a.recipientName} · {a.recipientPhone}
|
||||||
|
</Typography>
|
||||||
|
{a.comment && (
|
||||||
|
<Typography color="text.secondary" variant="body2" sx={{ mt: 0.5 }}>
|
||||||
|
Комментарий: {a.comment}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1} sx={{ mt: 1.5, flexWrap: 'wrap' }}>
|
||||||
|
{!a.isDefault && (
|
||||||
|
<Button size="small" onClick={() => defaultMut.mutate(a.id)} disabled={defaultMut.isPending}>
|
||||||
|
Сделать основным
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button size="small" onClick={() => openEdit(a)}>
|
||||||
|
Изменить
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
disabled={deleteMut.isPending}
|
||||||
|
onClick={() => {
|
||||||
|
if (!confirm('Удалить адрес?')) return
|
||||||
|
deleteMut.mutate(a.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Удалить
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{addressesQuery.isSuccess && items.length === 0 && (
|
||||||
|
<Alert severity="info">Адресов пока нет — добавьте первый адрес доставки.</Alert>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Dialog open={open} onClose={() => setOpen(false)} fullWidth maxWidth="md">
|
||||||
|
<DialogTitle>{editing ? 'Редактировать адрес' : 'Новый адрес'}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Stack spacing={2} sx={{ mt: 1 }}>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="label"
|
||||||
|
render={({ field }) => <TextField label="Метка (например: Дом/Работа)" fullWidth {...field} />}
|
||||||
|
/>
|
||||||
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="recipientName"
|
||||||
|
render={({ field }) => <TextField label="ФИО получателя" fullWidth required {...field} />}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="recipientPhone"
|
||||||
|
render={({ field }) => <TextField label="Телефон получателя" fullWidth required {...field} />}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="addressLine"
|
||||||
|
render={({ field }) => <TextField label="Адрес" fullWidth required {...field} />}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="comment"
|
||||||
|
render={({ field }) => <TextField label="Комментарий (необязательно)" fullWidth {...field} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="lat"
|
||||||
|
render={({ field: latField }) => (
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="lng"
|
||||||
|
render={({ field: lngField }) => (
|
||||||
|
<AddressMapPicker
|
||||||
|
value={
|
||||||
|
latField.value !== null && lngField.value !== null
|
||||||
|
? { lat: latField.value, lng: lngField.value }
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onChange={(v) => {
|
||||||
|
latField.onChange(v.lat)
|
||||||
|
lngField.onChange(v.lng)
|
||||||
|
if (v.addressLine) form.setValue('addressLine', v.addressLine, { shouldDirty: true })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="isDefault"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Switch checked={Boolean(field.value)} onChange={(_, v) => field.onChange(v)} />}
|
||||||
|
label="Адрес по умолчанию"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setOpen(false)}>Отмена</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => (editing ? updateMut.mutate() : createMut.mutate())}
|
||||||
|
disabled={
|
||||||
|
createMut.isPending ||
|
||||||
|
updateMut.isPending ||
|
||||||
|
!form.watch('recipientName').trim() ||
|
||||||
|
!form.watch('recipientPhone').trim() ||
|
||||||
|
!form.watch('addressLine').trim()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
|
||||||
|
export function MessagesPage() {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Сообщения
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary">Скоро здесь появятся сообщения и уведомления.</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
|
||||||
|
export function OrdersPage() {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Заказы
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary">Скоро здесь появится история заказов.</Typography>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import Alert from '@mui/material/Alert'
|
||||||
|
import Box from '@mui/material/Box'
|
||||||
|
import Button from '@mui/material/Button'
|
||||||
|
import Divider from '@mui/material/Divider'
|
||||||
|
import Stack from '@mui/material/Stack'
|
||||||
|
import TextField from '@mui/material/TextField'
|
||||||
|
import Typography from '@mui/material/Typography'
|
||||||
|
import { useUnit } from 'effector-react'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import {
|
||||||
|
$changePasswordError,
|
||||||
|
$requestEmailChangeCodeError,
|
||||||
|
$updateProfileError,
|
||||||
|
$user,
|
||||||
|
$verifyEmailChangeError,
|
||||||
|
changePasswordFx,
|
||||||
|
requestEmailChangeCodeFx,
|
||||||
|
updateProfileFx,
|
||||||
|
verifyEmailChangeFx,
|
||||||
|
} from '@/shared/model/auth'
|
||||||
|
import type { AxiosError } from 'axios'
|
||||||
|
|
||||||
|
function getApiErrorMessage(error: unknown): string | null {
|
||||||
|
const e = error as AxiosError<{ error?: string }>
|
||||||
|
const msg = e?.response?.data?.error
|
||||||
|
return msg ? String(msg) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsPage() {
|
||||||
|
const user = useUnit($user)
|
||||||
|
const pendingPassword = useUnit(changePasswordFx.pending)
|
||||||
|
const pendingEmailReq = useUnit(requestEmailChangeCodeFx.pending)
|
||||||
|
const pendingEmailVerify = useUnit(verifyEmailChangeFx.pending)
|
||||||
|
const pendingProfile = useUnit(updateProfileFx.pending)
|
||||||
|
const errorPassword = useUnit($changePasswordError)
|
||||||
|
const errorEmailReq = useUnit($requestEmailChangeCodeError)
|
||||||
|
const errorProfile = useUnit($updateProfileError)
|
||||||
|
const errorEmailVerify = useUnit($verifyEmailChangeError)
|
||||||
|
|
||||||
|
const passwordForm = useForm<{ currentPassword: string; newPassword: string }>({
|
||||||
|
defaultValues: { currentPassword: '', newPassword: '' },
|
||||||
|
mode: 'onChange',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emailForm = useForm<{ newEmail: string; code: string }>({
|
||||||
|
defaultValues: { newEmail: '', code: '' },
|
||||||
|
mode: 'onChange',
|
||||||
|
})
|
||||||
|
|
||||||
|
const profileForm = useForm<{ name: string; phone: string }>({
|
||||||
|
defaultValues: { name: user?.name ? String(user.name) : '', phone: user?.phone ? String(user.phone) : '' },
|
||||||
|
mode: 'onChange',
|
||||||
|
})
|
||||||
|
|
||||||
|
const passwordErrorMsg = getApiErrorMessage(errorPassword)
|
||||||
|
const emailErrorMsg = getApiErrorMessage(errorEmailReq) ?? getApiErrorMessage(errorEmailVerify)
|
||||||
|
const profileErrorMsg = getApiErrorMessage(errorProfile)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <Alert severity="info">Нужно войти. Перейдите на страницу «Вход».</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Настройки
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
Текущая почта: <b>{user.email}</b>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{passwordErrorMsg && (
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
{passwordErrorMsg}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{emailErrorMsg && (
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
{emailErrorMsg}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{profileErrorMsg && (
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
{profileErrorMsg}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack spacing={3} sx={{ maxWidth: 560 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Профиль
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<TextField
|
||||||
|
label="Имя или ник"
|
||||||
|
helperText="До 40 символов"
|
||||||
|
slotProps={{ htmlInput: { maxLength: 40 } }}
|
||||||
|
{...profileForm.register('name')}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Телефон"
|
||||||
|
helperText="Можно указать для связи по заказам"
|
||||||
|
{...profileForm.register('phone')}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={pendingProfile}
|
||||||
|
onClick={() => {
|
||||||
|
const raw = profileForm.getValues('name')
|
||||||
|
const name = raw.trim()
|
||||||
|
const phoneRaw = profileForm.getValues('phone')
|
||||||
|
const phone = phoneRaw.trim()
|
||||||
|
updateProfileFx({ name: name.length ? name : null, phone: phone.length ? phone : null })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Смена почты
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<TextField label="Новая почта" {...emailForm.register('newEmail')} />
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
disabled={!emailForm.watch('newEmail') || pendingEmailReq}
|
||||||
|
onClick={() => requestEmailChangeCodeFx(emailForm.getValues('newEmail').trim())}
|
||||||
|
>
|
||||||
|
Отправить код на новую почту
|
||||||
|
</Button>
|
||||||
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
|
||||||
|
<TextField label="Код (6 цифр)" inputMode="numeric" {...emailForm.register('code')} />
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={emailForm.watch('code').trim().length !== 6 || pendingEmailVerify}
|
||||||
|
onClick={() =>
|
||||||
|
verifyEmailChangeFx({
|
||||||
|
newEmail: emailForm.getValues('newEmail').trim(),
|
||||||
|
code: emailForm.getValues('code').trim(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Подтвердить
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Смена пароля
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<TextField
|
||||||
|
label="Текущий пароль (если установлен)"
|
||||||
|
type="password"
|
||||||
|
{...passwordForm.register('currentPassword')}
|
||||||
|
/>
|
||||||
|
<TextField label="Новый пароль" type="password" {...passwordForm.register('newPassword')} />
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={passwordForm.watch('newPassword').length < 8 || pendingPassword}
|
||||||
|
onClick={() =>
|
||||||
|
changePasswordFx({
|
||||||
|
currentPassword: passwordForm.getValues('currentPassword') || undefined,
|
||||||
|
newPassword: passwordForm.getValues('newPassword'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Сохранить пароль
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createEffect, createEvent, createStore, sample } from 'effector'
|
import { createEffect, createEvent, createStore, sample } from 'effector'
|
||||||
import { apiClient } from '@/shared/api/client'
|
import { apiClient } from '@/shared/api/client'
|
||||||
|
|
||||||
export type AuthUser = { id: string; email: string; name?: string | null }
|
export type AuthUser = { id: string; email: string; name?: string | null; phone?: string | null }
|
||||||
|
|
||||||
const TOKEN_KEY = 'craftshop_auth_token'
|
const TOKEN_KEY = 'craftshop_auth_token'
|
||||||
|
|
||||||
@@ -28,7 +28,9 @@ export const verifyEmailChangeFx = createEffect(async (params: { newEmail: strin
|
|||||||
return data.user
|
return data.user
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateProfileFx = createEffect(async (params: { name: string | null }) => {
|
export type UpdateProfileParams = { name: string | null; phone?: string | null }
|
||||||
|
|
||||||
|
export const updateProfileFx = createEffect(async (params: UpdateProfileParams) => {
|
||||||
const { data } = await apiClient.patch<{ user: AuthUser }>('me/profile', params)
|
const { data } = await apiClient.patch<{ user: AuthUser }>('me/profile', params)
|
||||||
return data.user
|
return data.user
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "phone" TEXT;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ShippingAddress" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"label" TEXT,
|
||||||
|
"recipientName" TEXT NOT NULL,
|
||||||
|
"recipientPhone" TEXT NOT NULL,
|
||||||
|
"addressLine" TEXT NOT NULL,
|
||||||
|
"comment" TEXT,
|
||||||
|
"lat" REAL NOT NULL,
|
||||||
|
"lng" REAL NOT NULL,
|
||||||
|
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "ShippingAddress_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ShippingAddress_userId_isDefault_idx" ON "ShippingAddress"("userId", "isDefault");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ShippingAddress_userId_updatedAt_idx" ON "ShippingAddress"("userId", "updatedAt");
|
||||||
@@ -56,11 +56,33 @@ model User {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
email String @unique
|
email String @unique
|
||||||
name String?
|
name String?
|
||||||
|
phone String?
|
||||||
passwordHash String?
|
passwordHash String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
codes AuthCode[]
|
codes AuthCode[]
|
||||||
|
addresses ShippingAddress[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model ShippingAddress {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
label String?
|
||||||
|
recipientName String
|
||||||
|
recipientPhone String
|
||||||
|
addressLine String
|
||||||
|
comment String?
|
||||||
|
lat Float
|
||||||
|
lng Float
|
||||||
|
isDefault Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
|
||||||
|
@@index([userId, isDefault])
|
||||||
|
@@index([userId, updatedAt])
|
||||||
}
|
}
|
||||||
|
|
||||||
model AuthCode {
|
model AuthCode {
|
||||||
|
|||||||
+209
-8
@@ -27,7 +27,7 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const token = fastify.jwt.sign({ sub: user.id, email: user.email })
|
const token = fastify.jwt.sign({ sub: user.id, email: user.email })
|
||||||
return { token, user: { id: user.id, email: user.email, name: user.name } }
|
return { token, user: { id: user.id, email: user.email, name: user.name, phone: user.phone } }
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.post('/api/auth/register', async (request, reply) => {
|
fastify.post('/api/auth/register', async (request, reply) => {
|
||||||
@@ -42,7 +42,7 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
const passwordHash = await hashPassword(password)
|
const passwordHash = await hashPassword(password)
|
||||||
const user = await prisma.user.create({ data: { email, passwordHash } })
|
const user = await prisma.user.create({ data: { email, passwordHash } })
|
||||||
const token = fastify.jwt.sign({ sub: user.id, email: user.email })
|
const token = fastify.jwt.sign({ sub: user.id, email: user.email })
|
||||||
return reply.code(201).send({ token, user: { id: user.id, email: user.email, name: user.name } })
|
return reply.code(201).send({ token, user: { id: user.id, email: user.email, name: user.name, phone: user.phone } })
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.post('/api/auth/login', async (request, reply) => {
|
fastify.post('/api/auth/login', async (request, reply) => {
|
||||||
@@ -58,7 +58,7 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
if (!ok) return reply.code(401).send({ error: 'Неверные данные' })
|
if (!ok) return reply.code(401).send({ error: 'Неверные данные' })
|
||||||
|
|
||||||
const token = fastify.jwt.sign({ sub: user.id, email: user.email })
|
const token = fastify.jwt.sign({ sub: user.id, email: user.email })
|
||||||
return { token, user: { id: user.id, email: user.email, name: user.name } }
|
return { token, user: { id: user.id, email: user.email, name: user.name, phone: user.phone } }
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.get(
|
fastify.get(
|
||||||
@@ -68,7 +68,7 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const user = await prisma.user.findUnique({ where: { id: userId } })
|
const user = await prisma.user.findUnique({ where: { id: userId } })
|
||||||
if (!user) return { user: null }
|
if (!user) return { user: null }
|
||||||
return { user: { id: user.id, email: user.email, name: user.name } }
|
return { user: { id: user.id, email: user.email, name: user.name, phone: user.phone } }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
data: { email: newEmail },
|
data: { email: newEmail },
|
||||||
})
|
})
|
||||||
return { user: { id: user.id, email: user.email, name: user.name } }
|
return { user: { id: user.id, email: user.email, name: user.name, phone: user.phone } }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
|
|
||||||
const passwordHash = await hashPassword(newPassword)
|
const passwordHash = await hashPassword(newPassword)
|
||||||
const updated = await prisma.user.update({ where: { id: userId }, data: { passwordHash } })
|
const updated = await prisma.user.update({ where: { id: userId }, data: { passwordHash } })
|
||||||
return { user: { id: updated.id, email: updated.email, name: updated.name } }
|
return { user: { id: updated.id, email: updated.email, name: updated.name, phone: updated.phone } }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -144,14 +144,215 @@ export async function registerAuthRoutes(fastify) {
|
|||||||
const userId = request.user.sub
|
const userId = request.user.sub
|
||||||
const nameRaw = request.body?.name
|
const nameRaw = request.body?.name
|
||||||
const name = nameRaw === null || nameRaw === undefined ? null : String(nameRaw).trim()
|
const name = nameRaw === null || nameRaw === undefined ? null : String(nameRaw).trim()
|
||||||
|
const phoneRaw = request.body?.phone
|
||||||
|
const phone = phoneRaw === null || phoneRaw === undefined ? null : String(phoneRaw).trim()
|
||||||
|
|
||||||
if (name !== null && name.length > 40) return reply.code(400).send({ error: 'Имя/ник максимум 40 символов' })
|
if (name !== null && name.length > 40) return reply.code(400).send({ error: 'Имя/ник максимум 40 символов' })
|
||||||
|
if (phone !== null) {
|
||||||
|
const compact = phone.replace(/[\s()-]/g, '')
|
||||||
|
if (compact.length > 20) return reply.code(400).send({ error: 'Телефон слишком длинный' })
|
||||||
|
if (compact.length && !/^\+?\d{7,20}$/.test(compact)) {
|
||||||
|
return reply.code(400).send({ error: 'Некорректный телефон' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updated = await prisma.user.update({
|
const updated = await prisma.user.update({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
data: { name: name && name.length ? name : null },
|
data: { name: name && name.length ? name : null, phone: phone && phone.length ? phone : null },
|
||||||
})
|
})
|
||||||
return { user: { id: updated.id, email: updated.email, name: updated.name } }
|
return { user: { id: updated.id, email: updated.email, name: updated.name, phone: updated.phone } }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---- Адреса доставки ----
|
||||||
|
|
||||||
|
function normalizePhoneLite(input) {
|
||||||
|
const s = String(input || '').trim()
|
||||||
|
if (!s) return ''
|
||||||
|
return s.replace(/[\s()-]/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAddressPayload(body, reply) {
|
||||||
|
const labelRaw = body?.label
|
||||||
|
const label = labelRaw === null || labelRaw === undefined ? null : String(labelRaw).trim()
|
||||||
|
if (label !== null && label.length > 40) return reply.code(400).send({ error: 'Метка адреса максимум 40 символов' })
|
||||||
|
|
||||||
|
const recipientName = String(body?.recipientName || '').trim()
|
||||||
|
if (!recipientName) return reply.code(400).send({ error: 'Укажите ФИО получателя' })
|
||||||
|
if (recipientName.length > 80) return reply.code(400).send({ error: 'ФИО получателя максимум 80 символов' })
|
||||||
|
|
||||||
|
const recipientPhone = normalizePhoneLite(body?.recipientPhone)
|
||||||
|
if (!recipientPhone) return reply.code(400).send({ error: 'Укажите телефон получателя' })
|
||||||
|
if (!/^\+?\d{7,20}$/.test(recipientPhone)) return reply.code(400).send({ error: 'Некорректный телефон получателя' })
|
||||||
|
|
||||||
|
const addressLine = String(body?.addressLine || '').trim()
|
||||||
|
if (!addressLine) return reply.code(400).send({ error: 'Укажите адрес' })
|
||||||
|
if (addressLine.length > 200) return reply.code(400).send({ error: 'Адрес максимум 200 символов' })
|
||||||
|
|
||||||
|
const commentRaw = body?.comment
|
||||||
|
const comment = commentRaw === null || commentRaw === undefined ? null : String(commentRaw).trim()
|
||||||
|
if (comment !== null && comment.length > 200) return reply.code(400).send({ error: 'Комментарий максимум 200 символов' })
|
||||||
|
|
||||||
|
const lat = Number(body?.lat)
|
||||||
|
const lng = Number(body?.lng)
|
||||||
|
if (!Number.isFinite(lat) || lat < -90 || lat > 90) return reply.code(400).send({ error: 'Некорректная широта' })
|
||||||
|
if (!Number.isFinite(lng) || lng < -180 || lng > 180) return reply.code(400).send({ error: 'Некорректная долгота' })
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
recipientName,
|
||||||
|
recipientPhone,
|
||||||
|
addressLine,
|
||||||
|
comment,
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fastify.get(
|
||||||
|
'/api/me/addresses',
|
||||||
|
{ preHandler: [fastify.authenticate] },
|
||||||
|
async (request) => {
|
||||||
|
const userId = request.user.sub
|
||||||
|
const items = await prisma.shippingAddress.findMany({
|
||||||
|
where: { userId },
|
||||||
|
orderBy: [{ isDefault: 'desc' }, { updatedAt: 'desc' }],
|
||||||
|
})
|
||||||
|
return { items }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fastify.post(
|
||||||
|
'/api/me/addresses',
|
||||||
|
{ preHandler: [fastify.authenticate] },
|
||||||
|
async (request, reply) => {
|
||||||
|
const userId = request.user.sub
|
||||||
|
const validated = validateAddressPayload(request.body, reply)
|
||||||
|
if (!validated) return
|
||||||
|
|
||||||
|
const isDefault = Boolean(request.body?.isDefault)
|
||||||
|
const created = await prisma.$transaction(async (tx) => {
|
||||||
|
if (isDefault) {
|
||||||
|
await tx.shippingAddress.updateMany({ where: { userId, isDefault: true }, data: { isDefault: false } })
|
||||||
|
}
|
||||||
|
return tx.shippingAddress.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
...validated,
|
||||||
|
isDefault,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return reply.code(201).send({ item: created })
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fastify.patch(
|
||||||
|
'/api/me/addresses/:id',
|
||||||
|
{ preHandler: [fastify.authenticate] },
|
||||||
|
async (request, reply) => {
|
||||||
|
const userId = request.user.sub
|
||||||
|
const { id } = request.params
|
||||||
|
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
||||||
|
if (!existing) return reply.code(404).send({ error: 'Адрес не найден' })
|
||||||
|
|
||||||
|
const body = request.body ?? {}
|
||||||
|
const data = {}
|
||||||
|
|
||||||
|
if (body.label !== undefined) {
|
||||||
|
const labelRaw = body.label
|
||||||
|
const label = labelRaw === null || labelRaw === undefined ? null : String(labelRaw).trim()
|
||||||
|
if (label !== null && label.length > 40) return reply.code(400).send({ error: 'Метка адреса максимум 40 символов' })
|
||||||
|
data.label = label && label.length ? label : null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.recipientName !== undefined) {
|
||||||
|
const v = String(body.recipientName || '').trim()
|
||||||
|
if (!v) return reply.code(400).send({ error: 'Укажите ФИО получателя' })
|
||||||
|
if (v.length > 80) return reply.code(400).send({ error: 'ФИО получателя максимум 80 символов' })
|
||||||
|
data.recipientName = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.recipientPhone !== undefined) {
|
||||||
|
const v = normalizePhoneLite(body.recipientPhone)
|
||||||
|
if (!v) return reply.code(400).send({ error: 'Укажите телефон получателя' })
|
||||||
|
if (!/^\+?\d{7,20}$/.test(v)) return reply.code(400).send({ error: 'Некорректный телефон получателя' })
|
||||||
|
data.recipientPhone = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.addressLine !== undefined) {
|
||||||
|
const v = String(body.addressLine || '').trim()
|
||||||
|
if (!v) return reply.code(400).send({ error: 'Укажите адрес' })
|
||||||
|
if (v.length > 200) return reply.code(400).send({ error: 'Адрес максимум 200 символов' })
|
||||||
|
data.addressLine = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.comment !== undefined) {
|
||||||
|
const commentRaw = body.comment
|
||||||
|
const comment = commentRaw === null || commentRaw === undefined ? null : String(commentRaw).trim()
|
||||||
|
if (comment !== null && comment.length > 200) return reply.code(400).send({ error: 'Комментарий максимум 200 символов' })
|
||||||
|
data.comment = comment && comment.length ? comment : null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.lat !== undefined) {
|
||||||
|
const lat = Number(body.lat)
|
||||||
|
if (!Number.isFinite(lat) || lat < -90 || lat > 90) return reply.code(400).send({ error: 'Некорректная широта' })
|
||||||
|
data.lat = lat
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.lng !== undefined) {
|
||||||
|
const lng = Number(body.lng)
|
||||||
|
if (!Number.isFinite(lng) || lng < -180 || lng > 180) return reply.code(400).send({ error: 'Некорректная долгота' })
|
||||||
|
data.lng = lng
|
||||||
|
}
|
||||||
|
|
||||||
|
const setDefault = body.isDefault === true
|
||||||
|
const updated = await prisma.$transaction(async (tx) => {
|
||||||
|
if (setDefault) {
|
||||||
|
await tx.shippingAddress.updateMany({ where: { userId, isDefault: true }, data: { isDefault: false } })
|
||||||
|
}
|
||||||
|
return tx.shippingAddress.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
...(setDefault ? { isDefault: true } : {}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return { item: updated }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fastify.delete(
|
||||||
|
'/api/me/addresses/:id',
|
||||||
|
{ preHandler: [fastify.authenticate] },
|
||||||
|
async (request, reply) => {
|
||||||
|
const userId = request.user.sub
|
||||||
|
const { id } = request.params
|
||||||
|
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
||||||
|
if (!existing) return reply.code(404).send({ error: 'Адрес не найден' })
|
||||||
|
|
||||||
|
await prisma.shippingAddress.delete({ where: { id } })
|
||||||
|
return reply.code(204).send()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
fastify.post(
|
||||||
|
'/api/me/addresses/:id/default',
|
||||||
|
{ preHandler: [fastify.authenticate] },
|
||||||
|
async (request, reply) => {
|
||||||
|
const userId = request.user.sub
|
||||||
|
const { id } = request.params
|
||||||
|
const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } })
|
||||||
|
if (!existing) return reply.code(404).send({ error: 'Адрес не найден' })
|
||||||
|
|
||||||
|
const updated = await prisma.$transaction(async (tx) => {
|
||||||
|
await tx.shippingAddress.updateMany({ where: { userId, isDefault: true }, data: { isDefault: false } })
|
||||||
|
return tx.shippingAddress.update({ where: { id }, data: { isDefault: true } })
|
||||||
|
})
|
||||||
|
|
||||||
|
return { item: updated }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user