This commit is contained in:
parent
842f777c34
commit
3babb277d0
|
|
@ -346,9 +346,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||||
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
|
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -466,9 +466,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.39.1",
|
"version": "9.39.2",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
|
||||||
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
|
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -673,9 +673,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||||
"version": "24.3.1",
|
"version": "24.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.4.1.tgz",
|
||||||
"integrity": "sha512-TUM5JD40H2mgtVXl5IwWz03BuQabw8oZQLJTmPpJA0YTYF+B+oZppy5lNMO6bMvHzB+/5mxqW9VLG3wFdeqtOw==",
|
"integrity": "sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||||
|
|
@ -702,9 +702,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@maplibre/vt-pbf": {
|
"node_modules/@maplibre/vt-pbf": {
|
||||||
"version": "4.1.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.2.0.tgz",
|
||||||
"integrity": "sha512-9LjFAoWtxdGRns8RK9vG3Fcw/fb3eHMxvAn2jffwn3jnVO1k49VOv6+FEza70rK7WzF8GnBiKa0K39RyfevKUw==",
|
"integrity": "sha512-bxrk/kQUwWXZgmqYgwOCnZCMONCRi3MJMqJdza4T3E4AeR5i+VyMnaJ8iDWtWxdfEAJRtrzIOeJtxZSy5mFrFA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/point-geometry": "^1.1.0",
|
"@mapbox/point-geometry": "^1.1.0",
|
||||||
|
|
@ -717,9 +717,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
||||||
"integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==",
|
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
|
@ -727,6 +727,10 @@
|
||||||
"@emnapi/core": "^1.7.1",
|
"@emnapi/core": "^1.7.1",
|
||||||
"@emnapi/runtime": "^1.7.1",
|
"@emnapi/runtime": "^1.7.1",
|
||||||
"@tybys/wasm-util": "^0.10.1"
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/runtime": {
|
"node_modules/@oxc-project/runtime": {
|
||||||
|
|
@ -1219,9 +1223,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.9.5",
|
"version": "2.9.11",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
||||||
"integrity": "sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==",
|
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -1298,9 +1302,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001759",
|
"version": "1.0.30001762",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
|
||||||
"integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==",
|
"integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -1556,9 +1560,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.39.1",
|
"version": "9.39.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
|
|
@ -1569,7 +1573,7 @@
|
||||||
"@eslint/config-helpers": "^0.4.2",
|
"@eslint/config-helpers": "^0.4.2",
|
||||||
"@eslint/core": "^0.17.0",
|
"@eslint/core": "^0.17.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.39.1",
|
"@eslint/js": "9.39.2",
|
||||||
"@eslint/plugin-kit": "^0.4.1",
|
"@eslint/plugin-kit": "^0.4.1",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
|
|
@ -1637,9 +1641,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react-refresh": {
|
"node_modules/eslint-plugin-react-refresh": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.26",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
|
||||||
"integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==",
|
"integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
@ -1695,9 +1699,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esquery": {
|
"node_modules/esquery": {
|
||||||
"version": "1.6.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||||
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -2212,9 +2216,9 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/keycloak-js": {
|
"node_modules/keycloak-js": {
|
||||||
"version": "26.2.1",
|
"version": "26.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.2.tgz",
|
||||||
"integrity": "sha512-bZt6fQj/TLBAmivXSxSlqAJxBx/knNZDQGJIW4ensGYGN4N6tUKV8Zj3Y7/LOV8eIpvWsvqV70fbACihK8Ze0Q==",
|
"integrity": "sha512-ug7pNZ1xNkd7PPkerOJCEU2VnUhS7CYStDOCFJgqCNQ64h53ppxaKrh4iXH0xM8hFu5b1W6e6lsyYWqBMvaQFg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"test"
|
"test"
|
||||||
|
|
@ -2539,9 +2543,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/maplibre-gl": {
|
"node_modules/maplibre-gl": {
|
||||||
"version": "5.14.0",
|
"version": "5.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.15.0.tgz",
|
||||||
"integrity": "sha512-O2ok6N/bQ9NA9nJ22r/PRQQYkUe9JwfDMjBPkQ+8OwsVH4TpA5skIAM2wc0k+rni5lVbAVONVyBvgi1rF2vEPA==",
|
"integrity": "sha512-pPeu/t4yPDX/+Uf9ibLUdmaKbNMlGxMAX+tBednYukol2qNk2TZXAlhdohWxjVvTO3is8crrUYv3Ok02oAaKzA==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/geojson-rewind": "^0.5.2",
|
"@mapbox/geojson-rewind": "^0.5.2",
|
||||||
|
|
@ -2551,9 +2555,9 @@
|
||||||
"@mapbox/unitbezier": "^0.0.1",
|
"@mapbox/unitbezier": "^0.0.1",
|
||||||
"@mapbox/vector-tile": "^2.0.4",
|
"@mapbox/vector-tile": "^2.0.4",
|
||||||
"@mapbox/whoots-js": "^3.1.0",
|
"@mapbox/whoots-js": "^3.1.0",
|
||||||
"@maplibre/maplibre-gl-style-spec": "^24.3.1",
|
"@maplibre/maplibre-gl-style-spec": "^24.4.1",
|
||||||
"@maplibre/mlt": "^1.1.2",
|
"@maplibre/mlt": "^1.1.2",
|
||||||
"@maplibre/vt-pbf": "^4.1.0",
|
"@maplibre/vt-pbf": "^4.2.0",
|
||||||
"@types/geojson": "^7946.0.16",
|
"@types/geojson": "^7946.0.16",
|
||||||
"@types/geojson-vt": "3.2.5",
|
"@types/geojson-vt": "3.2.5",
|
||||||
"@types/supercluster": "^7.1.3",
|
"@types/supercluster": "^7.1.3",
|
||||||
|
|
@ -2864,9 +2868,9 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.1",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -2874,16 +2878,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.2.1",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^19.2.1"
|
"react": "^19.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
|
|
@ -2897,9 +2901,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.10.1",
|
"version": "7.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz",
|
||||||
"integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==",
|
"integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
|
|
@ -2919,12 +2923,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "7.10.1",
|
"version": "7.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.11.0.tgz",
|
||||||
"integrity": "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==",
|
"integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-router": "7.10.1"
|
"react-router": "7.11.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
|
@ -3134,9 +3138,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||||
"integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
|
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -3299,9 +3303,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.1.13",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.4.tgz",
|
||||||
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
|
"integrity": "sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,11 @@ async function resolveAssetRefsInPosts(posts) {
|
||||||
[p?.image_small, p?.image_medium, p?.image_large, p?.image, p?.media_url].forEach((v) => {
|
[p?.image_small, p?.image_medium, p?.image_large, p?.image, p?.media_url].forEach((v) => {
|
||||||
if (isAssetRef(v)) ids.push(v.replace("asset://", ""));
|
if (isAssetRef(v)) ids.push(v.replace("asset://", ""));
|
||||||
});
|
});
|
||||||
|
if (Array.isArray(p?.media_urls)) {
|
||||||
|
p.media_urls.forEach((v) => {
|
||||||
|
if (isAssetRef(v)) ids.push(v.replace("asset://", ""));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const uniq = Array.from(new Set(ids));
|
const uniq = Array.from(new Set(ids));
|
||||||
if (!uniq.length) return posts;
|
if (!uniq.length) return posts;
|
||||||
|
|
@ -127,6 +132,15 @@ async function resolveAssetRefsInPosts(posts) {
|
||||||
if (resolved[id]) clone[k] = resolved[id];
|
if (resolved[id]) clone[k] = resolved[id];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (Array.isArray(clone.media_urls)) {
|
||||||
|
clone.media_urls = clone.media_urls.map((value) => {
|
||||||
|
if (isAssetRef(value)) {
|
||||||
|
const id = value.replace("asset://", "");
|
||||||
|
return resolved[id] || value;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
return clone;
|
return clone;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,92 @@ import UserProfile from "../Profile/UserProfile";
|
||||||
import { fetchPostById, fetchPostByUrl } from "../../api/client";
|
import { fetchPostById, fetchPostByUrl } from "../../api/client";
|
||||||
import { trackEvent } from "../../utils/analytics";
|
import { trackEvent } from "../../utils/analytics";
|
||||||
|
|
||||||
|
const CONTENT_CREATOR_MODES = [
|
||||||
|
{
|
||||||
|
id: "live",
|
||||||
|
label: "Live video",
|
||||||
|
icon: "fa-solid fa-broadcast-tower",
|
||||||
|
hint: "Broadcast a live scene with a headline or link.",
|
||||||
|
detail: [
|
||||||
|
"Drop the stream URL or quick headline to pin the feed instantly.",
|
||||||
|
"We’ll open the location where you place the crosshair so viewers know where to tune.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "link",
|
||||||
|
label: "Link story",
|
||||||
|
icon: "fa-solid fa-link",
|
||||||
|
hint: "Share a URL and we’ll prefill the preview.",
|
||||||
|
detail: [
|
||||||
|
"Paste a news link, playlist, or update so we can fetch an image and snippet.",
|
||||||
|
"Choose the most relevant pin so the story belongs to the right neighborhood.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "photo",
|
||||||
|
label: "Photo story",
|
||||||
|
icon: "fa-solid fa-camera-retro",
|
||||||
|
hint: "Upload or link an image with context.",
|
||||||
|
detail: [
|
||||||
|
"Upload a photo set or paste an image URL from your gallery.",
|
||||||
|
"Caption quickly so the community knows what they’re looking at.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "text",
|
||||||
|
label: "Written update",
|
||||||
|
icon: "fa-solid fa-pen-to-square",
|
||||||
|
hint: "Type a short report or microblog.",
|
||||||
|
detail: [
|
||||||
|
"Write the key message in the title field (or add more context in the body).",
|
||||||
|
"Use subcategory filters to mark the tone: News, Event, Market, etc.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "other",
|
||||||
|
label: "Other",
|
||||||
|
icon: "fa-solid fa-ellipsis",
|
||||||
|
hint: "Custom content that spans media types.",
|
||||||
|
detail: [
|
||||||
|
"Mix media, public drafts, or reference notes in one place.",
|
||||||
|
"Tag a region or Island to give the update a home.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const MODE_FLOW_CONFIG = {
|
||||||
|
live: {
|
||||||
|
hero: "Pin where the live scene is happening and paste your stream link or headline.",
|
||||||
|
placeholder: "Streaming link or headline",
|
||||||
|
hint: "We’ll treat this as a live broadcast and show it as soon as the pin lands.",
|
||||||
|
bodyPlaceholder: "Context for viewers, optional.",
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
hero: "Drop the link, describe what it is, and pin the relevant spot.",
|
||||||
|
placeholder: "Article or playlist URL",
|
||||||
|
hint: "We’ll auto-fetch a preview once the link is detected.",
|
||||||
|
bodyPlaceholder: "What matters about this link?",
|
||||||
|
},
|
||||||
|
photo: {
|
||||||
|
hero: "Snap or upload a photo and pin the place you captured it.",
|
||||||
|
placeholder: "Photo caption or image URL",
|
||||||
|
hint: "Add a quick caption so the community knows what they’re seeing.",
|
||||||
|
bodyPlaceholder: "Describe the scene in a few words.",
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
hero: "Type a short update and pin the area you’re reporting from.",
|
||||||
|
placeholder: "Headline or short note",
|
||||||
|
hint: "Readers should understand the gist before opening the full card.",
|
||||||
|
bodyPlaceholder: "Provide more context, opinions, or call to actions.",
|
||||||
|
},
|
||||||
|
other: {
|
||||||
|
hero: "Combine media, notes, or references and pin wherever it matters.",
|
||||||
|
placeholder: "Describe your update",
|
||||||
|
hint: "This mode is flexible—add whatever helps the reader.",
|
||||||
|
bodyPlaceholder: "Add links, attachments, or extra context if needed.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function MapView({
|
export default function MapView({
|
||||||
theme = "dark",
|
theme = "dark",
|
||||||
onPostsState,
|
onPostsState,
|
||||||
|
|
@ -198,7 +284,14 @@ export default function MapView({
|
||||||
const [draftSubCategory, setDraftSubCategory] = useState(
|
const [draftSubCategory, setDraftSubCategory] = useState(
|
||||||
CREATE_CATEGORY_MAP.News[0]
|
CREATE_CATEGORY_MAP.News[0]
|
||||||
);
|
);
|
||||||
|
const [contentMode, setContentMode] = useState(CONTENT_CREATOR_MODES[0].id);
|
||||||
|
const currentContentMode =
|
||||||
|
CONTENT_CREATOR_MODES.find((mode) => mode.id === contentMode) ||
|
||||||
|
CONTENT_CREATOR_MODES[0];
|
||||||
|
const currentFlow = MODE_FLOW_CONFIG[contentMode] || MODE_FLOW_CONFIG.text;
|
||||||
const [draftCoords, setDraftCoords] = useState(null);
|
const [draftCoords, setDraftCoords] = useState(null);
|
||||||
|
const [modeSelectorVisible, setModeSelectorVisible] = useState(false);
|
||||||
|
const [modeChosen, setModeChosen] = useState(false);
|
||||||
const [draftImage, setDraftImage] = useState("");
|
const [draftImage, setDraftImage] = useState("");
|
||||||
const [draftImageSmall, setDraftImageSmall] = useState("");
|
const [draftImageSmall, setDraftImageSmall] = useState("");
|
||||||
const [draftImageMedium, setDraftImageMedium] = useState("");
|
const [draftImageMedium, setDraftImageMedium] = useState("");
|
||||||
|
|
@ -888,6 +981,22 @@ export default function MapView({
|
||||||
return () => document.removeEventListener('click', handleAuthorClick);
|
return () => document.removeEventListener('click', handleAuthorClick);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const closeCreateModal = () => {
|
||||||
|
setIsCreating(false);
|
||||||
|
setModeSelectorVisible(false);
|
||||||
|
setModeChosen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModeSelect = (modeId) => {
|
||||||
|
setContentMode(modeId);
|
||||||
|
setModeSelectorVisible(false);
|
||||||
|
setModeChosen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevStep = () => {
|
||||||
|
setCreateStep((prev) => Math.max(0, prev - 1));
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenCreate = () => {
|
const handleOpenCreate = () => {
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -946,6 +1055,8 @@ export default function MapView({
|
||||||
const list = CREATE_CATEGORY_MAP[main] || CREATE_CATEGORY_MAP.News;
|
const list = CREATE_CATEGORY_MAP[main] || CREATE_CATEGORY_MAP.News;
|
||||||
setDraftSubCategory(list[0]);
|
setDraftSubCategory(list[0]);
|
||||||
setDraftCoords(null);
|
setDraftCoords(null);
|
||||||
|
setModeSelectorVisible(true);
|
||||||
|
setModeChosen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearchSubmit = (e) => {
|
const handleSearchSubmit = (e) => {
|
||||||
|
|
@ -1357,408 +1468,278 @@ export default function MapView({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isCreating && (
|
{isCreating && modeChosen && (
|
||||||
<div className="map-overlay map-crosshair">
|
<div className="map-overlay map-crosshair">
|
||||||
<div className="crosshair-aim" />
|
<div className="crosshair-aim" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCreating && (
|
{modeSelectorVisible && (
|
||||||
<div className="map-overlay create-post-panel">
|
<div className="map-overlay mode-selector-widget">
|
||||||
<div className="create-post-header">
|
<div className="mode-selector-bg" />
|
||||||
<span>Create wire</span>
|
<div className="mode-selector-chips">
|
||||||
<button
|
{CONTENT_CREATOR_MODES.map((mode) => (
|
||||||
className="create-close"
|
<button
|
||||||
onClick={() => setIsCreating(false)}
|
key={mode.id}
|
||||||
>
|
className={
|
||||||
|
"mode-selector-chip" +
|
||||||
|
(mode.id === contentMode ? " is-active" : "")
|
||||||
|
}
|
||||||
|
onClick={() => handleModeSelect(mode.id)}
|
||||||
|
>
|
||||||
|
<i className={mode.icon} />
|
||||||
|
<span>{mode.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isCreating && modeChosen && (
|
||||||
|
<div className="map-overlay create-panel-compact">
|
||||||
|
<div className="create-panel-header">
|
||||||
|
<span>{`Share as ${currentContentMode.label}`}</span>
|
||||||
|
<button className="create-close" onClick={closeCreateModal}>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="create-panel-body">
|
||||||
<div className="create-stepper">
|
<div className="create-step-label">Step {createStep + 1} / 3</div>
|
||||||
{["Pin", "Details", "Share"].map((label, idx) => (
|
<p className="create-panel-hero">{currentFlow.hero}</p>
|
||||||
<div
|
<div className="create-panel-step">
|
||||||
key={label}
|
{createStep === 0 && (
|
||||||
className={
|
<p className="create-panel-note">{currentFlow.hint}</p>
|
||||||
"create-step" +
|
|
||||||
(idx === createStep ? " create-step-current" : "") +
|
|
||||||
(idx <= createStep ? " create-step-active" : "")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="create-step-dot">{idx + 1}</div>
|
|
||||||
<div className="create-step-label">{label}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="create-step-progress">
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
width: `${(createStep / 2) * 100}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{createStep === 0 && (
|
|
||||||
<div className="create-step-body">
|
|
||||||
<span className="crosshair-label">
|
|
||||||
Move the map, aim with the crosshair — your wire will be pinned
|
|
||||||
there.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className="create-row">
|
|
||||||
<select
|
|
||||||
value={draftCategory}
|
|
||||||
onChange={(e) => {
|
|
||||||
const cat = e.target.value;
|
|
||||||
setDraftCategory(cat);
|
|
||||||
setDraftSubCategory(
|
|
||||||
(CREATE_CATEGORY_MAP[cat] || CREATE_CATEGORY_MAP.News)[0]
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{["News", "Friends", "Events", "Market"].map((cat) => (
|
|
||||||
<option key={cat}>{cat}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select
|
|
||||||
value={draftSubCategory}
|
|
||||||
onChange={(e) => setDraftSubCategory(e.target.value)}
|
|
||||||
>
|
|
||||||
{(CREATE_CATEGORY_MAP[draftCategory] ||
|
|
||||||
CREATE_CATEGORY_MAP.News
|
|
||||||
).map((sub) => (
|
|
||||||
<option key={sub}>{sub}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className="create-input"
|
|
||||||
placeholder="Short title or paste a link..."
|
|
||||||
value={draftTitle}
|
|
||||||
onChange={(e) => {
|
|
||||||
const next = e.target.value;
|
|
||||||
if (isLikelyUrl(next)) {
|
|
||||||
setDraftTitle(next);
|
|
||||||
setTitleTouched(false);
|
|
||||||
setDraftUrl(next.trim());
|
|
||||||
setUrlPreviewError("");
|
|
||||||
setDraftSource("");
|
|
||||||
setDraftPublishedAt("");
|
|
||||||
handleUrlPreview(next);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setDraftTitle(next);
|
|
||||||
setTitleTouched(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="create-hint">
|
|
||||||
Type a title or paste a link — we’ll auto-fill from the URL.
|
|
||||||
</div>
|
|
||||||
{draftUrl && !draftSource && !draftPublishedAt && (
|
|
||||||
<div className="create-link-meta">
|
|
||||||
<span>Link detected · fetching preview...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
{(draftSource || draftPublishedAt) && (
|
{createStep === 1 && (
|
||||||
<div className="create-link-meta">
|
<>
|
||||||
{draftSource ? <span>{draftSource}</span> : null}
|
<div className="create-panel-field">
|
||||||
{draftPublishedAt ? (
|
<label>Category</label>
|
||||||
<span>{formatPreviewDate(draftPublishedAt)}</span>
|
<div className="create-panel-selects">
|
||||||
) : null}
|
<select
|
||||||
</div>
|
value={draftCategory}
|
||||||
)}
|
onChange={(e) => {
|
||||||
{urlPreviewError && (
|
const cat = e.target.value;
|
||||||
<div className="create-link-error">{urlPreviewError}</div>
|
setDraftCategory(cat);
|
||||||
)}
|
setDraftSubCategory(
|
||||||
</div>
|
(CREATE_CATEGORY_MAP[cat] || CREATE_CATEGORY_MAP.News)[0]
|
||||||
)}
|
);
|
||||||
|
|
||||||
{createStep === 1 && (
|
|
||||||
<div className="create-step-body">
|
|
||||||
{(draftImagePreview ||
|
|
||||||
draftImageLarge ||
|
|
||||||
draftImageMedium ||
|
|
||||||
draftImageSmall ||
|
|
||||||
draftImage) && (
|
|
||||||
<div className="create-link-preview">
|
|
||||||
<img
|
|
||||||
src={
|
|
||||||
draftImagePreview ||
|
|
||||||
draftImageLarge ||
|
|
||||||
draftImageMedium ||
|
|
||||||
draftImageSmall ||
|
|
||||||
draftImage
|
|
||||||
}
|
|
||||||
alt="Link preview"
|
|
||||||
/>
|
|
||||||
<div className="create-link-preview-meta">
|
|
||||||
{draftSource ? <span>{draftSource}</span> : null}
|
|
||||||
{draftPublishedAt ? (
|
|
||||||
<span>{formatPreviewDate(draftPublishedAt)}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
className="create-textarea"
|
|
||||||
rows={3}
|
|
||||||
placeholder="Context (optional)..."
|
|
||||||
value={draftBody}
|
|
||||||
onChange={(e) => {
|
|
||||||
setDraftBody(e.target.value);
|
|
||||||
setBodyTouched(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="create-image-choice">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={
|
|
||||||
"create-image-choice-btn" +
|
|
||||||
(!useCustomImage ? " is-active" : "")
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setUseCustomImage(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Use category icon
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={
|
|
||||||
"create-image-choice-btn" +
|
|
||||||
(useCustomImage ? " is-active" : "")
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setUseCustomImage(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add an image
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="create-muted">
|
|
||||||
No image? We’ll use the category icon.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!useCustomImage && (
|
|
||||||
<div className="create-image-placeholder">
|
|
||||||
<div className="create-review-icon">
|
|
||||||
<i className={getSubcatIcon(draftCategory, draftSubCategory)} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="create-image-placeholder-title">
|
|
||||||
Category icon
|
|
||||||
</div>
|
|
||||||
<div className="create-image-placeholder-sub">
|
|
||||||
Clean default if you skip images.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{useCustomImage && (
|
|
||||||
<div className="create-image-panel">
|
|
||||||
<div className="create-image-row">
|
|
||||||
<label className="create-file-btn">
|
|
||||||
Choose file
|
|
||||||
<input
|
|
||||||
className="create-file-input"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleImageFileChange}
|
|
||||||
disabled={uploadingImage}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="create-file-btn create-file-btn--camera">
|
|
||||||
Take photo
|
|
||||||
<input
|
|
||||||
className="create-file-input"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
capture="environment"
|
|
||||||
onChange={handleImageFileChange}
|
|
||||||
disabled={uploadingImage}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{uploadingImage && (
|
|
||||||
<span className="create-uploading">Uploading...</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{draftImagePreview && (
|
|
||||||
<div className="create-image-preview-row">
|
|
||||||
<img src={draftImagePreview} alt="Preview" />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setDraftImageFile(null);
|
|
||||||
setDraftImagePreview("");
|
|
||||||
setDraftImage("");
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
{['News', 'Friends', 'Events', 'Market'].map((cat) => (
|
||||||
</button>
|
<option key={cat}>{cat}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
value={draftSubCategory}
|
||||||
|
onChange={(e) => setDraftSubCategory(e.target.value)}
|
||||||
|
>
|
||||||
|
{(CREATE_CATEGORY_MAP[draftCategory] ||
|
||||||
|
CREATE_CATEGORY_MAP.News
|
||||||
|
).map((sub) => (
|
||||||
|
<option key={sub}>{sub}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
<div className="create-panel-field">
|
||||||
{!draftUrl.trim() && !draftImageFile && !draftImagePreview ? (
|
<label>Title or link</label>
|
||||||
<>
|
<input
|
||||||
<div className="create-muted">
|
className="create-input create-input-narrow"
|
||||||
Or paste an image URL:
|
placeholder={currentFlow.placeholder}
|
||||||
</div>
|
value={draftTitle}
|
||||||
<input
|
|
||||||
className="create-input"
|
|
||||||
placeholder="https://example.com/image.jpg"
|
|
||||||
value={draftImage}
|
|
||||||
onChange={(e) => {
|
|
||||||
setDraftImage(e.target.value);
|
|
||||||
setDraftImageFile(null);
|
|
||||||
setDraftImagePreview("");
|
|
||||||
setImageTouched(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="create-privacy-row">
|
|
||||||
<label>Image visibility</label>
|
|
||||||
<select
|
|
||||||
value={imageVisibility}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const next = e.target.value;
|
const next = e.target.value;
|
||||||
setImageVisibility(next);
|
if (isLikelyUrl(next)) {
|
||||||
if (next !== "group") setImageGroupId("");
|
setDraftTitle(next);
|
||||||
if (next !== "custom") setImageUsers("");
|
setTitleTouched(false);
|
||||||
|
setDraftUrl(next.trim());
|
||||||
|
setUrlPreviewError("");
|
||||||
|
setDraftSource("");
|
||||||
|
setDraftPublishedAt("");
|
||||||
|
handleUrlPreview(next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDraftTitle(next);
|
||||||
|
setTitleTouched(true);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<option value="public">Public</option>
|
<div className="create-hint create-hint-tight">
|
||||||
<option value="private">Private</option>
|
{currentFlow.hint}
|
||||||
<option value="group">Group</option>
|
</div>
|
||||||
<option value="custom">Custom</option>
|
{draftUrl && !draftSource && !draftPublishedAt && (
|
||||||
</select>
|
<div className="create-link-meta">
|
||||||
|
<span>Link detected · fetching preview...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(draftSource || draftPublishedAt) && (
|
||||||
|
<div className="create-link-meta">
|
||||||
|
{draftSource ? <span>{draftSource}</span> : null}
|
||||||
|
{draftPublishedAt ? (
|
||||||
|
<span>{formatPreviewDate(draftPublishedAt)}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{urlPreviewError && (
|
||||||
|
<div className="create-link-error">{urlPreviewError}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{imageVisibility === "group" ? (
|
</>
|
||||||
<input
|
|
||||||
className="create-input"
|
|
||||||
placeholder="Group ID"
|
|
||||||
value={imageGroupId}
|
|
||||||
onChange={(e) => setImageGroupId(e.target.value)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{imageVisibility === "custom" ? (
|
|
||||||
<input
|
|
||||||
className="create-input"
|
|
||||||
placeholder="Usernames (comma separated)"
|
|
||||||
value={imageUsers}
|
|
||||||
onChange={(e) => setImageUsers(e.target.value)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
{createStep === 2 && (
|
||||||
)}
|
<>
|
||||||
|
{(draftImagePreview ||
|
||||||
{createStep === 2 && (
|
draftImageLarge ||
|
||||||
<div className="create-step-body">
|
draftImageMedium ||
|
||||||
<div className="create-privacy-row">
|
draftImageSmall ||
|
||||||
<label>Post visibility</label>
|
draftImage) && (
|
||||||
<select
|
<div className="create-link-preview create-link-preview-tight">
|
||||||
value={postVisibility}
|
<img
|
||||||
onChange={(e) => {
|
src={
|
||||||
const next = e.target.value;
|
draftImagePreview ||
|
||||||
setPostVisibility(next);
|
draftImageLarge ||
|
||||||
if (next !== "group") setPostGroupId("");
|
draftImageMedium ||
|
||||||
if (next !== "custom") setPostUsers("");
|
draftImageSmall ||
|
||||||
}}
|
draftImage
|
||||||
>
|
}
|
||||||
<option value="public">Public</option>
|
alt="Link preview"
|
||||||
<option value="private">Private</option>
|
/>
|
||||||
<option value="group">Group</option>
|
<div className="create-link-preview-meta">
|
||||||
<option value="custom">Custom</option>
|
{draftSource ? <span>{draftSource}</span> : null}
|
||||||
</select>
|
{draftPublishedAt ? (
|
||||||
</div>
|
<span>{formatPreviewDate(draftPublishedAt)}</span>
|
||||||
{postVisibility === "group" ? (
|
) : null}
|
||||||
<input
|
</div>
|
||||||
className="create-input"
|
|
||||||
placeholder="Group ID"
|
|
||||||
value={postGroupId}
|
|
||||||
onChange={(e) => setPostGroupId(e.target.value)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{postVisibility === "custom" ? (
|
|
||||||
<input
|
|
||||||
className="create-input"
|
|
||||||
placeholder="Usernames (comma separated)"
|
|
||||||
value={postUsers}
|
|
||||||
onChange={(e) => setPostUsers(e.target.value)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="create-review-card">
|
|
||||||
<div className="create-review-media">
|
|
||||||
{useCustomImage && (draftImagePreview || draftImage) ? (
|
|
||||||
<img src={draftImagePreview || draftImage} alt="Preview" />
|
|
||||||
) : (
|
|
||||||
<div className="create-review-icon">
|
|
||||||
<i className={getSubcatIcon(draftCategory, draftSubCategory)} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
<textarea
|
||||||
<div className="create-review-meta">
|
className="create-textarea create-textarea-tight"
|
||||||
<div className="create-review-title">
|
rows={3}
|
||||||
{draftTitle || "Untitled wire"}
|
placeholder={currentFlow.bodyPlaceholder}
|
||||||
|
value={draftBody}
|
||||||
|
onChange={(e) => {
|
||||||
|
setDraftBody(e.target.value);
|
||||||
|
setBodyTouched(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="create-image-choice">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={
|
||||||
|
"create-image-choice-btn" + (!useCustomImage ? " is-active" : "")
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setUseCustomImage(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Use category icon
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={
|
||||||
|
"create-image-choice-btn" + (useCustomImage ? " is-active" : "")
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setUseCustomImage(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add an image
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="create-review-sub">
|
<div className="create-muted">
|
||||||
{draftCategory} · {draftSubCategory}
|
We’ll use the category icon if you skip images.
|
||||||
</div>
|
</div>
|
||||||
<div className="create-review-body">
|
{useCustomImage && (
|
||||||
{draftBody || "Add a quick line of context if you want."}
|
<div className="create-image-panel">
|
||||||
</div>
|
<div className="create-image-row">
|
||||||
<div className="create-review-visibility">
|
<label className="create-file-btn">
|
||||||
Visibility: {postVisibility}
|
Choose file
|
||||||
</div>
|
<input
|
||||||
</div>
|
className="create-file-input"
|
||||||
</div>
|
type="file"
|
||||||
</div>
|
accept="image/*"
|
||||||
)}
|
onChange={handleImageFileChange}
|
||||||
|
disabled={uploadingImage}
|
||||||
{saveError && <div className="create-error">{saveError}</div>}
|
/>
|
||||||
|
</label>
|
||||||
<div className="create-actions">
|
<label className="create-file-btn create-file-btn--camera">
|
||||||
<button
|
Take photo
|
||||||
type="button"
|
<input
|
||||||
className="create-back"
|
className="create-file-input"
|
||||||
onClick={() => setCreateStep((s) => Math.max(0, s - 1))}
|
type="file"
|
||||||
disabled={createStep === 0}
|
accept="image/*"
|
||||||
>
|
capture="environment"
|
||||||
Back
|
onChange={handleImageFileChange}
|
||||||
</button>
|
disabled={uploadingImage}
|
||||||
<div className="create-actions-right">
|
/>
|
||||||
{createStep < 2 ? (
|
</label>
|
||||||
<button
|
{uploadingImage && (
|
||||||
type="button"
|
<span className="create-uploading">Uploading...</span>
|
||||||
className="chip-pill"
|
)}
|
||||||
onClick={() => setCreateStep((s) => Math.min(2, s + 1))}
|
</div>
|
||||||
disabled={!draftTitle.trim() || uploadingImage}
|
{draftImagePreview && (
|
||||||
>
|
<div className="create-image-preview-row">
|
||||||
Next
|
<img src={draftImagePreview} alt="Preview" />
|
||||||
</button>
|
<button
|
||||||
) : (
|
type="button"
|
||||||
<button
|
onClick={() => {
|
||||||
className="chip-pill"
|
setDraftImageFile(null);
|
||||||
onClick={handleSubmitPost}
|
setDraftImagePreview("");
|
||||||
disabled={isSaving}
|
setDraftImage("");
|
||||||
>
|
}}
|
||||||
{isSaving ? "Posting..." : "Post"}
|
>
|
||||||
</button>
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!draftUrl.trim() && !draftImageFile && !draftImagePreview ? (
|
||||||
|
<>
|
||||||
|
<div className="create-muted">Or paste an image URL:</div>
|
||||||
|
<input
|
||||||
|
className="create-input"
|
||||||
|
placeholder="https://example.com/image.jpg"
|
||||||
|
value={draftImage}
|
||||||
|
onChange={(e) => {
|
||||||
|
setDraftImage(e.target.value);
|
||||||
|
setDraftImageFile(null);
|
||||||
|
setDraftImagePreview("");
|
||||||
|
setImageTouched(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="create-panel-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="chip-pill create-panel-btn"
|
||||||
|
onClick={handlePrevStep}
|
||||||
|
disabled={createStep === 0}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="chip-pill create-panel-btn primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (createStep < 2) {
|
||||||
|
setCreateStep((prev) => prev + 1);
|
||||||
|
} else {
|
||||||
|
handleSubmitPost();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
{createStep === 2 ? (isSaving ? "Posting..." : "Post") : "Next"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User Profile Modal */}
|
{/* User Profile Modal */}
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,8 @@ async function getAvatarForUser(name) {
|
||||||
export default function PostCard({ post, selected, onSelect }) {
|
export default function PostCard({ post, selected, onSelect }) {
|
||||||
const [liked, setLiked] = useState(false);
|
const [liked, setLiked] = useState(false);
|
||||||
const [authed, setAuthed] = useState(isAuthed());
|
const [authed, setAuthed] = useState(isAuthed());
|
||||||
|
const [activeImageIndex, setActiveImageIndex] = useState(0);
|
||||||
|
const [failedImageIndices, setFailedImageIndices] = useState(() => new Set());
|
||||||
const [imgFailed, setImgFailed] = useState(false);
|
const [imgFailed, setImgFailed] = useState(false);
|
||||||
const [showPrivacy, setShowPrivacy] = useState(false);
|
const [showPrivacy, setShowPrivacy] = useState(false);
|
||||||
const [privacySaving, setPrivacySaving] = useState(false);
|
const [privacySaving, setPrivacySaving] = useState(false);
|
||||||
|
|
@ -187,9 +189,31 @@ export default function PostCard({ post, selected, onSelect }) {
|
||||||
setPostGroupId((post?.group_id || "").toString());
|
setPostGroupId((post?.group_id || "").toString());
|
||||||
}, [post?.visibility, post?.group_id]);
|
}, [post?.visibility, post?.group_id]);
|
||||||
|
|
||||||
const rawImg =
|
const mediaUrls = useMemo(() => {
|
||||||
post?.image_medium || post?.image_large || post?.image_small || post?.image || post?.media_url || "";
|
const list = [];
|
||||||
const img = normalizeImageUrl(rawImg);
|
const seen = new Set();
|
||||||
|
const addUrl = (value) => {
|
||||||
|
const normalized = normalizeImageUrl(value);
|
||||||
|
if (!normalized || seen.has(normalized)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seen.add(normalized);
|
||||||
|
list.push(normalized);
|
||||||
|
};
|
||||||
|
if (Array.isArray(post?.media_urls)) {
|
||||||
|
post.media_urls.forEach(addUrl);
|
||||||
|
}
|
||||||
|
[post?.image_large, post?.image_medium, post?.image_small, post?.image, post?.media_url].forEach(addUrl);
|
||||||
|
return list;
|
||||||
|
}, [post?.media_urls, post?.image_large, post?.image_medium, post?.image_small, post?.image, post?.media_url]);
|
||||||
|
const mediaKey = mediaUrls.join("|");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveImageIndex(0);
|
||||||
|
setFailedImageIndices(new Set());
|
||||||
|
setImgFailed(false);
|
||||||
|
}, [mediaKey]);
|
||||||
|
|
||||||
const title = post?.title || "Untitled";
|
const title = post?.title || "Untitled";
|
||||||
const snippet = post?.snippet || "";
|
const snippet = post?.snippet || "";
|
||||||
const author = (post?.author || post?.Author || "anon").toString();
|
const author = (post?.author || post?.Author || "anon").toString();
|
||||||
|
|
@ -206,6 +230,41 @@ export default function PostCard({ post, selected, onSelect }) {
|
||||||
return parts.join(" • ");
|
return parts.join(" • ");
|
||||||
}, [sub, post?.region]);
|
}, [sub, post?.region]);
|
||||||
|
|
||||||
|
const safeImageIndex = mediaUrls.length
|
||||||
|
? Math.min(Math.max(activeImageIndex, 0), mediaUrls.length - 1)
|
||||||
|
: 0;
|
||||||
|
const activeImage = mediaUrls[safeImageIndex] || "";
|
||||||
|
const hasGalleryImage = Boolean(activeImage && !imgFailed);
|
||||||
|
|
||||||
|
const findNextAvailableIndex = (current, failed) => {
|
||||||
|
if (mediaUrls.length === 0) return -1;
|
||||||
|
for (let offset = 1; offset <= mediaUrls.length; offset += 1) {
|
||||||
|
const idx = (current + offset) % mediaUrls.length;
|
||||||
|
if (!failed.has(idx)) {
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageError = () => {
|
||||||
|
if (!activeImage) {
|
||||||
|
setImgFailed(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextFailed = new Set(failedImageIndices);
|
||||||
|
nextFailed.add(safeImageIndex);
|
||||||
|
setFailedImageIndices(nextFailed);
|
||||||
|
if (nextFailed.size >= mediaUrls.length) {
|
||||||
|
setImgFailed(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextIndex = findNextAvailableIndex(safeImageIndex, nextFailed);
|
||||||
|
if (nextIndex >= 0) {
|
||||||
|
setActiveImageIndex(nextIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const who = (author || "").trim();
|
const who = (author || "").trim();
|
||||||
|
|
@ -241,14 +300,35 @@ export default function PostCard({ post, selected, onSelect }) {
|
||||||
<div className="sw-wall-badge">{cat}</div>
|
<div className="sw-wall-badge">{cat}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{img && !imgFailed ? (
|
{hasGalleryImage ? (
|
||||||
<div className="sw-wall-image">
|
<div className="sw-wall-gallery">
|
||||||
<img
|
<div className="sw-wall-image">
|
||||||
src={img}
|
<img
|
||||||
alt=""
|
src={activeImage}
|
||||||
loading="lazy"
|
alt=""
|
||||||
onError={() => setImgFailed(true)}
|
loading="lazy"
|
||||||
/>
|
onError={handleImageError}
|
||||||
|
/>
|
||||||
|
{mediaUrls.length > 1 && (
|
||||||
|
<div className="sw-wall-gallery-count">
|
||||||
|
{safeImageIndex + 1}/{mediaUrls.length}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{mediaUrls.length > 1 && (
|
||||||
|
<div className="sw-wall-gallery-thumbs">
|
||||||
|
{mediaUrls.map((value, idx) => (
|
||||||
|
<button
|
||||||
|
key={`thumb-${value}-${idx}`}
|
||||||
|
type="button"
|
||||||
|
className={`sw-wall-gallery-thumb${idx === safeImageIndex ? " active" : ""}`}
|
||||||
|
onClick={() => setActiveImageIndex(idx)}
|
||||||
|
>
|
||||||
|
<img src={value} alt="" loading="lazy" />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="sw-wall-image sw-wall-image-fallback" aria-hidden="true">
|
<div className="sw-wall-image sw-wall-image-fallback" aria-hidden="true">
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,9 @@ export default function SmartSearchBar({
|
||||||
const skipSearchRef = useRef(false);
|
const skipSearchRef = useRef(false);
|
||||||
const boxRef = useRef(null);
|
const boxRef = useRef(null);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
const searchCacheRef = useRef(new Map());
|
||||||
|
const latestQueryRef = useRef("");
|
||||||
|
const emptySuggestionsFetchedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onDocDown(e) {
|
function onDocDown(e) {
|
||||||
|
|
@ -95,23 +98,32 @@ export default function SmartSearchBar({
|
||||||
const triggerSearch = async (searchQuery) => {
|
const triggerSearch = async (searchQuery) => {
|
||||||
if (abortRef.current) abortRef.current.abort();
|
if (abortRef.current) abortRef.current.abort();
|
||||||
|
|
||||||
|
const cached = searchCacheRef.current.get(searchQuery);
|
||||||
|
if (cached) {
|
||||||
|
setItems(cached);
|
||||||
|
setActive(cached.length ? 0 : -1);
|
||||||
|
setOpen(true);
|
||||||
|
setHasSearched(true);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
abortRef.current = ctrl;
|
abortRef.current = ctrl;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setErr("");
|
setErr("");
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
|
latestQueryRef.current = searchQuery;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[SmartSearchBar] Searching for:', searchQuery);
|
|
||||||
const res = await recoSearch(searchQuery, { signal: ctrl.signal, limit: 12 });
|
const res = await recoSearch(searchQuery, { signal: ctrl.signal, limit: 12 });
|
||||||
console.log('[SmartSearchBar] Got results:', res?.length || 0, res);
|
if (latestQueryRef.current !== searchQuery) return;
|
||||||
setItems(res);
|
setItems(res);
|
||||||
setOpen(true);
|
|
||||||
setActive(res.length ? 0 : -1);
|
setActive(res.length ? 0 : -1);
|
||||||
setHasSearched(true);
|
setHasSearched(true);
|
||||||
|
searchCacheRef.current.set(searchQuery, res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (ctrl.signal.aborted) return;
|
if (ctrl.signal.aborted) return;
|
||||||
console.error('[SmartSearchBar] Search error:', e);
|
|
||||||
setErr((e && e.message) ? e.message : "Search error");
|
setErr((e && e.message) ? e.message : "Search error");
|
||||||
setItems([]);
|
setItems([]);
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
|
|
@ -132,9 +144,16 @@ export default function SmartSearchBar({
|
||||||
|
|
||||||
if (abortRef.current) abortRef.current.abort();
|
if (abortRef.current) abortRef.current.abort();
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
setItems([]);
|
||||||
|
setOpen(false);
|
||||||
|
setHasSearched(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const t = setTimeout(() => {
|
const t = setTimeout(() => {
|
||||||
triggerSearch(query);
|
triggerSearch(query);
|
||||||
}, 150); // Debounce 150ms
|
}, 200); // Debounce 200ms (faster UX)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(t);
|
clearTimeout(t);
|
||||||
|
|
@ -228,11 +247,11 @@ export default function SmartSearchBar({
|
||||||
onSearch && onSearch("");
|
onSearch && onSearch("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setOpen(true);
|
if (!q.trim() && !emptySuggestionsFetchedRef.current) {
|
||||||
// Si pas encore cherché et query vide, charger suggestions populaires
|
emptySuggestionsFetchedRef.current = true;
|
||||||
if (!hasSearched && !q.trim()) {
|
|
||||||
triggerSearch("");
|
triggerSearch("");
|
||||||
}
|
}
|
||||||
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
setTimeout(() => setFocused(false), 120);
|
setTimeout(() => setFocused(false), 120);
|
||||||
|
|
@ -276,7 +295,7 @@ export default function SmartSearchBar({
|
||||||
{loading && <div className="sw-search__row sw-search__muted">Searching…</div>}
|
{loading && <div className="sw-search__row sw-search__muted">Searching…</div>}
|
||||||
{err && <div className="sw-search__row sw-search__error">{err}</div>}
|
{err && <div className="sw-search__row sw-search__error">{err}</div>}
|
||||||
|
|
||||||
{!loading && !err && items.map((it, idx) => (
|
{!err && items.map((it, idx) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={it.id}
|
key={it.id}
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ function apiPrefix() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PATHS = [
|
const DEFAULT_PATHS = [
|
||||||
"/suggestions", // NEW: Autocomplete suggestions (cities, posts, profiles)
|
"/suggestions", // FAST: Meilisearch autocomplete (cities, posts, profiles) - PRIMARY
|
||||||
"/unified-search", // Unified search (posts + profiles + places)
|
"/unified-search", // Fallback: Unified search (posts + profiles + places)
|
||||||
"/search", // Fallback: Posts only
|
"/search", // Fallback: Posts only
|
||||||
"/reco/search",
|
"/reco/search",
|
||||||
"/reco/suggest",
|
"/reco/suggest",
|
||||||
|
|
@ -77,7 +77,22 @@ function normalizeOne(r) {
|
||||||
: (lat != null && lon != null ? [lon, lat] : null);
|
: (lat != null && lon != null ? [lon, lat] : null);
|
||||||
|
|
||||||
const type = (r.type ?? r.kind ?? r.entity ?? "").toString().toLowerCase().trim();
|
const type = (r.type ?? r.kind ?? r.entity ?? "").toString().toLowerCase().trim();
|
||||||
const kind = type || (coords ? "place" : "item");
|
const hasPostSignal =
|
||||||
|
!!(r.category ||
|
||||||
|
r.sub_category ||
|
||||||
|
r.subCategory ||
|
||||||
|
r.author ||
|
||||||
|
r.snippet ||
|
||||||
|
r.body ||
|
||||||
|
r.content ||
|
||||||
|
r.url ||
|
||||||
|
r.cluster_key ||
|
||||||
|
r.clusterKey ||
|
||||||
|
r.template ||
|
||||||
|
r.source);
|
||||||
|
const kind =
|
||||||
|
type ||
|
||||||
|
(coords ? "place" : hasPostSignal ? "post" : "item");
|
||||||
const tags =
|
const tags =
|
||||||
Array.isArray(r.event_tags)
|
Array.isArray(r.event_tags)
|
||||||
? r.event_tags
|
? r.event_tags
|
||||||
|
|
@ -103,9 +118,10 @@ function normalizeOne(r) {
|
||||||
* Tente une recherche sur une liste de paths (au cas où le endpoint exact diffère).
|
* Tente une recherche sur une liste de paths (au cas où le endpoint exact diffère).
|
||||||
* Retourne [] sur 404/erreur.
|
* Retourne [] sur 404/erreur.
|
||||||
*/
|
*/
|
||||||
async function tryFetchAnyPath(q, { signal } = {}) {
|
async function tryFetchAnyPath(q, { signal, limit } = {}) {
|
||||||
const query = (q || "").toString().trim();
|
const query = (q || "").toString().trim();
|
||||||
if (!query) return [];
|
const isEmptyQuery = query === "";
|
||||||
|
const maxResults = limit && limit > 0 ? limit : 12;
|
||||||
const cached = cacheGet(query);
|
const cached = cacheGet(query);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
|
|
@ -117,23 +133,28 @@ async function tryFetchAnyPath(q, { signal } = {}) {
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const prefix = apiPrefix();
|
const prefix = apiPrefix();
|
||||||
const paths = override ? [override, ...DEFAULT_PATHS] : DEFAULT_PATHS;
|
const basePaths = isEmptyQuery ? ["/suggestions"] : DEFAULT_PATHS;
|
||||||
|
const paths = override ? [override, ...basePaths] : basePaths;
|
||||||
|
const posts = [];
|
||||||
|
const locations = [];
|
||||||
|
const profiles = [];
|
||||||
|
const seen = new Set();
|
||||||
|
const minLocationResults = Math.min(2, maxResults);
|
||||||
|
const locationKinds = new Set([
|
||||||
|
"place", "city", "region", "province", "state",
|
||||||
|
"country", "area", "district", "poi", "venue", "location",
|
||||||
|
]);
|
||||||
for (const p of paths) {
|
for (const p of paths) {
|
||||||
try {
|
try {
|
||||||
const base = p.startsWith("http") ? p : `${prefix}${p}`;
|
const base = p.startsWith("http") ? p : `${prefix}${p}`;
|
||||||
const url = `${base}?q=${encodeURIComponent(query)}`;
|
const url = `${base}?q=${encodeURIComponent(query)}`;
|
||||||
console.log('[recoSearch] Trying:', url);
|
|
||||||
const res = await fetch(url, { credentials: "include", signal });
|
const res = await fetch(url, { credentials: "include", signal });
|
||||||
|
|
||||||
console.log('[recoSearch] Response status:', res.status);
|
|
||||||
if (res.status === 404) {
|
if (res.status === 404) {
|
||||||
console.log('[recoSearch] 404, trying next path');
|
continue; // Try next path
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.log('[recoSearch] Not OK, returning empty');
|
return []; // Avoid spam errors
|
||||||
return []; // évite spam erreurs
|
|
||||||
}
|
}
|
||||||
const data = await res.json().catch(() => null);
|
const data = await res.json().catch(() => null);
|
||||||
|
|
||||||
|
|
@ -145,23 +166,56 @@ async function tryFetchAnyPath(q, { signal } = {}) {
|
||||||
? data.results
|
? data.results
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
console.log('[recoSearch] Success! Returning', arr.length, 'results from', p);
|
if (arr.length === 0) {
|
||||||
const out = arr.map(normalizeOne).filter(Boolean);
|
continue; // Try next path
|
||||||
cacheSet(query, out, 30 * 1000);
|
}
|
||||||
return out;
|
|
||||||
|
const normalized = arr.map(normalizeOne).filter(Boolean);
|
||||||
|
if (normalized.length === 0) {
|
||||||
|
continue; // Try next path
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const it of normalized) {
|
||||||
|
if (seen.has(it.id)) continue;
|
||||||
|
seen.add(it.id);
|
||||||
|
const kind = (it.kind || "").toLowerCase();
|
||||||
|
if (locationKinds.has(kind)) {
|
||||||
|
locations.push(it);
|
||||||
|
} else if (kind === "profile") {
|
||||||
|
profiles.push(it);
|
||||||
|
} else {
|
||||||
|
posts.push(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalCollected = posts.length + locations.length + profiles.length;
|
||||||
|
if (totalCollected >= maxResults && locations.length >= minLocationResults) {
|
||||||
|
break; // Collected enough results
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Abort => stop net
|
// Abort => stop network calls
|
||||||
if (e && (e.name === "AbortError" || e.code === 20)) {
|
if (e && (e.name === "AbortError" || e.code === 20)) {
|
||||||
console.log('[recoSearch] Aborted');
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
// autres erreurs => on teste prochain path
|
// Other errors => try next path
|
||||||
console.log('[recoSearch] Error on', p, ':', e.message, '- trying next');
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
const totalCollected = posts.length + locations.length + profiles.length;
|
||||||
|
if (totalCollected === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const locationSlots = Math.min(minLocationResults, locations.length);
|
||||||
|
const initialResults = [
|
||||||
|
...locations.slice(0, locationSlots),
|
||||||
|
...posts,
|
||||||
|
...locations.slice(locationSlots),
|
||||||
|
...profiles,
|
||||||
|
];
|
||||||
|
const resultSlice = initialResults.slice(0, maxResults);
|
||||||
|
cacheSet(query, resultSlice, 30 * 1000);
|
||||||
|
return resultSlice;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@
|
||||||
z-index: 500; /* Au-dessus des filtres (z-index: 300) */
|
z-index: 500; /* Au-dessus des filtres (z-index: 300) */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Left / right: filtres */
|
/* Left / right: filtres */
|
||||||
.map-overlay-left{
|
.map-overlay-left{
|
||||||
left:.7rem;
|
left:.7rem;
|
||||||
|
|
@ -142,20 +143,120 @@
|
||||||
height:70px;
|
height:70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Panneau de création */
|
.create-panel-compact{
|
||||||
.create-post-panel{
|
position:absolute;
|
||||||
left:50%;
|
left:50%;
|
||||||
bottom:8%;
|
top:45%;
|
||||||
transform:translateX(-50%);
|
transform:translate(-50%,-45%);
|
||||||
width:92%;
|
width:300px;
|
||||||
max-width:560px;
|
max-height:74vh;
|
||||||
max-height:62vh;
|
overflow:auto;
|
||||||
overflow-y:auto;
|
background: var(--sw-panel-bg, rgba(7, 10, 14, 0.78));
|
||||||
background: rgba(15, 23, 42, 0.96);
|
backdrop-filter: blur(18px);
|
||||||
border-radius:16px;
|
border-radius:18px;
|
||||||
border:1px solid rgba(148, 163, 184, 0.9);
|
border:1px solid var(--sw-panel-border, rgba(56,189,248,0.5));
|
||||||
box-shadow: 0 16px 40px rgba(0,0,0,.9), 0 0 24px rgba(56,189,248,.6);
|
box-shadow: 0 20px 40px rgba(0,0,0,0.65), 0 0 30px rgba(56,189,248,0.25);
|
||||||
padding: .6rem .7rem .7rem;
|
padding: .4rem .5rem .55rem;
|
||||||
|
color: var(--sw-text, #e5e7eb);
|
||||||
|
}
|
||||||
|
.create-panel-header{
|
||||||
|
display:flex;
|
||||||
|
justify-content:space-between;
|
||||||
|
align-items:center;
|
||||||
|
font-size:.8rem;
|
||||||
|
letter-spacing:.08em;
|
||||||
|
text-transform:uppercase;
|
||||||
|
color:var(--sw-text,#e5e7eb);
|
||||||
|
margin-bottom:.25rem;
|
||||||
|
}
|
||||||
|
.create-panel-body{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:.4rem;
|
||||||
|
}
|
||||||
|
.create-step-label{
|
||||||
|
font-size:.65rem;
|
||||||
|
text-transform:uppercase;
|
||||||
|
letter-spacing:.2em;
|
||||||
|
color:var(--sw-muted,rgba(255,255,255,0.6));
|
||||||
|
}
|
||||||
|
.create-panel-hero{
|
||||||
|
font-size:.85rem;
|
||||||
|
color:var(--sw-text,#f8fafc);
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
.create-panel-step{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:.35rem;
|
||||||
|
}
|
||||||
|
.create-panel-note{
|
||||||
|
font-size:.75rem;
|
||||||
|
color:var(--sw-muted,rgba(255,255,255,0.6));
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
.create-panel-field{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:.25rem;
|
||||||
|
font-size:.7rem;
|
||||||
|
color:var(--sw-text,#e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-selects select,
|
||||||
|
.create-input-narrow{
|
||||||
|
background:rgba(255,255,255,0.04);
|
||||||
|
color:var(--sw-text,#e5e7eb);
|
||||||
|
border-radius:.65rem;
|
||||||
|
border:1px solid var(--sw-panel-border,rgba(255,255,255,0.28));
|
||||||
|
padding:.35rem .55rem;
|
||||||
|
}
|
||||||
|
.create-panel-selects select{
|
||||||
|
min-height:36px;
|
||||||
|
}
|
||||||
|
.create-input-narrow::placeholder{
|
||||||
|
color:var(--sw-muted,rgba(255,255,255,0.6));
|
||||||
|
}
|
||||||
|
.create-textarea-tight{
|
||||||
|
background:rgba(255,255,255,0.03);
|
||||||
|
border:1px solid var(--sw-panel-border,rgba(255,255,255,0.28));
|
||||||
|
color:var(--sw-text,#e5e7eb);
|
||||||
|
border-radius:.8rem;
|
||||||
|
padding:.35rem .6rem;
|
||||||
|
font-size:.75rem;
|
||||||
|
min-height:100px;
|
||||||
|
resize:none;
|
||||||
|
}
|
||||||
|
.create-panel-actions{
|
||||||
|
display:flex;
|
||||||
|
justify-content:space-between;
|
||||||
|
gap:.5rem;
|
||||||
|
margin-top:.2rem;
|
||||||
|
}
|
||||||
|
.create-panel-btn{
|
||||||
|
flex:1;
|
||||||
|
padding:.4rem .6rem;
|
||||||
|
font-size:.7rem;
|
||||||
|
border-radius:999px;
|
||||||
|
border:1px solid var(--sw-panel-border,rgba(226,232,240,0.4));
|
||||||
|
background:var(--sw-chip-bg,rgba(15,23,42,0.75));
|
||||||
|
color:var(--sw-text,#e5e7eb);
|
||||||
|
}
|
||||||
|
.create-panel-btn.primary{
|
||||||
|
background:linear-gradient(120deg,#38bdf8,#22c55e);
|
||||||
|
border-color:transparent;
|
||||||
|
color:#0f172a;
|
||||||
|
}
|
||||||
|
.create-panel-hero,
|
||||||
|
.create-panel-note,
|
||||||
|
.create-hint-tight{
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
.create-link-preview-tight{
|
||||||
|
border-radius:12px;
|
||||||
|
padding:.25rem;
|
||||||
|
background:var(--sw-panel-bg-2,rgba(15,23,42,0.8));
|
||||||
|
border:1px solid var(--sw-panel-border,rgba(148,163,184,0.4));
|
||||||
}
|
}
|
||||||
.create-post-header{
|
.create-post-header{
|
||||||
display:flex;
|
display:flex;
|
||||||
|
|
@ -165,6 +266,255 @@
|
||||||
margin-bottom:.4rem;
|
margin-bottom:.4rem;
|
||||||
color:#e5e7eb;
|
color:#e5e7eb;
|
||||||
}
|
}
|
||||||
|
.create-mode-bar{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:.35rem;
|
||||||
|
margin-bottom:.4rem;
|
||||||
|
}
|
||||||
|
.create-mode-bar-label{
|
||||||
|
font-size:.66rem;
|
||||||
|
text-transform:uppercase;
|
||||||
|
letter-spacing:.18em;
|
||||||
|
color:#94a3b8;
|
||||||
|
}
|
||||||
|
.create-mode-chips{
|
||||||
|
display:flex;
|
||||||
|
gap:.35rem;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
}
|
||||||
|
.create-mode-chip{
|
||||||
|
flex:1 1 110px;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:.4rem;
|
||||||
|
padding:.35rem .65rem;
|
||||||
|
border-radius:999px;
|
||||||
|
border:1px solid rgba(148, 163, 184, 0.4);
|
||||||
|
background:rgba(15, 23, 42, 0.65);
|
||||||
|
color:#e5e7eb;
|
||||||
|
font-size:.7rem;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:border-color .2s ease, box-shadow .2s ease;
|
||||||
|
}
|
||||||
|
.create-mode-chip.is-active{
|
||||||
|
border-color:transparent;
|
||||||
|
background:linear-gradient(120deg,#38bdf8,#22c55e);
|
||||||
|
color:#0f172a;
|
||||||
|
box-shadow:0 10px 20px rgba(56,189,248,.25);
|
||||||
|
}
|
||||||
|
.create-mode-hint{
|
||||||
|
font-size:.72rem;
|
||||||
|
color:#cbd5f5;
|
||||||
|
line-height:1.3;
|
||||||
|
min-height:1.1rem;
|
||||||
|
}
|
||||||
|
.mode-change-btn{
|
||||||
|
margin-top:.5rem;
|
||||||
|
align-self:flex-start;
|
||||||
|
}
|
||||||
|
.mode-selector-widget{
|
||||||
|
position:absolute;
|
||||||
|
left:50%;
|
||||||
|
top:18%;
|
||||||
|
transform:translate(-50%, -5%);
|
||||||
|
pointer-events:auto;
|
||||||
|
}
|
||||||
|
.mode-selector-bg{
|
||||||
|
position:absolute;
|
||||||
|
inset:0;
|
||||||
|
border-radius:18px;
|
||||||
|
background:linear-gradient(135deg,var(--sw-panel-bg-2,rgba(15,23,42,0.95)),var(--sw-panel-bg,rgba(15,23,42,0.8)));
|
||||||
|
border:1px solid var(--sw-panel-border,rgba(56,189,248,0.4));
|
||||||
|
box-shadow:0 20px 30px rgba(0,0,0,0.45);
|
||||||
|
}
|
||||||
|
.mode-selector-chips{
|
||||||
|
position:relative;
|
||||||
|
display:flex;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
gap:.5rem;
|
||||||
|
padding:.5rem;
|
||||||
|
z-index:1;
|
||||||
|
}
|
||||||
|
.mode-selector-chip{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:.35rem;
|
||||||
|
padding:.5rem .8rem;
|
||||||
|
border-radius:999px;
|
||||||
|
border:1px solid var(--sw-panel-border,rgba(255,255,255,0.15));
|
||||||
|
background:var(--sw-panel-bg,rgba(15,23,42,0.9));
|
||||||
|
color:var(--sw-text,#e5e7eb);
|
||||||
|
font-size:.7rem;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:transform .15s ease, border-color .15s ease;
|
||||||
|
}
|
||||||
|
.mode-selector-chip.is-active{
|
||||||
|
border-color:rgba(56,189,248,0.8);
|
||||||
|
background:rgba(56,189,248,0.25);
|
||||||
|
}
|
||||||
|
.mode-selector-chip i{
|
||||||
|
font-size:1rem;
|
||||||
|
}
|
||||||
|
.create-mode-details{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:.2rem;
|
||||||
|
font-size:.68rem;
|
||||||
|
color:#9ca3af;
|
||||||
|
}
|
||||||
|
.create-stepper{
|
||||||
|
background:rgba(255,255,255,0.02);
|
||||||
|
border-radius:999px;
|
||||||
|
padding:.2rem .3rem;
|
||||||
|
margin-bottom:.35rem;
|
||||||
|
}
|
||||||
|
.create-step{
|
||||||
|
font-size:.6rem;
|
||||||
|
}
|
||||||
|
.create-mode-detail-line{
|
||||||
|
display:flex;
|
||||||
|
gap:.35rem;
|
||||||
|
}
|
||||||
|
.create-mode-detail-line::before{
|
||||||
|
content:"•";
|
||||||
|
font-size:.9rem;
|
||||||
|
line-height:1.8;
|
||||||
|
color:#38bdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px){
|
||||||
|
.create-mode-chips{
|
||||||
|
flex-wrap:nowrap;
|
||||||
|
overflow-x:auto;
|
||||||
|
}
|
||||||
|
.create-mode-chip{
|
||||||
|
flex:0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.create-mode-panel{
|
||||||
|
margin-bottom:.4rem;
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:.35rem;
|
||||||
|
}
|
||||||
|
.create-mode-summary{
|
||||||
|
display:none;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:space-between;
|
||||||
|
gap:.35rem;
|
||||||
|
border-radius:999px;
|
||||||
|
border:1px solid rgba(148,163,184,0.4);
|
||||||
|
padding:.35rem .6rem;
|
||||||
|
background:rgba(15,23,42,0.5);
|
||||||
|
font-size:.72rem;
|
||||||
|
color:#e5e7eb;
|
||||||
|
margin-bottom:.4rem;
|
||||||
|
}
|
||||||
|
.create-mode-summary-label{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:.3rem;
|
||||||
|
}
|
||||||
|
.create-mode-summary-btn{
|
||||||
|
border-radius:999px;
|
||||||
|
border:1px solid rgba(148,163,184,0.6);
|
||||||
|
background:transparent;
|
||||||
|
color:#e5e7eb;
|
||||||
|
padding:.2rem .6rem;
|
||||||
|
font-size:.65rem;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
.create-mode-title{
|
||||||
|
font-size:.68rem;
|
||||||
|
text-transform:uppercase;
|
||||||
|
letter-spacing:.18em;
|
||||||
|
color:#94a3b8;
|
||||||
|
}
|
||||||
|
.create-mode-chips{
|
||||||
|
display:flex;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
gap:.4rem;
|
||||||
|
}
|
||||||
|
.create-mode-chip{
|
||||||
|
flex:1 1 110px;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:.35rem;
|
||||||
|
padding:.35rem .6rem;
|
||||||
|
border-radius:999px;
|
||||||
|
border:1px solid rgba(148, 163, 184, 0.5);
|
||||||
|
background:rgba(15,23,42,0.65);
|
||||||
|
color:#e5e7eb;
|
||||||
|
font-size:.7rem;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:border-color .2s ease, box-shadow .2s ease;
|
||||||
|
}
|
||||||
|
.create-mode-chip:hover{
|
||||||
|
border-color:rgba(56,189,248,0.8);
|
||||||
|
}
|
||||||
|
.create-mode-chip.is-active{
|
||||||
|
border-color:transparent;
|
||||||
|
background:linear-gradient(120deg,#38bdf8,#22c55e);
|
||||||
|
color:#0f172a;
|
||||||
|
box-shadow:0 6px 16px rgba(34,197,94,0.35);
|
||||||
|
}
|
||||||
|
.create-mode-icon{
|
||||||
|
font-size:0.9rem;
|
||||||
|
}
|
||||||
|
.create-mode-helper{
|
||||||
|
font-size:.72rem;
|
||||||
|
color:#cbd5f5;
|
||||||
|
line-height:1.25;
|
||||||
|
min-height:1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px){
|
||||||
|
.create-mode-summary{
|
||||||
|
display:flex;
|
||||||
|
}
|
||||||
|
.create-mode-panel{
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
.create-mode-panel.create-mode-visible-mobile{
|
||||||
|
display:flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 640px){
|
||||||
|
.create-post-panel{
|
||||||
|
bottom:3%;
|
||||||
|
left:50%;
|
||||||
|
transform:translateX(-50%);
|
||||||
|
width:95%;
|
||||||
|
max-width:100%;
|
||||||
|
max-height:70vh;
|
||||||
|
border-radius:14px;
|
||||||
|
padding:.5rem .6rem .6rem;
|
||||||
|
}
|
||||||
|
.create-mode-chips{
|
||||||
|
gap:.3rem;
|
||||||
|
flex-wrap:nowrap;
|
||||||
|
overflow-x:auto;
|
||||||
|
}
|
||||||
|
.create-mode-chip{
|
||||||
|
flex:0 0 auto;
|
||||||
|
}
|
||||||
|
.create-mode-chip span{
|
||||||
|
font-size:.65rem;
|
||||||
|
}
|
||||||
|
.create-mode-title{
|
||||||
|
font-size:.6rem;
|
||||||
|
}
|
||||||
|
.create-stepper{
|
||||||
|
gap:.25rem;
|
||||||
|
}
|
||||||
|
.create-step-label{
|
||||||
|
font-size:.6rem;
|
||||||
|
}
|
||||||
|
.create-hint{
|
||||||
|
font-size:.65rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
.create-close{
|
.create-close{
|
||||||
background:transparent;
|
background:transparent;
|
||||||
border:none;
|
border:none;
|
||||||
|
|
@ -225,7 +575,8 @@
|
||||||
background:linear-gradient(90deg, #38bdf8, #22c55e);
|
background:linear-gradient(90deg, #38bdf8, #22c55e);
|
||||||
}
|
}
|
||||||
.create-step-body{
|
.create-step-body{
|
||||||
margin-top:.5rem;
|
margin-top:.35rem;
|
||||||
|
padding-bottom:.4rem;
|
||||||
}
|
}
|
||||||
.create-row{
|
.create-row{
|
||||||
display:flex;
|
display:flex;
|
||||||
|
|
@ -825,3 +1176,6 @@ body[data-theme="light"] .map-action-btn * {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
.map-overlay.mode-selector-widget{
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,10 @@
|
||||||
overflow:hidden;
|
overflow:hidden;
|
||||||
border:1px solid rgba(148,163,184,0.25);
|
border:1px solid rgba(148,163,184,0.25);
|
||||||
background: rgba(15,23,42,0.85);
|
background: rgba(15,23,42,0.85);
|
||||||
|
position:relative;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
}
|
}
|
||||||
.sw-wall-image-fallback{
|
.sw-wall-image-fallback{
|
||||||
display:flex;
|
display:flex;
|
||||||
|
|
@ -150,6 +154,49 @@ body[data-theme="light"] .sw-wall-image-fallback{
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display:block;
|
display:block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sw-wall-gallery {
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:.35rem;
|
||||||
|
}
|
||||||
|
.sw-wall-gallery-count {
|
||||||
|
position:absolute;
|
||||||
|
top:8px;
|
||||||
|
right:8px;
|
||||||
|
padding:.18rem .45rem;
|
||||||
|
border-radius:999px;
|
||||||
|
font-size:.65rem;
|
||||||
|
letter-spacing:.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border:1px solid rgba(148,163,184,0.45);
|
||||||
|
background: rgba(15,23,42,0.75);
|
||||||
|
}
|
||||||
|
.sw-wall-gallery-thumbs {
|
||||||
|
display:flex;
|
||||||
|
gap:.35rem;
|
||||||
|
overflow-x:auto;
|
||||||
|
padding-bottom:.25rem;
|
||||||
|
}
|
||||||
|
.sw-wall-gallery-thumb {
|
||||||
|
width:48px;
|
||||||
|
height:48px;
|
||||||
|
border-radius:10px;
|
||||||
|
border:1px solid transparent;
|
||||||
|
overflow:hidden;
|
||||||
|
background: rgba(15,23,42,0.85);
|
||||||
|
flex:0 0 auto;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
.sw-wall-gallery-thumb.active {
|
||||||
|
border-color: rgba(56,189,248,0.9);
|
||||||
|
}
|
||||||
|
.sw-wall-gallery-thumb img {
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
object-fit:cover;
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
.sw-wall-title{
|
.sw-wall-title{
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,36 @@
|
||||||
/* SmartSearchBar — built to play nice with themes (uses CSS variables with fallbacks) */
|
/* SmartSearchBar — built to play nice with themes (uses CSS variables with fallbacks) */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sw-panel-bg: rgba(7, 10, 14, 0.88);
|
||||||
|
--sw-panel-border: rgba(255, 255, 255, 0.14);
|
||||||
|
--sw-panel-bg-2: rgba(8, 11, 18, 0.95);
|
||||||
|
--sw-chip-bg: rgba(255, 255, 255, 0.08);
|
||||||
|
--sw-text: rgba(255, 255, 255, 0.92);
|
||||||
|
--sw-muted: rgba(255, 255, 255, 0.6);
|
||||||
|
--sw-hover: rgba(255, 255, 255, 0.08);
|
||||||
|
--sw-danger: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="blue"] {
|
||||||
|
--sw-panel-bg: rgba(6, 10, 22, 0.86);
|
||||||
|
--sw-panel-border: rgba(56, 189, 248, 0.45);
|
||||||
|
--sw-panel-bg-2: rgba(5, 7, 15, 0.97);
|
||||||
|
--sw-chip-bg: rgba(56, 189, 248, 0.18);
|
||||||
|
--sw-text: #e3f3ff;
|
||||||
|
--sw-muted: rgba(226, 239, 255, 0.82);
|
||||||
|
--sw-hover: rgba(56, 189, 248, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="light"] {
|
||||||
|
--sw-panel-bg: rgba(255, 255, 255, 0.92);
|
||||||
|
--sw-panel-border: rgba(148, 163, 184, 0.72);
|
||||||
|
--sw-panel-bg-2: rgba(255, 255, 255, 0.98);
|
||||||
|
--sw-chip-bg: rgba(148, 163, 184, 0.2);
|
||||||
|
--sw-text: #0b1220;
|
||||||
|
--sw-muted: rgba(91, 103, 122, 0.85);
|
||||||
|
--sw-hover: rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.sw-search {
|
.sw-search {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: min(580px, 100%);
|
width: min(580px, 100%);
|
||||||
|
|
@ -147,6 +178,20 @@
|
||||||
background: var(--sw-panel-bg-2, rgba(12, 14, 22, 0.80));
|
background: var(--sw-panel-bg-2, rgba(12, 14, 22, 0.80));
|
||||||
border: 1px solid var(--sw-panel-border, rgba(255, 255, 255, 0.10));
|
border: 1px solid var(--sw-panel-border, rgba(255, 255, 255, 0.10));
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
|
|
||||||
|
max-height: 360px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.35) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-search__dropdown::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sw-search__dropdown::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.35);
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sw-search__row {
|
.sw-search__row {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue