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:
egecankomur
2026-05-05 20:32:45 +03:00
parent 1d5ad5f62f
commit 3caddff515
9 changed files with 584 additions and 2 deletions
+1
View File
@@ -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",
+189
View File
@@ -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>
)}
</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">
+11
View File
@@ -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}`}
/>
);
}
+20
View File
@@ -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ı..." />