feat: add MapLibre GL map to property form and detail page
- Install maplibre-gl; use OpenFreeMap tiles (no API key) - PropertyMapPickerInner: address search via Nominatim, draggable marker, click-to-place, geolocation, clear button - PropertyMapPicker/View: dynamic next/dynamic wrappers (ssr: false) - PropertyMapViewInner: read-only marker view with navigation control - PropertyFormSheet: hidden mapLat/mapLng inputs, picker renders only when sheet is open, resets on property change - Property detail page: Konum section with PropertyMapView + Google Maps link - Sunum page: Google Maps deep link on PropertyCard when coordinates exist
This commit is contained in:
@@ -45,6 +45,7 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
|
"maplibre-gl": "^5.24.0",
|
||||||
"next": "16.1.1",
|
"next": "16.1.1",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"node-appwrite": "^23.1.0",
|
"node-appwrite": "^23.1.0",
|
||||||
|
|||||||
Generated
+189
@@ -113,6 +113,9 @@ importers:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.562.0
|
specifier: ^0.562.0
|
||||||
version: 0.562.0(react@19.2.3)
|
version: 0.562.0(react@19.2.3)
|
||||||
|
maplibre-gl:
|
||||||
|
specifier: ^5.24.0
|
||||||
|
version: 5.24.0
|
||||||
next:
|
next:
|
||||||
specifier: 16.1.1
|
specifier: 16.1.1
|
||||||
version: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
@@ -546,6 +549,42 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@mapbox/jsonlint-lines-primitives@2.0.2':
|
||||||
|
resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
'@mapbox/point-geometry@1.1.0':
|
||||||
|
resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==}
|
||||||
|
|
||||||
|
'@mapbox/tiny-sdf@2.2.0':
|
||||||
|
resolution: {integrity: sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==}
|
||||||
|
|
||||||
|
'@mapbox/unitbezier@0.0.1':
|
||||||
|
resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==}
|
||||||
|
|
||||||
|
'@mapbox/vector-tile@2.0.4':
|
||||||
|
resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==}
|
||||||
|
|
||||||
|
'@mapbox/whoots-js@3.1.0':
|
||||||
|
resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
'@maplibre/geojson-vt@5.0.4':
|
||||||
|
resolution: {integrity: sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==}
|
||||||
|
|
||||||
|
'@maplibre/geojson-vt@6.1.0':
|
||||||
|
resolution: {integrity: sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==}
|
||||||
|
|
||||||
|
'@maplibre/maplibre-gl-style-spec@24.8.4':
|
||||||
|
resolution: {integrity: sha512-kvtUcthzQGn7nzwiwIggB7lzHbXIprMboLdsem8yNCRIZluyxs7aNzvMHgmdl/lAuX8bkGrSUMiy/lIBysajmg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@maplibre/mlt@1.1.9':
|
||||||
|
resolution: {integrity: sha512-g/tD8EYJB97udq33ipuJ9a4Q7fcbZnTEnUrgnEc/tLMmEL+zaCbR+X5fkDBO2dgpaAMsLH179qE3UXg2N0Nc/g==}
|
||||||
|
|
||||||
|
'@maplibre/vt-pbf@4.3.0':
|
||||||
|
resolution: {integrity: sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@0.2.12':
|
'@napi-rs/wasm-runtime@0.2.12':
|
||||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||||
|
|
||||||
@@ -1366,6 +1405,9 @@ packages:
|
|||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
|
'@types/geojson@7946.0.16':
|
||||||
|
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
@@ -1383,6 +1425,9 @@ packages:
|
|||||||
'@types/react@19.2.7':
|
'@types/react@19.2.7':
|
||||||
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
||||||
|
|
||||||
|
'@types/supercluster@7.1.3':
|
||||||
|
resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==}
|
||||||
|
|
||||||
'@types/use-sync-external-store@0.0.6':
|
'@types/use-sync-external-store@0.0.6':
|
||||||
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||||
|
|
||||||
@@ -1826,6 +1871,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
earcut@3.0.2:
|
||||||
|
resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.267:
|
electron-to-chromium@1.5.267:
|
||||||
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
||||||
|
|
||||||
@@ -2086,6 +2134,9 @@ packages:
|
|||||||
get-tsconfig@4.13.0:
|
get-tsconfig@4.13.0:
|
||||||
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
||||||
|
|
||||||
|
gl-matrix@3.4.4:
|
||||||
|
resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -2317,6 +2368,9 @@ packages:
|
|||||||
json-stable-stringify-without-jsonify@1.0.1:
|
json-stable-stringify-without-jsonify@1.0.1:
|
||||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||||
|
|
||||||
|
json-stringify-pretty-compact@4.0.0:
|
||||||
|
resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==}
|
||||||
|
|
||||||
json5@1.0.2:
|
json5@1.0.2:
|
||||||
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -2330,6 +2384,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
|
||||||
|
kdbush@4.0.2:
|
||||||
|
resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
@@ -2440,6 +2497,10 @@ packages:
|
|||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
maplibre-gl@5.24.0:
|
||||||
|
resolution: {integrity: sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==}
|
||||||
|
engines: {node: '>=16.14.0', npm: '>=8.1.0'}
|
||||||
|
|
||||||
math-intrinsics@1.1.0:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2465,6 +2526,9 @@ packages:
|
|||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
murmurhash-js@1.0.0:
|
||||||
|
resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==}
|
||||||
|
|
||||||
nanoid@3.3.11:
|
nanoid@3.3.11:
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
@@ -2577,6 +2641,10 @@ packages:
|
|||||||
path-parse@1.0.7:
|
path-parse@1.0.7:
|
||||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||||
|
|
||||||
|
pbf@4.0.1:
|
||||||
|
resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -2600,6 +2668,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
potpack@2.1.0:
|
||||||
|
resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==}
|
||||||
|
|
||||||
prelude-ls@1.2.1:
|
prelude-ls@1.2.1:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -2607,6 +2678,9 @@ packages:
|
|||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
|
|
||||||
|
protocol-buffers-schema@3.6.1:
|
||||||
|
resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==}
|
||||||
|
|
||||||
punycode@2.3.1:
|
punycode@2.3.1:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2614,6 +2688,9 @@ packages:
|
|||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
|
quickselect@3.0.0:
|
||||||
|
resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==}
|
||||||
|
|
||||||
react-day-picker@9.13.0:
|
react-day-picker@9.13.0:
|
||||||
resolution: {integrity: sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==}
|
resolution: {integrity: sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2723,6 +2800,9 @@ packages:
|
|||||||
resolve-pkg-maps@1.0.0:
|
resolve-pkg-maps@1.0.0:
|
||||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
|
resolve-protobuf-schema@2.1.0:
|
||||||
|
resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==}
|
||||||
|
|
||||||
resolve@1.22.11:
|
resolve@1.22.11:
|
||||||
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
|
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2867,6 +2947,9 @@ packages:
|
|||||||
babel-plugin-macros:
|
babel-plugin-macros:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
supercluster@8.0.1:
|
||||||
|
resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==}
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2895,6 +2978,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tinyqueue@3.0.0:
|
||||||
|
resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
@@ -3407,6 +3493,51 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@mapbox/jsonlint-lines-primitives@2.0.2': {}
|
||||||
|
|
||||||
|
'@mapbox/point-geometry@1.1.0': {}
|
||||||
|
|
||||||
|
'@mapbox/tiny-sdf@2.2.0': {}
|
||||||
|
|
||||||
|
'@mapbox/unitbezier@0.0.1': {}
|
||||||
|
|
||||||
|
'@mapbox/vector-tile@2.0.4':
|
||||||
|
dependencies:
|
||||||
|
'@mapbox/point-geometry': 1.1.0
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
pbf: 4.0.1
|
||||||
|
|
||||||
|
'@mapbox/whoots-js@3.1.0': {}
|
||||||
|
|
||||||
|
'@maplibre/geojson-vt@5.0.4': {}
|
||||||
|
|
||||||
|
'@maplibre/geojson-vt@6.1.0':
|
||||||
|
dependencies:
|
||||||
|
kdbush: 4.0.2
|
||||||
|
|
||||||
|
'@maplibre/maplibre-gl-style-spec@24.8.4':
|
||||||
|
dependencies:
|
||||||
|
'@mapbox/jsonlint-lines-primitives': 2.0.2
|
||||||
|
'@mapbox/unitbezier': 0.0.1
|
||||||
|
json-stringify-pretty-compact: 4.0.0
|
||||||
|
minimist: 1.2.8
|
||||||
|
quickselect: 3.0.0
|
||||||
|
tinyqueue: 3.0.0
|
||||||
|
|
||||||
|
'@maplibre/mlt@1.1.9':
|
||||||
|
dependencies:
|
||||||
|
'@mapbox/point-geometry': 1.1.0
|
||||||
|
|
||||||
|
'@maplibre/vt-pbf@4.3.0':
|
||||||
|
dependencies:
|
||||||
|
'@mapbox/point-geometry': 1.1.0
|
||||||
|
'@mapbox/vector-tile': 2.0.4
|
||||||
|
'@maplibre/geojson-vt': 5.0.4
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
'@types/supercluster': 7.1.3
|
||||||
|
pbf: 4.0.1
|
||||||
|
supercluster: 8.0.1
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@0.2.12':
|
'@napi-rs/wasm-runtime@0.2.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.7.1
|
'@emnapi/core': 1.7.1
|
||||||
@@ -4196,6 +4327,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
|
'@types/geojson@7946.0.16': {}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
'@types/json5@0.0.29': {}
|
'@types/json5@0.0.29': {}
|
||||||
@@ -4212,6 +4345,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@types/supercluster@7.1.3':
|
||||||
|
dependencies:
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
|
||||||
'@types/use-sync-external-store@0.0.6': {}
|
'@types/use-sync-external-store@0.0.6': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
'@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
|
||||||
@@ -4663,6 +4800,8 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
|
earcut@3.0.2: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.267: {}
|
electron-to-chromium@1.5.267: {}
|
||||||
|
|
||||||
emoji-regex@9.2.2: {}
|
emoji-regex@9.2.2: {}
|
||||||
@@ -5079,6 +5218,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
|
gl-matrix@3.4.4: {}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -5296,6 +5437,8 @@ snapshots:
|
|||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||||
|
|
||||||
|
json-stringify-pretty-compact@4.0.0: {}
|
||||||
|
|
||||||
json5@1.0.2:
|
json5@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
@@ -5309,6 +5452,8 @@ snapshots:
|
|||||||
object.assign: 4.1.7
|
object.assign: 4.1.7
|
||||||
object.values: 1.2.1
|
object.values: 1.2.1
|
||||||
|
|
||||||
|
kdbush@4.0.2: {}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
@@ -5395,6 +5540,28 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
maplibre-gl@5.24.0:
|
||||||
|
dependencies:
|
||||||
|
'@mapbox/jsonlint-lines-primitives': 2.0.2
|
||||||
|
'@mapbox/point-geometry': 1.1.0
|
||||||
|
'@mapbox/tiny-sdf': 2.2.0
|
||||||
|
'@mapbox/unitbezier': 0.0.1
|
||||||
|
'@mapbox/vector-tile': 2.0.4
|
||||||
|
'@mapbox/whoots-js': 3.1.0
|
||||||
|
'@maplibre/geojson-vt': 6.1.0
|
||||||
|
'@maplibre/maplibre-gl-style-spec': 24.8.4
|
||||||
|
'@maplibre/mlt': 1.1.9
|
||||||
|
'@maplibre/vt-pbf': 4.3.0
|
||||||
|
'@types/geojson': 7946.0.16
|
||||||
|
earcut: 3.0.2
|
||||||
|
gl-matrix: 3.4.4
|
||||||
|
kdbush: 4.0.2
|
||||||
|
murmurhash-js: 1.0.0
|
||||||
|
pbf: 4.0.1
|
||||||
|
potpack: 2.1.0
|
||||||
|
quickselect: 3.0.0
|
||||||
|
tinyqueue: 3.0.0
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
@@ -5416,6 +5583,8 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
murmurhash-js@1.0.0: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
napi-postinstall@0.3.4: {}
|
napi-postinstall@0.3.4: {}
|
||||||
@@ -5535,6 +5704,10 @@ snapshots:
|
|||||||
|
|
||||||
path-parse@1.0.7: {}
|
path-parse@1.0.7: {}
|
||||||
|
|
||||||
|
pbf@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
resolve-protobuf-schema: 2.1.0
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
@@ -5555,6 +5728,8 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
potpack@2.1.0: {}
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
@@ -5563,10 +5738,14 @@ snapshots:
|
|||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
react-is: 16.13.1
|
react-is: 16.13.1
|
||||||
|
|
||||||
|
protocol-buffers-schema@3.6.1: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
|
quickselect@3.0.0: {}
|
||||||
|
|
||||||
react-day-picker@9.13.0(react@19.2.3):
|
react-day-picker@9.13.0(react@19.2.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@date-fns/tz': 1.4.1
|
'@date-fns/tz': 1.4.1
|
||||||
@@ -5682,6 +5861,10 @@ snapshots:
|
|||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|
||||||
|
resolve-protobuf-schema@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
protocol-buffers-schema: 3.6.1
|
||||||
|
|
||||||
resolve@1.22.11:
|
resolve@1.22.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
@@ -5893,6 +6076,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
|
|
||||||
|
supercluster@8.0.1:
|
||||||
|
dependencies:
|
||||||
|
kdbush: 4.0.2
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
@@ -5916,6 +6103,8 @@ snapshots:
|
|||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
tinyqueue@3.0.0: {}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { createAdminClient } from "@/lib/appwrite/server";
|
import { createAdminClient } from "@/lib/appwrite/server";
|
||||||
import { getPropertyImagePreviewUrl, parseImageIds } from "@/lib/appwrite/storage-utils";
|
import { getPropertyImagePreviewUrl, parseImageIds } from "@/lib/appwrite/storage-utils";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { PropertyMapView } from "@/components/map/property-map-view";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
@@ -166,6 +167,17 @@ export default async function PropertyDetailPage({ params }: Props) {
|
|||||||
{property.address}
|
{property.address}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{property.mapLat != null && property.mapLng != null && (
|
||||||
|
<a
|
||||||
|
href={`https://www.google.com/maps?q=${property.mapLat},${property.mapLng}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-xs text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
Google Maps'te aç ↗
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{property.description && (
|
{property.description && (
|
||||||
@@ -177,6 +189,18 @@ export default async function PropertyDetailPage({ params }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{property.mapLat != null && property.mapLng != null && (
|
||||||
|
<div className="rounded-lg border overflow-hidden">
|
||||||
|
<h2 className="px-4 pt-4 pb-2 text-sm font-semibold">Konum</h2>
|
||||||
|
<PropertyMapView
|
||||||
|
lat={property.mapLat}
|
||||||
|
lng={property.mapLng}
|
||||||
|
title={property.title}
|
||||||
|
className="h-64 rounded-none border-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Activities */}
|
{/* Activities */}
|
||||||
{activities.length > 0 && (
|
{activities.length > 0 && (
|
||||||
<div className="rounded-lg border p-4">
|
<div className="rounded-lg border p-4">
|
||||||
|
|||||||
@@ -144,6 +144,17 @@ function PropertyCard({ property: p }: { property: Property }) {
|
|||||||
{p.description && (
|
{p.description && (
|
||||||
<p className="text-xs text-gray-500 line-clamp-3">{p.description}</p>
|
<p className="text-xs text-gray-500 line-clamp-3">{p.description}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{p.mapLat != null && p.mapLng != null && (
|
||||||
|
<a
|
||||||
|
href={`https://www.google.com/maps?q=${p.mapLat},${p.mapLng}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-1 text-xs text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
📍 Haritada gör
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import maplibregl from "maplibre-gl";
|
||||||
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
|
import { Search, Loader2, MapPin, X } from "lucide-react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
const STYLE_URL = "https://tiles.openfreemap.org/styles/bright";
|
||||||
|
const TURKEY_CENTER: [number, number] = [35.0, 39.0];
|
||||||
|
const MARKER_COLOR = "#2563eb";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
initialLat?: number | null;
|
||||||
|
initialLng?: number | null;
|
||||||
|
initialSearchQuery?: string;
|
||||||
|
onLocationChange: (lat: number, lng: number) => void;
|
||||||
|
onClear: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PropertyMapPickerInner({
|
||||||
|
initialLat,
|
||||||
|
initialLng,
|
||||||
|
initialSearchQuery = "",
|
||||||
|
onLocationChange,
|
||||||
|
onClear,
|
||||||
|
}: Props) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const mapRef = useRef<maplibregl.Map | null>(null);
|
||||||
|
const markerRef = useRef<maplibregl.Marker | null>(null);
|
||||||
|
const placeRef = useRef<(lat: number, lng: number) => void>(() => {});
|
||||||
|
|
||||||
|
const [search, setSearch] = useState(initialSearchQuery);
|
||||||
|
const [geocoding, setGeocoding] = useState(false);
|
||||||
|
const [notFound, setNotFound] = useState(false);
|
||||||
|
const [coords, setCoords] = useState<{ lat: number; lng: number } | null>(
|
||||||
|
initialLat != null && initialLng != null
|
||||||
|
? { lat: initialLat, lng: initialLng }
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Keep placeRef current so closures in event handlers always have latest state
|
||||||
|
placeRef.current = (lat: number, lng: number) => {
|
||||||
|
setCoords({ lat, lng });
|
||||||
|
onLocationChange(lat, lng);
|
||||||
|
|
||||||
|
if (markerRef.current) {
|
||||||
|
markerRef.current.setLngLat([lng, lat]);
|
||||||
|
} else if (mapRef.current) {
|
||||||
|
const marker = new maplibregl.Marker({ draggable: true, color: MARKER_COLOR })
|
||||||
|
.setLngLat([lng, lat])
|
||||||
|
.addTo(mapRef.current);
|
||||||
|
|
||||||
|
marker.on("dragend", () => {
|
||||||
|
const pos = marker.getLngLat();
|
||||||
|
placeRef.current(pos.lat, pos.lng);
|
||||||
|
});
|
||||||
|
|
||||||
|
markerRef.current = marker;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const hasInitial = initialLat != null && initialLng != null;
|
||||||
|
|
||||||
|
const map = new maplibregl.Map({
|
||||||
|
container: containerRef.current,
|
||||||
|
style: STYLE_URL,
|
||||||
|
center: hasInitial ? [initialLng!, initialLat!] : TURKEY_CENTER,
|
||||||
|
zoom: hasInitial ? 14 : 5.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||||
|
map.addControl(
|
||||||
|
new maplibregl.GeolocateControl({ positionOptions: { enableHighAccuracy: true } }),
|
||||||
|
"top-right",
|
||||||
|
);
|
||||||
|
|
||||||
|
mapRef.current = map;
|
||||||
|
|
||||||
|
if (hasInitial) {
|
||||||
|
const marker = new maplibregl.Marker({ draggable: true, color: MARKER_COLOR })
|
||||||
|
.setLngLat([initialLng!, initialLat!])
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
marker.on("dragend", () => {
|
||||||
|
const pos = marker.getLngLat();
|
||||||
|
placeRef.current(pos.lat, pos.lng);
|
||||||
|
});
|
||||||
|
|
||||||
|
markerRef.current = marker;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.on("click", (e) => {
|
||||||
|
placeRef.current(e.lngLat.lat, e.lngLat.lng);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
map.remove();
|
||||||
|
mapRef.current = null;
|
||||||
|
markerRef.current = null;
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleSearch(e?: React.FormEvent) {
|
||||||
|
e?.preventDefault();
|
||||||
|
if (!search.trim()) return;
|
||||||
|
setGeocoding(true);
|
||||||
|
setNotFound(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const q = search.includes("Türkiye") ? search : `${search}, Türkiye`;
|
||||||
|
const res = await fetch(
|
||||||
|
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=json&limit=1`,
|
||||||
|
{ headers: { "Accept-Language": "tr,en" } },
|
||||||
|
);
|
||||||
|
const data = (await res.json()) as { lat: string; lon: string }[];
|
||||||
|
|
||||||
|
if (!data.length) {
|
||||||
|
setNotFound(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lat = parseFloat(data[0].lat);
|
||||||
|
const lng = parseFloat(data[0].lon);
|
||||||
|
|
||||||
|
placeRef.current(lat, lng);
|
||||||
|
mapRef.current?.flyTo({ center: [lng, lat], zoom: 15, duration: 1000 });
|
||||||
|
} catch {
|
||||||
|
setNotFound(true);
|
||||||
|
} finally {
|
||||||
|
setGeocoding(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClear() {
|
||||||
|
markerRef.current?.remove();
|
||||||
|
markerRef.current = null;
|
||||||
|
setCoords(null);
|
||||||
|
onClear();
|
||||||
|
mapRef.current?.flyTo({ center: TURKEY_CENTER, zoom: 5.5 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* Search bar */}
|
||||||
|
<form onSubmit={handleSearch} className="flex gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="text-muted-foreground absolute left-2.5 top-2.5 size-4" />
|
||||||
|
<Input
|
||||||
|
className="pl-8"
|
||||||
|
placeholder="Adres, mahalle veya şehir ara..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
setNotFound(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" variant="outline" size="icon" disabled={geocoding}>
|
||||||
|
{geocoding ? (
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Search className="size-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{notFound && (
|
||||||
|
<p className="text-destructive text-xs">
|
||||||
|
Adres bulunamadı — farklı bir arama deneyin.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Map */}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="h-64 w-full overflow-hidden rounded-lg border"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Coordinate display / clear */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-muted-foreground flex items-center gap-1.5 text-xs">
|
||||||
|
<MapPin className="size-3 shrink-0" />
|
||||||
|
{coords ? (
|
||||||
|
<span className="font-mono">
|
||||||
|
{coords.lat.toFixed(5)}, {coords.lng.toFixed(5)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>Haritaya tıklayın veya adres arayın</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{coords && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClear}
|
||||||
|
className="text-muted-foreground hover:text-foreground flex items-center gap-1 text-xs"
|
||||||
|
>
|
||||||
|
<X className="size-3" />
|
||||||
|
Konumu kaldır
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { MapPin } from "lucide-react";
|
||||||
|
|
||||||
|
const Inner = dynamic(
|
||||||
|
() =>
|
||||||
|
import("./property-map-picker-inner").then((m) => m.PropertyMapPickerInner),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
loading: () => (
|
||||||
|
<div className="flex h-64 w-full items-center justify-center rounded-lg border bg-muted/30">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<MapPin className="size-4 animate-pulse" />
|
||||||
|
Harita yükleniyor...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
initialLat?: number | null;
|
||||||
|
initialLng?: number | null;
|
||||||
|
initialSearchQuery?: string;
|
||||||
|
onLocationChange: (lat: number, lng: number) => void;
|
||||||
|
onClear: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PropertyMapPicker(props: Props) {
|
||||||
|
return <Inner {...props} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import maplibregl from "maplibre-gl";
|
||||||
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
|
|
||||||
|
const STYLE_URL = "https://tiles.openfreemap.org/styles/bright";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PropertyMapViewInner({ lat, lng, title, className = "" }: Props) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
const map = new maplibregl.Map({
|
||||||
|
container: containerRef.current,
|
||||||
|
style: STYLE_URL,
|
||||||
|
center: [lng, lat],
|
||||||
|
zoom: 15,
|
||||||
|
interactive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||||
|
|
||||||
|
const popup = title
|
||||||
|
? new maplibregl.Popup({ offset: 25, closeButton: false }).setText(title)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
new maplibregl.Marker({ color: "#2563eb" })
|
||||||
|
.setLngLat([lng, lat])
|
||||||
|
.setPopup(popup)
|
||||||
|
.addTo(map);
|
||||||
|
|
||||||
|
return () => map.remove();
|
||||||
|
}, [lat, lng, title]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`overflow-hidden rounded-lg border ${className}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
const Inner = dynamic(
|
||||||
|
() =>
|
||||||
|
import("./property-map-view-inner").then((m) => m.PropertyMapViewInner),
|
||||||
|
{ ssr: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
title?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PropertyMapView(props: Props) {
|
||||||
|
return <Inner {...props} />;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useActionState, useEffect } from "react";
|
import { useActionState, useEffect, useState } from "react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -8,7 +8,6 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
@@ -18,6 +17,7 @@ import {
|
|||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
import { createPropertyAction, updatePropertyAction } from "@/lib/appwrite/property-actions";
|
import { createPropertyAction, updatePropertyAction } from "@/lib/appwrite/property-actions";
|
||||||
import { PropertyImageUploader } from "./property-image-uploader";
|
import { PropertyImageUploader } from "./property-image-uploader";
|
||||||
|
import { PropertyMapPicker } from "@/components/map/property-map-picker";
|
||||||
import { parseImageIds } from "@/lib/appwrite/storage-utils";
|
import { parseImageIds } from "@/lib/appwrite/storage-utils";
|
||||||
import type { Property } from "@/lib/appwrite/schema";
|
import type { Property } from "@/lib/appwrite/schema";
|
||||||
|
|
||||||
@@ -37,6 +37,8 @@ export function PropertyFormSheet({ open, onOpenChange, property, onSuccess }: P
|
|||||||
: createPropertyAction;
|
: createPropertyAction;
|
||||||
|
|
||||||
const [state, formAction, isPending] = useActionState(action, INITIAL);
|
const [state, formAction, isPending] = useActionState(action, INITIAL);
|
||||||
|
const [mapLat, setMapLat] = useState<number | null>(property?.mapLat ?? null);
|
||||||
|
const [mapLng, setMapLng] = useState<number | null>(property?.mapLng ?? null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.ok) {
|
if (state.ok) {
|
||||||
@@ -48,8 +50,24 @@ export function PropertyFormSheet({ open, onOpenChange, property, onSuccess }: P
|
|||||||
}
|
}
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
|
// Reset lat/lng when sheet opens for a different property
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setMapLat(property?.mapLat ?? null);
|
||||||
|
setMapLng(property?.mapLng ?? null);
|
||||||
|
}
|
||||||
|
}, [open, property]);
|
||||||
|
|
||||||
const fe = state.fieldErrors ?? {};
|
const fe = state.fieldErrors ?? {};
|
||||||
|
|
||||||
|
const initialSearchQuery = [
|
||||||
|
property?.neighborhood,
|
||||||
|
property?.district,
|
||||||
|
property?.city,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent className="w-full sm:max-w-xl overflow-y-auto">
|
<SheetContent className="w-full sm:max-w-xl overflow-y-auto">
|
||||||
@@ -58,6 +76,10 @@ export function PropertyFormSheet({ open, onOpenChange, property, onSuccess }: P
|
|||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<form action={formAction} className="mt-4 space-y-4 pb-6">
|
<form action={formAction} className="mt-4 space-y-4 pb-6">
|
||||||
|
{/* Hidden lat/lng — updated by map picker */}
|
||||||
|
<input type="hidden" name="mapLat" value={mapLat ?? ""} />
|
||||||
|
<input type="hidden" name="mapLng" value={mapLng ?? ""} />
|
||||||
|
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
<Label htmlFor="title">Başlık *</Label>
|
<Label htmlFor="title">Başlık *</Label>
|
||||||
<Input id="title" name="title" defaultValue={property?.title} placeholder="3+1 Daire, Kadıköy" />
|
<Input id="title" name="title" defaultValue={property?.title} placeholder="3+1 Daire, Kadıköy" />
|
||||||
@@ -165,6 +187,29 @@ export function PropertyFormSheet({ open, onOpenChange, property, onSuccess }: P
|
|||||||
<Textarea id="address" name="address" rows={2} defaultValue={property?.address ?? ""} placeholder="Sokak, kapı no..." />
|
<Textarea id="address" name="address" rows={2} defaultValue={property?.address ?? ""} placeholder="Sokak, kapı no..." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Map picker */}
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
<Label>Konum (harita)</Label>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
Adres arayın veya haritaya tıklayarak pin bırakın. Sürükleyerek hassaslaştırabilirsiniz.
|
||||||
|
</p>
|
||||||
|
{open && (
|
||||||
|
<PropertyMapPicker
|
||||||
|
initialLat={mapLat}
|
||||||
|
initialLng={mapLng}
|
||||||
|
initialSearchQuery={initialSearchQuery}
|
||||||
|
onLocationChange={(lat, lng) => {
|
||||||
|
setMapLat(lat);
|
||||||
|
setMapLng(lng);
|
||||||
|
}}
|
||||||
|
onClear={() => {
|
||||||
|
setMapLat(null);
|
||||||
|
setMapLng(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
<Label htmlFor="description">Açıklama</Label>
|
<Label htmlFor="description">Açıklama</Label>
|
||||||
<Textarea id="description" name="description" rows={3} defaultValue={property?.description ?? ""} placeholder="İlan detayları..." />
|
<Textarea id="description" name="description" rows={3} defaultValue={property?.description ?? ""} placeholder="İlan detayları..." />
|
||||||
|
|||||||
Reference in New Issue
Block a user