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}
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
+
+const styles = {
+ wrapper: {
+ display: "flex",
+ justifyContent: "center",
+ alignItems: "center",
+ height: "100vh",
+ backgroundColor: "#f0f2f5",
+ },
+ chatContainer: {
+ width: "600px",
+ height: "800px",
+ border: "1px solid #ccc",
+ borderRadius: "8px",
+ display: "flex",
+ flexDirection: "column",
+ backgroundColor: "#fafafa",
+ fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
+ boxShadow: "0 4px 15px rgba(0,0,0,0.2)",
+ },
+ messagesContainer: {
+ flex: 1,
+ padding: "15px",
+ display: "flex",
+ flexDirection: "column",
+ overflowY: "auto",
+ gap: "10px",
+ },
+ message: {
+ maxWidth: "70%",
+ padding: "12px 18px",
+ borderRadius: "20px",
+ fontSize: "15px",
+ lineHeight: "1.5",
+ wordWrap: "break-word",
+ },
+ inputContainer: {
+ display: "flex",
+ padding: "15px",
+ borderTop: "1px solid #ccc",
+ gap: "12px",
+ },
+ input: {
+ flex: 1,
+ resize: "none",
+ padding: "12px",
+ fontSize: "15px",
+ borderRadius: "8px",
+ border: "1px solid #ccc",
+ },
+ button: {
+ backgroundColor: "#4a90e2",
+ border: "none",
+ borderRadius: "8px",
+ color: "white",
+ fontWeight: "bold",
+ padding: "12px 25px",
+ cursor: "pointer",
+ },
+};
+
+export default Chatbot;
diff --git a/src/components/news/FavoriteList.jsx b/src/components/news/FavoriteList.jsx
new file mode 100644
index 0000000..5e0ffee
--- /dev/null
+++ b/src/components/news/FavoriteList.jsx
@@ -0,0 +1,63 @@
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+import api from "@/security/auth/Api";
+import { useEffect, useState } from "react";
+
+function FavoritesList() {
+ const [favorites, setFavorites] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ api.get("/favorites")
+ .then((res) => {
+ setFavorites(res.data);
+ setLoading(false);
+ })
+ .catch((err) => {
+ console.error("[FavoritesList] Erreur lors du fetch:", err);
+ setLoading(false);
+ });
+ }, []);
+
+ return (
+
+
⭐ Mes articles favoris
+
+ {loading ? (
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+ ) : favorites.length === 0 ? (
+
Aucun favori pour le moment.
+ ) : (
+
+ {favorites.map((fav, index) => (
+
+ {fav.image_url && (
+
+ )}
+
+ {fav.title}
+
+ {fav.description?.slice(0, 200)}...
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default FavoritesList;
diff --git a/src/components/sidebar/nav-main.jsx b/src/components/sidebar/nav-main.jsx
index 44d9052..969feaf 100644
--- a/src/components/sidebar/nav-main.jsx
+++ b/src/components/sidebar/nav-main.jsx
@@ -1,79 +1,139 @@
-"use client";
-
-import { ChevronRight } from "lucide-react"
-import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from "@/components/ui/collapsible"
-import {
- SidebarGroup,
- SidebarGroupLabel,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
- SidebarMenuSub,
- SidebarMenuSubButton,
- SidebarMenuSubItem,
- useSidebar,
-} from "@/components/ui/sidebar"
-import { NavItinerary } from "@/components/sidebar/NavItinerary"
-import { NavSettings } from "@/components/sidebar/NavSettings";
-
-export function NavMain({ items, setStartCoords, routeDuration, setEndCoords, setTransportMode, routeInstructions, transportMode }) {
- const { state } = useSidebar(); // "expanded" ou "collapsed"
-
- return (
-
- Navigation
-
- {items.map((item) => (
-
-
-
-
- {item.icon && }
- {item.title}
-
-
-
-
- {item.showItinerary && (
-
- )}
- {item.showSettings && (
-
- )}
- {item.items && !item.showItinerary && !item.showSettings && (
-
- {item.items?.map((subItem) => (
-
-
-
- {subItem.title}
-
-
-
- ))}
-
- )}
-
-
-
- ))}
-
-
- );
-}
+"use client";
+
+import React from "react";
+import { ChevronRight } from "lucide-react"
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible"
+
+import {
+ SidebarGroup,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ useSidebar,
+} from "@/components/ui/sidebar"
+import { NavItinerary } from "@/components/sidebar/NavItinerary"
+import { NavSettings } from "@/components/sidebar/NavSettings";
+import { FaSave } from "react-icons/fa";
+import mapboxgl from "mapbox-gl";
+
+export function NavMain({ items, setStartCoords, routeDuration, setEndCoords, setTransportMode, routeInstructions, transportMode, onSaveRoute, setWaypoints, addWaypointMarker, pois, places = [], startCoords, endCoords, waypoints }) {
+ const { state } = useSidebar(); // "expanded" ou "collapsed"
+
+ console.log("Données reçues dans NavMain :", { startCoords, endCoords, waypoints, transportMode, routeInstructions }); // Pour déboguer
+
+ const handleSaveRoute = async () => {
+ const routeData = {
+ startCoords,
+ endCoords,
+ waypoints,
+ transportMode,
+ routeInstructions,
+ };
+
+ try {
+ const response = await fetch('/api/save-route', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(routeData),
+ });
+
+ if (!response.ok) {
+ throw new Error('Erreur lors de l\'enregistrement du trajet');
+ }
+
+ const result = await response.json();
+ alert('Trajet enregistré avec succès !');
+ console.log(result); // Affichez le résultat si nécessaire
+ } catch (error) {
+ console.error('Erreur:', error);
+ alert('Échec de l\'enregistrement du trajet.');
+ }
+ };
+
+ return (
+
+ Navigation
+
+ {items.map((item) => (
+
+
+
+
+ {item.icon && }
+ {item.title}
+
+
+
+
+ {item.showItinerary && (
+
+ )}
+ {item.showSettings && (
+
+ )}
+ {item.items && !item.showItinerary && !item.showSettings && (
+
+ {item.items?.map((subItem) => (
+
+
+
+ {subItem.title}
+
+
+
+ ))}
+
+ )}
+
+
+
+ ))}
+
+
+
+ {/* Afficher la liste des établissements */}
+
+
Établissements à proximité :
+
+ {places.length > 0 ? (
+ places.map((place, index) => (
+ -
+ {place.name}
+
{place.description}
+
+ ))
+ ) : (
+ - Aucun établissement trouvé.
+ )}
+
+
+
+ );
+}
diff --git a/src/components/ui/select.jsx b/src/components/ui/select.jsx
new file mode 100644
index 0000000..a7b197e
--- /dev/null
+++ b/src/components/ui/select.jsx
@@ -0,0 +1,120 @@
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}>
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/src/layouts/BaseLayouts.jsx b/src/layouts/BaseLayouts.jsx
index 546ba0f..ddcbad4 100644
--- a/src/layouts/BaseLayouts.jsx
+++ b/src/layouts/BaseLayouts.jsx
@@ -1,8 +1,7 @@
-import React from 'react';
import { Outlet, useLocation } from 'react-router-dom';
+import Background from './Background.jsx';
import Footer from './components/Footer.jsx';
import Header from './components/Header.jsx';
-import Background from './Background.jsx';
const BaseLayout = () => {
const location = useLocation();
@@ -10,10 +9,11 @@ const BaseLayout = () => {
const isRegisterPage = location.pathname === '/register';
const isMapBoxPage = location.pathname === '/mapbox';
const isHelpAdminPage = location.pathname === '/help-admin';
-
+ const isEcoNewsPage = location.pathname === '/eco-news';
+ const isCarbonPage = location.pathname === '/carbon-footprint';
return (
- {!isMapBoxPage && !isHelpAdminPage &&
}
+ {!isMapBoxPage && !isHelpAdminPage && !isEcoNewsPage && !isCarbonPage &&
}
{!isRegisterPage && !isLoginPage && !isMapBoxPage &&
} {/* Affiche le Header sauf sur /login et /register */}
{/* Ajustez pt-20 selon la hauteur de votre Header */}
diff --git a/src/layouts/components/Footer.jsx b/src/layouts/components/Footer.jsx
index f9f2d8c..a3b497e 100644
--- a/src/layouts/components/Footer.jsx
+++ b/src/layouts/components/Footer.jsx
@@ -1,15 +1,15 @@
// Footer.jsx
-import React, { useState } from 'react';
-import { Link } from 'react-router-dom';
-import { Icon } from '@iconify/react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
- DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
+import { Icon } from '@iconify/react';
+import { useState } from 'react';
+import { Link } from 'react-router-dom';
const Footer = () => {
const [region, setRegion] = useState('France');
@@ -33,25 +33,23 @@ const Footer = () => {
to="/en-fr/about"
className="text-sm md:text-base lg:text-xl leading-7 md:leading-11 lg:leading-13 hover:opacity-70 transition duration-300 text-brand-dark dark:text-gray-200"
>
- A propos
+ À propos
- Blog
+ Empreinte carbone
- Forum
+ Éco-actualités
@@ -59,19 +57,19 @@ const Footer = () => {
{/* Section Get our App */}
- {/* Sélecteur de Région avec ShadCN Dropdown */}
+ {/* Sélecteur de Région */}
diff --git a/src/layouts/components/Header.jsx b/src/layouts/components/Header.jsx
index b4b96fe..e52d208 100644
--- a/src/layouts/components/Header.jsx
+++ b/src/layouts/components/Header.jsx
@@ -1,19 +1,17 @@
-import React from 'react';
-import { Link, useNavigate } from 'react-router-dom';
-import { useAuth } from '@/security/auth/AuthContext';
-import { ModeToggle } from '../../hooks/mode-toggle.jsx';
import { Button } from '@/components/ui/button';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import Helper from '@/help/Helper.jsx';
+import { useAuth } from '@/security/auth/AuthContext';
import { CircleUserRound } from 'lucide-react';
-
+import { Link, useNavigate } from 'react-router-dom';
+import { ModeToggle } from '../../hooks/mode-toggle.jsx';
const Header = () => {
const { user, logout } = useAuth();
const navigate = useNavigate();
@@ -51,10 +49,13 @@ const Header = () => {
GreenTrip
-
+
{renderUserOptions()}
-
+
+
+
+
{user ? (
@@ -71,7 +72,7 @@ const Header = () => {
) : (!isHelpAdminPage &&
-
+
)}
diff --git a/src/pages/CarbonCalculator.jsx b/src/pages/CarbonCalculator.jsx
new file mode 100644
index 0000000..dcfa6db
--- /dev/null
+++ b/src/pages/CarbonCalculator.jsx
@@ -0,0 +1,192 @@
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Sparkles } from "lucide-react";
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+const emissions = {
+ voiture: 0.2,
+ train: 0.04,
+ avion: 0.25,
+ velo: 0.0,
+};
+
+function haversineDistance(lat1, lon1, lat2, lon2) {
+ const toRad = (x) => (x * Math.PI) / 180;
+ const R = 6371; // km
+ const dLat = toRad(lat2 - lat1);
+ const dLon = toRad(lon2 - lon1);
+ const a =
+ Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(toRad(lat1)) *
+ Math.cos(toRad(lat2)) *
+ Math.sin(dLon / 2) *
+ Math.sin(dLon / 2);
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ return R * c;
+}
+
+export default function CarbonCalculator() {
+ const [start, setStart] = useState("");
+ const [end, setEnd] = useState("");
+ const [transport, setTransport] = useState("voiture");
+ const [result, setResult] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [animatedCO2, setAnimatedCO2] = useState(0);
+ const [startSuggestions, setStartSuggestions] = useState([]);
+ const [endSuggestions, setEndSuggestions] = useState([]);
+ const navigate = useNavigate();
+
+ const mapboxToken = "pk.eyJ1Ijoic3lsdmFpbmNvc3RlcyIsImEiOiJjbTNxZXNtN3cwa2hpMmpxdWd2cndhdnYwIn0.V2ZAp-BqZq6KIHQ6Lu8eAQ";
+
+ useEffect(() => {
+ if (result?.co2) {
+ let start = 0;
+ const target = parseFloat(result.co2);
+ const step = target / 50;
+ const interval = setInterval(() => {
+ start += step;
+ if (start >= target) {
+ start = target;
+ clearInterval(interval);
+ }
+ setAnimatedCO2(start.toFixed(2));
+ }, 10);
+ return () => clearInterval(interval);
+ }
+ }, [result]);
+
+ const fetchSuggestions = async (query, setSuggestions) => {
+ if (!query.trim()) {
+ setSuggestions([]);
+ return;
+ }
+ try {
+ const res = await fetch(`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(query)}.json?types=place&autocomplete=true&access_token=${mapboxToken}`);
+ const data = await res.json();
+ setSuggestions(data.features.map((f) => ({ name: f.place_name, coords: f.center })));
+ } catch (err) {
+ console.error("Erreur suggestions Mapbox:", err);
+ }
+ };
+
+ const getCoordinates = async (city) => {
+ const res = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(city)}&format=json&limit=1`);
+ const data = await res.json();
+ if (data.length === 0) throw new Error("Ville non trouvée");
+ return { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) };
+ };
+
+ const calculate = async () => {
+ try {
+ setLoading(true);
+ const startCoords = await getCoordinates(start);
+ const endCoords = await getCoordinates(end);
+ const distance = haversineDistance(startCoords.lat, startCoords.lon, endCoords.lat, endCoords.lon);
+ const co2 = (distance * emissions[transport]).toFixed(2);
+ setResult({ distance: distance.toFixed(1), co2 });
+ setLoading(false);
+ } catch (e) {
+ alert("Erreur : " + e.message);
+ setLoading(false);
+ }
+ };
+
+ const getImpactMessage = () => {
+ if (!result) return null;
+ const co2 = parseFloat(result.co2);
+ if (co2 < 2) {
+ return { text: "🌱 Excellent choix ! Ton empreinte est très faible.", color: "text-green-600" };
+ } else if (co2 < 10) {
+ return { text: "🧐 Moyenne acceptable, mais on peut faire mieux.", color: "text-yellow-600" };
+ } else {
+ return { text: "⚠️ Forte émission. Essaie un transport plus durable.", color: "text-red-600" };
+ }
+ };
+
+ const impact = getImpactMessage();
+
+ return (
+
+
+
+
+ Empreinte Carbone
+
+
Estime ton émission de CO₂ selon ton transport et ton trajet.
+
+
+
+
+
{
+ setStart(e.target.value);
+ fetchSuggestions(e.target.value, setStartSuggestions);
+ }} />
+ {startSuggestions.length > 0 && (
+
+ {startSuggestions.map((s, i) => (
+ - {
+ setStart(s.name);
+ setStartSuggestions([]);
+ }}>{s.name}
+ ))}
+
+ )}
+
+
+
{
+ setEnd(e.target.value);
+ fetchSuggestions(e.target.value, setEndSuggestions);
+ }} />
+ {endSuggestions.length > 0 && (
+
+ {endSuggestions.map((s, i) => (
+ - {
+ setEnd(s.name);
+ setEndSuggestions([]);
+ }}>{s.name}
+ ))}
+
+ )}
+
+
+
+
+
+
+ {result && (
+ <>
+
+ 🌿 Résultats
+ 📏 Distance estimée : {result.distance} km
+ 🌫️ Émission estimée : {animatedCO2} kg CO₂
+ {impact && {impact.text}
}
+
+
+
+
📚 Découvre comment réduire ton empreinte carbone
+
+
+ >
+ )}
+
+
+ Basé sur des moyennes d’émission par km par transport — Source : ADEME
+
+
+ );
+}
diff --git a/src/pages/EcoNews.jsx b/src/pages/EcoNews.jsx
new file mode 100644
index 0000000..2306500
--- /dev/null
+++ b/src/pages/EcoNews.jsx
@@ -0,0 +1,308 @@
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Skeleton } from "@/components/ui/skeleton";
+import api from "@/security/auth/Api";
+import { useAuth } from "@/security/auth/AuthContext";
+import { useEffect, useState } from "react";
+import { Link } from 'react-router-dom';
+
+const topics = [
+ { label: "🌿 Écologie", value: "écologie" },
+ { label: "🚴 Voyages durables", value: "voyage durable" },
+ { label: "🌍 Climat", value: "climat" },
+ { label: "🏕️ Tourisme vert", value: "tourisme vert" },
+ { label: "🔋 Énergies renouvelables", value: "énergies renouvelables" },
+ { label: "🛤️ Mobilité douce", value: "mobilité douce" },
+];
+
+function ArticleCard({
+ article,
+ isFavorite,
+ onFavorite,
+ onView,
+ isAuthenticated,
+ openDialog
+}) {
+ return (
+
+ {article.image_url && (
+
+ )}
+
+ {article.title}
+
+ {new Date(article.pubDate).toLocaleDateString()}
+
+
+ {article.description?.slice(0, 200)}…
+
+
+
+
+
+
+
+ );
+}
+
+export default function EcoNews() {
+ const { user } = useAuth();
+ const isAuthenticated = !!user;
+
+ const [articles, setArticles] = useState([]);
+ const [favorites, setFavorites] = useState([]);
+ const [stats, setStats] = useState(null);
+ const [topViews, setTopViews] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [search, setSearch] = useState("");
+ const [dialogOpen, setDialogOpen] = useState(false);
+
+ useEffect(() => {
+ if (!isAuthenticated) return;
+ api
+ .get("/favorites")
+ .then((res) => setFavorites(res.data || []))
+ .catch(console.error);
+ }, [isAuthenticated]);
+
+ useEffect(() => {
+ if (!search) return;
+ setLoading(true);
+ api
+ .get(`/news/external-news?topic=${encodeURIComponent(search)}`)
+ .then((res) => {
+ const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
+ setArticles(data.results || []);
+ setLoading(false);
+ })
+ .catch((err) => {
+ console.error("[EcoNews] Erreur de chargement des articles :", err);
+ setLoading(false);
+ });
+ }, [search]);
+
+ useEffect(() => {
+ if (!isAuthenticated) return;
+ api.get("news/views/stats").then((res) => setStats(res.data));
+ api.get("news/views/top").then((res) => setTopViews(res.data));
+ }, [isAuthenticated]);
+
+ const downloadCsv = async () => {
+ try {
+ const res = await api.get("/news/views/export", { responseType: "blob" });
+ const blob = new Blob([res.data], { type: "text/csv" });
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.setAttribute("download", "views.csv");
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ } catch (err) {
+ console.error("Erreur lors du téléchargement CSV:", err);
+ }
+ };
+
+ const trackView = (article) => {
+ api
+ .post("news/view", { title: article.title, url: article.link })
+ .then(() => {
+ window.open(article.link, "_blank");
+ api.get("news/views/stats").then((res) => setStats(res.data));
+ api.get("news/views/top").then((res) => setTopViews(res.data));
+ });
+ };
+
+ const toggleFavorite = (article) => {
+ const url = article.link || article.url;
+ const exists = favorites.some((f) => f.url === url);
+
+ if (exists) {
+ api
+ .delete(`/favorites?url=${encodeURIComponent(url)}`)
+ .then(() =>
+ setFavorites((prev) => prev.filter((f) => f.url !== url))
+ )
+ .catch(console.error);
+ } else {
+ const payload = {
+ title: article.title,
+ url: url,
+ imageUrl: article.image_url,
+ };
+ api
+ .post("/favorites", payload)
+ .then(() =>
+ setFavorites((prev) => [...prev, payload])
+ )
+ .catch(console.error);
+ }
+ };
+
+ return (
+
+
📰 Actualités écologiques
+
+
+
+
+
+ {isAuthenticated && (
+
+
+
+ ⭐ Mes favoris ({favorites.length})
+
+
+ {favorites.length === 0 ? (
+
+ Aucun favori pour le moment.
+
+ ) : (
+ favorites.map((fav, idx) => (
+ setDialogOpen(true)}
+ />
+ ))
+ )}
+
+
+
+ )}
+
+ {loading ? (
+
+ {[...Array(6)].map((_, i) => (
+
+ ))}
+
+ ) : (
+
+ {articles.map((article, idx) => (
+
f.url === article.link)}
+ onFavorite={toggleFavorite}
+ onView={trackView}
+ isAuthenticated={isAuthenticated}
+ openDialog={() => setDialogOpen(true)}
+ />
+ ))}
+
+ )}
+
+
+ {isAuthenticated && stats && (
+
+
+
+
+
+
+
+ 📊 Statistiques
+
+ Total vues : {stats.totalViews}
+
+
+ Dernier article consulté :{" "}
+ {stats.lastViewedTitle || "Aucun"}
+
+
+
+
+
+
+
+ 🔥 Top articles
+
+ {topViews.length === 0 ? (
+
+ Aucun article consulté pour l'instant.
+
+ ) : (
+
+ {topViews.map((a, i) => (
+ -
+
+ {a.url}
+ {" "}
+ ({a.views} vues)
+
+ ))}
+
+ )}
+
+
+
+ )}
+
+
+
+
+ );
+}
+EcoNews.displayName = "EcoNews";
diff --git a/src/router/routes.jsx b/src/router/routes.jsx
index f5b944c..290d549 100644
--- a/src/router/routes.jsx
+++ b/src/router/routes.jsx
@@ -7,6 +7,10 @@ import Test from '../pages/Test.jsx';
import Login from '../security/Login.jsx';
import Register from '../security/Register.jsx';
import HelpAdmin from '../help/HelpAdmin.jsx'
+import EcoNews from '../pages/EcoNews.jsx';
+import CarbonCalculator from '../pages/CarbonCalculator.jsx';
+import Chatbot from '@/components/chatbot/Chatbot.jsx';
+
const routes = [
{
@@ -25,6 +29,14 @@ const routes = [
path: '/mapbox',
element:
,
},
+ {
+ path: '/eco-news',
+ element:
,
+ },
+ {
+ path: '/carbon-footprint',
+ element:
,
+ },
{
path: '/login',
element:
,
@@ -40,7 +52,11 @@ const routes = [
{
path: '/help-admin',
element:
,
- }
+ },
+ {
+ path: '/chatbot',
+ element:
,
+ },
],
},
];