diff --git a/package-lock.json b/package-lock.json index 50f9904..ddfb68b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,10 @@ "@iconify/react": "^5.1.0", "@mui/material": "^6.1.7", "@radix-ui/react-avatar": "^1.1.2", - "@radix-ui/react-collapsible": "^1.1.2", + "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", @@ -1224,6 +1225,12 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", @@ -1280,19 +1287,97 @@ } }, "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", + "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz", - "integrity": "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1309,6 +1394,81 @@ } } }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", @@ -1704,6 +1864,463 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz", @@ -1812,6 +2429,39 @@ } } }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", @@ -1845,6 +2495,21 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", @@ -7132,16 +7797,16 @@ } }, "node_modules/react-remove-scroll": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz", - "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.1", + "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.2" + "use-sidecar": "^1.1.3" }, "engines": { "node": ">=10" diff --git a/package.json b/package.json index 5e5614d..61be21f 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,10 @@ "@iconify/react": "^5.1.0", "@mui/material": "^6.1.7", "@radix-ui/react-avatar": "^1.1.2", - "@radix-ui/react-collapsible": "^1.1.2", + "@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", diff --git a/src/assets/backpack/backpack.bin b/src/assets/backpack/backpack.bin index 3267517..ce2c216 100644 Binary files a/src/assets/backpack/backpack.bin and b/src/assets/backpack/backpack.bin differ diff --git a/src/assets/boots/boots.bin b/src/assets/boots/boots.bin index 164f752..4a2384f 100644 Binary files a/src/assets/boots/boots.bin and b/src/assets/boots/boots.bin differ diff --git a/src/components/MapBoxMap.jsx b/src/components/MapBoxMap.jsx index 2485822..bfca8fc 100644 --- a/src/components/MapBoxMap.jsx +++ b/src/components/MapBoxMap.jsx @@ -1,377 +1,529 @@ -"use client"; - -import React, { useState, useRef, useEffect } from "react"; -import { AppSidebar } from "@/components/sidebar/app-sidebar"; -import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; -import MapDisplay from "@/components/sidebar/MapDisplay"; -import mapboxgl from "mapbox-gl"; -import { useTheme } from "@/hooks/theme-provider"; -import { useLocation } from "react-router-dom"; - -mapboxgl.accessToken = "pk.eyJ1Ijoic3lsdmFpbmNvc3RlcyIsImEiOiJjbTNxZXNtN3cwa2hpMmpxdWd2cndhdnYwIn0.V2ZAp-BqZq6KIHQ6Lu8eAQ"; - -const fetchCoordinatesFromCity = async (city) => { - const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(city)}.json?access_token=${mapboxgl.accessToken}`; - try { - const response = await fetch(url); - const data = await response.json(); - return data.features[0]?.center || null; - } catch (error) { - console.error(`Erreur lors de la récupération des coordonnées pour ${city}:`, error); - return null; - } -}; - -export default function Page() { - const [startCoords, setStartCoords] = useState(null); - const [endCoords, setEndCoords] = useState(null); - const [transportMode, setTransportMode] = useState("driving"); - const [routeInstructions, setRouteInstructions] = useState([]); - const [pois, setPois] = useState({ hotel: [], restaurant: [], gas_station: [], park: [] }); - const [parcoursData, setParcoursData] = useState(null); // État pour les données GeoJSON - - const { theme } = useTheme(); - const isDarkMode = theme === "dark"; - const markersRef = useRef([]); // Stocker les marqueurs pour nettoyage ultérieur - const location = useLocation(); // Lire les paramètres de l'URL - const mapRef = useRef(null); - const mapContainerRef = useRef(null); - const queryParams = new URLSearchParams(location.search); - const startCity = queryParams.get("startCity"); - const endCity = queryParams.get("endCity"); - - useEffect(() => { - const initializeCoords = async () => { - if (startCity) { - const start = await fetchCoordinatesFromCity(startCity); - setStartCoords(start); - } - if (endCity) { - const end = await fetchCoordinatesFromCity(endCity); - setEndCoords(end); - } - }; - - initializeCoords(); - }, [startCity, endCity]); - - - const fetchRoute = async () => { - if (!startCoords || !endCoords) return; - - const url = `https://api.mapbox.com/directions/v5/mapbox/${transportMode}/${startCoords[0]},${startCoords[1]};${endCoords[0]},${endCoords[1]}?geometries=geojson&steps=true&access_token=${mapboxgl.accessToken}`; - try { - const response = await fetch(url); - const data = await response.json(); - - const route = data.routes[0]?.geometry; - const steps = data.routes[0]?.legs[0]?.steps || []; - - if (route && mapRef.current) { - if (mapRef.current.getLayer("route")) mapRef.current.removeLayer("route"); - if (mapRef.current.getSource("route")) mapRef.current.removeSource("route"); - - mapRef.current.addSource("route", { type: "geojson", data: { type: "Feature", geometry: route } }); - mapRef.current.addLayer({ - id: "route", - type: "line", - source: "route", - layout: { "line-join": "round", "line-cap": "round" }, - paint: { "line-color": "#007bff", "line-width": 5 }, - }); - - mapRef.current.fitBounds([startCoords, endCoords], { padding: 50 }); - - setRouteInstructions( - steps.map((step) => ({ - instruction: step.maneuver.instruction, - type: step.maneuver.type, - modifier: step.maneuver.modifier, - })) - ); - - fetchPois(route); // Rechercher les POI - } - } catch (error) { - console.error("Erreur lors de la récupération de l’itinéraire :", error); - } - }; - - const fetchPois = async (geometry) => { - if (!geometry) return; - - const bbox = calculateBoundingBox(geometry.coordinates); - const categories = ["hotel", "restaurant", "gas_station", "park"]; - const poisByCategory = { hotel: [], restaurant: [], gas_station: [], park: [] }; - - clearMarkers(); // Nettoyer les anciens marqueurs - - for (const category of categories) { - const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${category}.json?bbox=${bbox.join(",")}&access_token=${mapboxgl.accessToken}`; - try { - const response = await fetch(url); - const data = await response.json(); - - if (data.features) { - const filteredPois = filterPoisByProximity( - data.features.map((feature) => ({ - name: feature.text, - coords: feature.geometry.coordinates, - category, - })), - geometry.coordinates, - 5 // Distance maximale en kilomètres - ); - poisByCategory[category] = filteredPois; - } - } catch (error) { - console.error(`Erreur lors de la récupération des POI pour ${category}:`, error); - } - } - - setPois(poisByCategory); // Mettre à jour les POI dans le state - addMarkers(poisByCategory); // Ajouter les nouveaux marqueurs - // addParcoursMarkers(); // Ajouter les marqueurs pour les parcours - }; - - const addParcoursMarkers = (parcoursData) => { - if (!parcoursData || !parcoursData.features) return; - - const map = mapRef.current; - const allParcours = []; - - // Préparation des données pour les clusters - parcoursData.features.forEach((parcours) => { - const { coordinates } = parcours.geometry; - const { name, distance } = parcours.properties; - const [lat, lon] = coordinates; - - allParcours.push({ - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [lon, lat], - }, - properties: { - name, - distance, // Garder la distance pour l'affichage - }, - }); - }); - - // Ajouter une source GeoJSON pour les clusters des parcours - map.addSource('parcours', { - type: 'geojson', - data: { - type: 'FeatureCollection', - features: allParcours, - }, - cluster: true, - clusterMaxZoom: 14, - clusterRadius: 50, - }); - - // Ajouter des couches pour les clusters des parcours - map.addLayer({ - id: 'parcours-clusters', - type: 'circle', - source: 'parcours', - filter: ['has', 'point_count'], - paint: { - 'circle-color': '#51bbd6', - 'circle-radius': [ - 'interpolate', - ['linear'], - ['get', 'point_count'], - 0, - 20, - 100, - 40, - ], - }, - }); - - // Ajouter une couche pour afficher le nombre de points dans chaque cluster - map.addLayer({ - id: 'parcours-cluster-count', - type: 'symbol', - source: 'parcours', - filter: ['has', 'point_count'], - layout: { - 'text-field': '{point_count_abbreviated}', // Afficher le nombre abrégé de points dans le cluster - 'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'], - 'text-size': 12, - }, - paint: { - 'text-color': '#ffffff', - }, - }); - - // Ajouter une couche pour les parcours individuels (non clusterisés) - map.addLayer({ - id: 'parcours-individual-points', - type: 'circle', - source: 'parcours', - filter: ['!has', 'point_count'], - paint: { - 'circle-color': '#f28cb1', - 'circle-radius': 15, - }, - }); - - // Événement de clic sur le cluster pour zoomer - map.on('click', 'parcours-cluster-count', (e) => { - const features = map.queryRenderedFeatures(e.point, { - layers: ['parcours-cluster-count'], - }); - const clusterId = features[0].properties.cluster_id; - map.getSource('parcours').getClusterExpansionZoom(clusterId, (err, zoom) => { - if (err) return; - - map.easeTo({ - center: features[0].geometry.coordinates, - zoom: zoom, - }); - }); - }); - - // Événement de clic sur les points individuels pour afficher un popup avec des détails - map.on('click', 'parcours-individual-points', (e) => { - const coordinates = e.features[0].geometry.coordinates.slice(); - const name = e.features[0].properties.name; - const distance = e.features[0].properties.distance; - - // Conversion de la distance en kilomètres pour l'afficher dans le popup - const distanceInKm = (distance / 1000).toFixed(3); - - new mapboxgl.Popup() - .setLngLat(coordinates) - .setHTML(` - ${name}
- Distance: ${distanceInKm} km - `) - .addTo(map); - }); - }; - - const filterPoisByProximity = (pois, routeCoordinates, maxDistance) => { - return pois.filter((poi) => { - return routeCoordinates.some((coordinate) => { - const distance = calculateDistance(coordinate, poi.coords); - return distance <= maxDistance; - }); - }); - }; - - const calculateDistance = ([lng1, lat1], [lng2, lat2]) => { - const toRad = (deg) => (deg * Math.PI) / 180; - const R = 6371; // Rayon de la Terre en km - const dLat = toRad(lat2 - lat1); - const dLng = toRad(lng2 - lng1); - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; - }; - - const calculateBoundingBox = (coordinates) => { - let minLng = Infinity, - minLat = Infinity, - maxLng = -Infinity, - maxLat = -Infinity; - - coordinates.forEach(([lng, lat]) => { - if (lng < minLng) minLng = lng; - if (lat < minLat) minLat = lat; - if (lng > maxLng) maxLng = lng; - if (lat > maxLat) maxLat = lat; - }); - - return [minLng, minLat, maxLng, maxLat]; - }; - - const clearMarkers = () => { - markersRef.current.forEach((marker) => marker.remove()); - markersRef.current = []; - }; - - const addMarkers = (poisByCategory) => { - const map = mapRef.current; - - Object.keys(poisByCategory).forEach((category) => { - poisByCategory[category].forEach((poi) => { - if (!poi.coords || !poi.name) return; - - const marker = new mapboxgl.Marker({ - color: category === "hotel" - ? "blue" - : category === "restaurant" - ? "red" - : category === "park" - ? "green" - : "orange", - }) - .setLngLat(poi.coords) - .setPopup( - new mapboxgl.Popup().setHTML(` - ${poi.name}
- ${category} - `) - ) - .addTo(map); - - markersRef.current.push(marker); - }); - }); - }; - - useEffect(() => { - // Charger le fichier GeoJSON - fetch("/data_extraction/marqueurs3.geojson") - .then((response) => response.json()) - .then((data) => { - setParcoursData(data); - addParcoursMarkers(data); // Appeler la fonction pour ajouter les marqueurs - }) - .catch((error) => console.error("Erreur lors de la récupération des données GeoJSON:", error)); - - fetchRoute(); - }, [startCoords, endCoords, transportMode]); - - return ( - - - - - {/* SidebarTrigger uniquement */} -
- -
- - {/* MapDisplay pour prendre tout l'espace */} -
- -
-
-
- ); - - }; - +"use client"; + +import React, { useState, useRef, useEffect } from "react"; +import { AppSidebar } from "@/components/sidebar/app-sidebar"; +import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import MapDisplay from "@/components/sidebar/MapDisplay"; +import mapboxgl from "mapbox-gl"; +import { useTheme } from "@/hooks/theme-provider"; +import { useLocation } from "react-router-dom"; +import { NavMain } from "@/components/sidebar/nav-main"; +import { FaTrash, FaEye, FaEyeSlash } from "react-icons/fa"; + +mapboxgl.accessToken = "pk.eyJ1Ijoic3lsdmFpbmNvc3RlcyIsImEiOiJjbTNxZXNtN3cwa2hpMmpxdWd2cndhdnYwIn0.V2ZAp-BqZq6KIHQ6Lu8eAQ"; + +const fetchCoordinatesFromCity = async (city) => { + const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(city)}.json?access_token=${mapboxgl.accessToken}`; + try { + const response = await fetch(url); + const data = await response.json(); + const coords = data.features[0]?.center || null; + if (coords && Array.isArray(coords) && coords.length === 2) { + return coords; // Assurez-vous que les coordonnées sont au format [lng, lat] + } + return null; + } catch (error) { + console.error(`Erreur lors de la récupération des coordonnées pour ${city}:`, error); + return null; + } +}; + +export default function Page() { + const [startCoords, setStartCoords] = useState(null); + const [endCoords, setEndCoords] = useState(null); + const [waypoints, setWaypoints] = useState([]); // Déclaration de setWaypoints + const [transportMode, setTransportMode] = useState("driving"); + const [routeInstructions, setRouteInstructions] = useState([]); + const [parcoursData, setParcoursData] = useState(null); // État pour les données GeoJSON + const [query, setQuery] = useState(""); // État pour le champ de saisie + const [suggestions, setSuggestions] = useState([]); // État pour les suggestions + const [showWaypoints, setShowWaypoints] = useState(true); // État pour contrôler la visibilité des villes de passage + const [showWaypointsBox, setShowWaypointsBox] = useState(true); // État pour contrôler la visibilité de la boîte des villes de passage + const [places, setPlaces] = useState([]); // Initialisez places avec un tableau vide + + const { theme } = useTheme(); + const isDarkMode = theme === "dark"; + const markersRef = useRef([]); // Stocker les marqueurs pour nettoyage ultérieur + const location = useLocation(); // Lire les paramètres de l'URL + const mapRef = useRef(null); + const mapContainerRef = useRef(null); + const queryParams = new URLSearchParams(location.search); + const startCity = queryParams.get("startCity"); + const endCity = queryParams.get("endCity"); + + useEffect(() => { + const initializeCoords = async () => { + if (startCity) { + const start = await fetchCoordinatesFromCity(startCity); + setStartCoords(start); + } + if (endCity) { + const end = await fetchCoordinatesFromCity(endCity); + setEndCoords(end); + } + }; + + initializeCoords(); + }, [startCity, endCity]); + + // Ajoutez un autre useEffect pour mettre à jour le trajet + useEffect(() => { + fetchRoute(waypoints); // Appelez fetchRoute avec les waypoints actuels + }, [startCoords, endCoords, waypoints, transportMode]); // Ajoutez transportMode ici + + useEffect(() => { + if (startCoords && endCoords) { + fetchRoute(waypoints); // Appelez fetchRoute avec les waypoints actuels + } + }, [startCoords, endCoords, waypoints, transportMode]); // Ajoutez transportMode ici + + const fetchSuggestions = async (query) => { + if (query.length < 3) { + setSuggestions([]); // Ne pas afficher de suggestions si la requête est trop courte + return; + } + + const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(query)}.json?access_token=${mapboxgl.accessToken}`; + try { + const response = await fetch(url); + const data = await response.json(); + const results = data.features.map((feature) => ({ + name: feature.place_name, + coords: feature.geometry.coordinates, + })); + setSuggestions(results); // Mettre à jour l'état avec les suggestions + } catch (error) { + console.error("Erreur lors de la récupération des suggestions :", error); + } + }; + + const handleQueryChange = (e) => { + const value = e.target.value; + setQuery(value); + fetchSuggestions(value); // Appeler la fonction pour récupérer les suggestions + }; + + const addWaypoint = (suggestion) => { + if (suggestion) { + const exists = waypoints.some((waypoint) => waypoint.name === suggestion.name); + if (exists) { + alert("Cette ville est déjà ajoutée comme point de passage."); + return; + } + + // Ajoutez la ville à la liste des waypoints + setWaypoints((prevWaypoints) => { + const newWaypoints = [ + ...prevWaypoints, + { name: suggestion.name, coords: suggestion.coords }, + ]; + + // Mettre à jour le chemin après l'ajout + fetchRoute(newWaypoints); // Passez les nouveaux waypoints à fetchRoute + + return newWaypoints; + }); + + // Créer un marqueur jaune sur la carte + const marker = new mapboxgl.Marker({ color: "yellow" }) + .setLngLat(suggestion.coords) + .setPopup(new mapboxgl.Popup().setHTML(`${suggestion.name}`)) + .addTo(mapRef.current); + + markersRef.current.push(marker); // Ajouter le marqueur à la référence + + setQuery(""); + setSuggestions([]); + } + }; + + const removeWaypoint = (index) => { + setWaypoints((prevWaypoints) => { + const newWaypoints = prevWaypoints.filter((_, i) => i !== index); + + if (markersRef.current[index]) { + markersRef.current[index].remove(); // Supprimez le marqueur de la carte + markersRef.current.splice(index, 1); // Supprimez le marqueur de la référence + } + + // Mettre à jour le chemin après la suppression + fetchRoute(newWaypoints); // Passez les nouveaux waypoints à fetchRoute + + return newWaypoints; + }); + }; + + const fetchRoute = async (waypointsToUse) => { + if (!startCoords || !endCoords) { + console.warn("Les coordonnées de départ ou d'arrivée ne sont pas définies."); + return; // Ne pas continuer si les coordonnées ne sont pas disponibles + } + + const waypointCoords = waypointsToUse.map((waypoint) => waypoint.coords); + const routePath = [startCoords, ...waypointCoords, endCoords] + .map(([lng, lat]) => `${lng},${lat}`) + .join(";"); + + console.log("Coordonnées du trajet :", routePath); // Ajoutez cette ligne pour déboguer + + const url = `https://api.mapbox.com/directions/v5/mapbox/${transportMode}/${routePath}?geometries=geojson&steps=true&access_token=${mapboxgl.accessToken}`; + try { + const response = await fetch(url); + const data = await response.json(); + + if (!data.routes || data.routes.length === 0) { + console.warn("Aucune route trouvée."); + return; // Ne pas continuer si aucune route n'est trouvée + } + + const route = data.routes[0]?.geometry; + const steps = data.routes[0]?.legs[0]?.steps || []; + + if (route && mapRef.current) { + if (mapRef.current.getLayer("route")) mapRef.current.removeLayer("route"); + if (mapRef.current.getSource("route")) mapRef.current.removeSource("route"); + + mapRef.current.addSource("route", { type: "geojson", data: { type: "Feature", geometry: route } }); + mapRef.current.addLayer({ + id: "route", + type: "line", + source: "route", + layout: { "line-join": "round", "line-cap": "round" }, + paint: { "line-color": "#007bff", "line-width": 5 }, + }); + + setRouteInstructions( + steps.map((step) => ({ + instruction: step.maneuver.instruction, + type: step.maneuver.type, + modifier: step.maneuver.modifier, + })) + ); + + // Récupérer les hôtels à proximité du trajet + const hotels = await fetchNearbyHotels(waypointCoords); + addHotelMarkers(hotels); // Ajouter les marqueurs pour les hôtels + + // Récupérer les établissements dans la ville d'arrivée + const arrivalPlaces = await fetchPlacesInCities([endCity]); + addHotelMarkers(arrivalPlaces); // Ajouter les marqueurs pour les établissements dans la ville d'arrivée + + // Récupérer les établissements proches du trajet + const routeCoords = await getRouteCoordinates(startCoords, endCoords, waypointsToUse); + const nearbyPlaces = await fetchNearbyPlaces(routeCoords); + addHotelMarkers(nearbyPlaces); // Ajouter les marqueurs pour les établissements proches + } + } catch (error) { + console.error("Erreur lors de la récupération de l'itinéraire :", error); + } + }; + + const addParcoursMarkers = (parcoursData) => { + if (!parcoursData || !parcoursData.features) return; + + const map = mapRef.current; + const allParcours = []; + + parcoursData.features.forEach((parcours) => { + const { coordinates } = parcours.geometry; + const { name, distance } = parcours.properties; + const [lat, lon] = coordinates; + + allParcours.push({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [lon, lat], + }, + properties: { + name, + distance, + }, + }); + }); + + // Vérifiez si la source existe déjà avant de l'ajouter + if (map.getSource('parcours')) { + map.removeSource('parcours'); + } + + // Ajouter une source GeoJSON pour les clusters des parcours + map.addSource('parcours', { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: allParcours, + }, + cluster: true, + clusterMaxZoom: 14, + clusterRadius: 50, + }); + + // Ajouter des couches pour les clusters des parcours + map.addLayer({ + id: 'parcours-clusters', + type: 'circle', + source: 'parcours', + filter: ['has', 'point_count'], + paint: { + 'circle-color': '#51bbd6', + 'circle-radius': [ + 'interpolate', + ['linear'], + ['get', 'point_count'], + 0, + 20, + 100, + 40, + ], + }, + }); + + // Ajouter une couche pour afficher le nombre de points dans chaque cluster + map.addLayer({ + id: 'parcours-cluster-count', + type: 'symbol', + source: 'parcours', + filter: ['has', 'point_count'], + layout: { + 'text-field': '{point_count_abbreviated}', // Afficher le nombre abrégé de points dans le cluster + 'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'], + 'text-size': 12, + }, + paint: { + 'text-color': '#ffffff', + }, + }); + + // Ajouter une couche pour les parcours individuels (non clusterisés) + map.addLayer({ + id: 'parcours-individual-points', + type: 'circle', + source: 'parcours', + filter: ['!has', 'point_count'], + paint: { + 'circle-color': '#f28cb1', + 'circle-radius': 15, + }, + }); + }; + + const clearMarkers = () => { + markersRef.current.forEach((marker) => marker.remove()); + markersRef.current = []; + }; + + const fetchNearbyHotels = async (routeCoordinates) => { + if (!routeCoordinates || routeCoordinates.length === 0) return []; + + const hotels = []; + + // Parcourez chaque segment de l'itinéraire + for (let i = 0; i < routeCoordinates.length - 1; i++) { + const [start, end] = [routeCoordinates[i], routeCoordinates[i + 1]]; + const midLng = (start[0] + end[0]) / 2; + const midLat = (start[1] + end[1]) / 2; + + const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/hotel.json?proximity=${midLng},${midLat}&access_token=${mapboxgl.accessToken}`; + + console.log("URL Mapbox :", url); // Ajoutez cette ligne pour déboguer + + try { + const response = await fetch(url); + const data = await response.json(); + + if (data.features) { + const segmentHotels = data.features.map((feature) => ({ + name: feature.text || "Hôtel sans nom", + coords: feature.geometry.coordinates, + })); + hotels.push(...segmentHotels); // Ajouter les hôtels récupérés à la liste + } else { + console.warn("Aucun hôtel trouvé à proximité."); + } + } catch (error) { + console.error("Erreur lors de la récupération des hôtels :", error); + } + } + + console.log("Hôtels récupérés le long du trajet :", hotels); // Affichez la liste des hôtels récupérés + return hotels; // Retourner la liste des hôtels + }; + + const addHotelMarkers = (places) => { + const map = mapRef.current; + + if (!Array.isArray(places)) { + console.warn("Les établissements doivent être un tableau."); + return; // Ne pas continuer si places n'est pas un tableau + } + + // Supprimer les anciens marqueurs avant d'ajouter de nouveaux + clearMarkers(); + + places.forEach((place) => { + const marker = new mapboxgl.Marker({ color: "blue" }) // Couleur pour les établissements + .setLngLat(place.coords) + .setPopup(new mapboxgl.Popup().setHTML(`${place.name}`)) + .addTo(map); + + markersRef.current.push(marker); // Ajouter le marqueur à la référence + }); + }; + + const fetchPlacesInCities = async (cities) => { + const allPlaces = []; + + for (const city of cities) { + const coords = await fetchCoordinatesFromCity(city); + if (!coords) continue; // Passer à la prochaine ville si les coordonnées ne sont pas disponibles + + const [lng, lat] = coords; + const url = `https://api.mapbox.com/search/searchbox/v1/forward?q=${encodeURIComponent("restaurant")}&proximity=${lng},${lat}&access_token=${mapboxgl.accessToken}`; + + try { + const response = await fetch(url); + const data = await response.json(); + + if (data.features) { + const places = data.features.map((feature) => ({ + name: feature.properties.name || "Établissement sans nom", + coords: feature.geometry.coordinates, + description: feature.properties.description || "Aucune description disponible", + image: feature.properties.image || "URL_de_l_image_par_défaut.jpg", + })); + allPlaces.push(...places); // Ajouter les établissements récupérés à la liste + } + } catch (error) { + console.error("Erreur lors de la récupération des établissements :", error); + } + } + + return allPlaces; // Retourner la liste de tous les établissements + }; + + const fetchNearbyPlaces = async (routeCoords) => { + const places = []; + for (const coord of routeCoords) { + const [lng, lat] = coord; // Décomposer les coordonnées + const url = `https://api.mapbox.com/search/searchbox/v1/forward?q=${encodeURIComponent("restaurant")}&proximity=${lng},${lat}&access_token=${mapboxgl.accessToken}`; + + try { + const response = await fetch(url); + const data = await response.json(); + + if (data.features) { + const nearbyPlaces = data.features.map((feature) => ({ + name: feature.properties.name || "Établissement sans nom", + coords: feature.geometry.coordinates, + description: feature.properties.description || "Aucune description disponible", + image: feature.properties.image || "URL_de_l_image_par_défaut.jpg", + })); + places.push(...nearbyPlaces); // Ajouter les établissements à la liste + } + } catch (error) { + console.error("Erreur lors de la récupération des établissements :", error); + } + } + return places; // Retourner la liste des établissements + }; + + useEffect(() => { + const fetchPlaces = async () => { + const cities = [endCity, ...waypoints.map(waypoint => waypoint.name)]; // Inclure la ville d'arrivée et les villes de passage + const placesInCities = await fetchPlacesInCities(cities); + setPlaces(placesInCities); // Mettez à jour l'état avec les établissements récupérés + addHotelMarkers(placesInCities); // Ajouter les marqueurs pour les établissements récupérés + }; + + fetchPlaces(); + }, [endCoords, waypoints]); // Déclencher cet effet lorsque endCoords ou waypoints changent + + return ( + + + + +
+ +
+ +
+ +
+ + {/* Boîte pour ajouter une ville de passage */} + {showWaypointsBox && ( // Affichez la boîte seulement si showWaypointsBox est vrai +
+

Ajouter une ville (point de passage)

+
+ +
+
    + {suggestions.map((suggestion, index) => ( +
  • addWaypoint(suggestion)} + > + {suggestion.name} +
  • + ))} +
+ + {/* Liste des villes de passage ajoutées */} +
+

Villes de passage :

+
    + {waypoints.map((waypoint, index) => ( +
  • + {waypoint.name} + +
  • + ))} +
+
+
+ )} + + {/* Icône d'œil fixe pour cacher ou afficher la boîte d'ajout de ville de passage */} + +
+
+ ); +} + diff --git a/src/components/chatbot/Chatbot.jsx b/src/components/chatbot/Chatbot.jsx new file mode 100644 index 0000000..d9ddda7 --- /dev/null +++ b/src/components/chatbot/Chatbot.jsx @@ -0,0 +1,148 @@ +import React, { useState, useRef, useEffect } from "react"; + +const Chatbot = () => { + const [messages, setMessages] = useState([ + { sender: "bot", text: "Salut ! Je suis là pour t’aider, demande-moi ce que tu veux." }, + ]); + const [input, setInput] = useState(""); + const messagesEndRef = useRef(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + const sendMessage = () => { + if (!input.trim()) return; + + const userMessage = input.trim(); + setMessages((prev) => [...prev, { sender: "user", text: userMessage }]); + setInput(""); + + // Simuler la réponse automatique côté front + const lowerMsg = userMessage.toLowerCase(); + let botReply = "🤖 Je suis en mode démo. Essaie de me demander une randonnée en IDF !"; + + if (lowerMsg.includes("randonnée") || lowerMsg.includes("rando")) { + botReply = `Voici quelques idées de randonnées en Île-de-France : +• Forêt de Fontainebleau 🌳 +• Parc naturel du Vexin 🗺️ +• Gorges de Franchard 🥾 +• Promenade bleue le long de la Seine 🚶‍♀️`; + } else if (lowerMsg.includes("bonjour") || lowerMsg.includes("salut")) { + botReply = "👋 Salut ! Comment puis-je t’aider ?"; + } else if (lowerMsg.includes("merci")) { + botReply = "🙏 Avec plaisir ! N’hésite pas si tu as d’autres questions."; + } + + setTimeout(() => { + setMessages((prev) => [...prev, { sender: "bot", text: botReply }]); + }, 600); + }; + + const handleKeyDown = (e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; + + return ( +
+
+
+ {messages.map((msg, idx) => ( +
+ {msg.text} +
+ ))} +
+
+ +
+