9 Commits

Author SHA1 Message Date
C4rnivore 99fa5a93d0 hover and selected states for markers 2025-07-28 17:47:50 +05:00
C4rnivore a1b896b37f upd 2025-07-28 16:00:24 +05:00
C4rnivore 72bfdeb5a3 upd 2025-07-28 15:43:08 +05:00
C4rnivore d1b6eb7c82 design update 2025-07-28 15:24:49 +05:00
C4rnivore 4afbf6bf66 upd 2025-07-25 16:10:47 +05:00
C4rnivore 54d48c8ce3 Mobile map modal and more POIs 2025-07-25 11:58:30 +05:00
C4rnivore 226397afac overflow fix 2025-07-25 10:50:17 +05:00
C4rnivore 1497d6ede3 Markers clusterization, mobile version of map 2025-07-24 17:27:14 +05:00
C4rnivore 6a415ec165 Google map with custom markers 2025-07-23 17:57:58 +05:00
22 changed files with 766 additions and 94 deletions
+3 -1
View File
@@ -2,4 +2,6 @@
# VITE_API_URL=http://192.168.1.144:4002 # VITE_API_URL=http://192.168.1.144:4002
# VITE_API_URL=http://194.26.138.94:4002 # VITE_API_URL=http://194.26.138.94:4002
# VITE_API_URL=https://irthtest.online/api # VITE_API_URL=https://irthtest.online/api
VITE_API_URL=https://irth.graff.estate/api VITE_API_URL=https://irth.graff.estate/api
VITE_GOOGLE_MAP_API_KEY=AIzaSyD1aCnh8qEIh9ACrZWeHddYJLyHMX4KsoE
VITE_GOOGLE_MAP_ID=30ff24bdc133c941ee0d0608
+2
View File
@@ -25,3 +25,5 @@ public/virtual-tours
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.env
+18
View File
@@ -4,11 +4,13 @@
"": { "": {
"name": "irth-new", "name": "irth-new",
"dependencies": { "dependencies": {
"@googlemaps/markerclusterer": "^2.6.2",
"@tailwindcss/vite": "^4.1.3", "@tailwindcss/vite": "^4.1.3",
"@tanstack/react-query": "^5.74.4", "@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-devtools": "^5.74.7", "@tanstack/react-query-devtools": "^5.74.7",
"@tweenjs/tween.js": "^25.0.0", "@tweenjs/tween.js": "^25.0.0",
"@uidotdev/usehooks": "^2.4.1", "@uidotdev/usehooks": "^2.4.1",
"@vis.gl/react-google-maps": "^1.5.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
@@ -111,6 +113,8 @@
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.8", "", { "dependencies": { "@eslint/core": "^0.13.0", "levn": "^0.4.1" } }, "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.8", "", { "dependencies": { "@eslint/core": "^0.13.0", "levn": "^0.4.1" } }, "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA=="],
"@googlemaps/markerclusterer": ["@googlemaps/markerclusterer@2.6.2", "", { "dependencies": { "@types/supercluster": "^7.1.3", "fast-equals": "^5.2.2", "supercluster": "^8.0.1" } }, "sha512-U6uVhq8iWhiIckA89sgRu8OK35mjd6/3CuoZKWakKEf0QmRRWpatlsPb3kqXkoWSmbcZkopRiI4dnW6DQSd7bQ=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
@@ -237,6 +241,10 @@
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/google.maps": ["@types/google.maps@3.58.1", "", {}, "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="], "@types/node": ["@types/node@22.14.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA=="],
@@ -245,6 +253,8 @@
"@types/react-dom": ["@types/react-dom@19.1.1", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w=="], "@types/react-dom": ["@types/react-dom@19.1.1", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w=="],
"@types/supercluster": ["@types/supercluster@7.1.3", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.29.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.29.0", "@typescript-eslint/type-utils": "8.29.0", "@typescript-eslint/utils": "8.29.0", "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.29.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.29.0", "@typescript-eslint/type-utils": "8.29.0", "@typescript-eslint/utils": "8.29.0", "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ=="],
@@ -265,6 +275,8 @@
"@uidotdev/usehooks": ["@uidotdev/usehooks@2.4.1", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg=="], "@uidotdev/usehooks": ["@uidotdev/usehooks@2.4.1", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg=="],
"@vis.gl/react-google-maps": ["@vis.gl/react-google-maps@1.5.4", "", { "dependencies": { "@types/google.maps": "^3.54.10", "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "react": ">=16.8.0 || ^19.0 || ^19.0.0-rc", "react-dom": ">=16.8.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-pD3e2wDtOfd439mamkacRgrM6I2B/lue61QCR0pGQT8MVaG9pz9/LajHbsjZW2lms8Ao8mf2PQJeiGC2FxI0Fw=="],
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.8.1", "", { "dependencies": { "@swc/core": "^1.11.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-aEUPCckHDcFyxpwFm0AIkbtv6PpUp3xTb9wYGFjtABynXjCYKkWoxX0AOK9NT9XCrdk6mBBUOeHQS+RKdcNO1A=="], "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.8.1", "", { "dependencies": { "@swc/core": "^1.11.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-aEUPCckHDcFyxpwFm0AIkbtv6PpUp3xTb9wYGFjtABynXjCYKkWoxX0AOK9NT9XCrdk6mBBUOeHQS+RKdcNO1A=="],
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
@@ -343,6 +355,8 @@
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-equals": ["fast-equals@5.2.2", "", {}, "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
@@ -403,6 +417,8 @@
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"kdbush": ["kdbush@4.0.2", "", {}, "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"ky": ["ky@1.8.1", "", {}, "sha512-7Bp3TpsE+L+TARSnnDpk3xg8Idi8RwSLdj6CMbNWoOARIrGrbuLGusV0dYwbZOm4bB3jHNxSw8Wk/ByDqJEnDw=="], "ky": ["ky@1.8.1", "", {}, "sha512-7Bp3TpsE+L+TARSnnDpk3xg8Idi8RwSLdj6CMbNWoOARIrGrbuLGusV0dYwbZOm4bB3jHNxSw8Wk/ByDqJEnDw=="],
@@ -513,6 +529,8 @@
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tailwindcss": ["tailwindcss@4.1.3", "", {}, "sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g=="], "tailwindcss": ["tailwindcss@4.1.3", "", {}, "sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g=="],
+2
View File
@@ -10,11 +10,13 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@googlemaps/markerclusterer": "^2.6.2",
"@tailwindcss/vite": "^4.1.3", "@tailwindcss/vite": "^4.1.3",
"@tanstack/react-query": "^5.74.4", "@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-devtools": "^5.74.7", "@tanstack/react-query-devtools": "^5.74.7",
"@tweenjs/tween.js": "^25.0.0", "@tweenjs/tween.js": "^25.0.0",
"@uidotdev/usehooks": "^2.4.1", "@uidotdev/usehooks": "^2.4.1",
"@vis.gl/react-google-maps": "^1.5.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="16" fill="#0D1922"/>
<path d="M13 11.6869H11C10.4477 11.6869 10 12.1346 10 12.6869V22C10 22.5523 10.4477 23 11 23H20.9993C21.5516 23 21.9993 22.5523 21.9993 22.0001L21.9999 12.687C22 12.1346 21.5522 11.6869 20.9999 11.6869H19M13 11.6869V11C13 9.34315 14.3431 8 16 8V8C17.6569 8 19 9.34315 19 11V11.6869M13 11.6869H19" stroke="white" stroke-width="1.5" stroke-linecap="square"/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

+15 -25
View File
@@ -20,6 +20,9 @@ import clsx from "clsx";
import Slider from "./Slider"; import Slider from "./Slider";
import PlusIcon from "./icons/map/PlusIcon"; import PlusIcon from "./icons/map/PlusIcon";
import EqualIcon from "./icons/EqualIcon"; import EqualIcon from "./icons/EqualIcon";
import GoogleMap from "./google-map/GoogleMap";
import { GoogleMapData } from "../data/googleMapData";
import GoogleMapMobile from "./google-map/GoogleMapMobile";
// import FullScreenButton from "./FullScreenButton"; // import FullScreenButton from "./FullScreenButton";
function AboutDubaiMarina() { function AboutDubaiMarina() {
@@ -336,7 +339,7 @@ function AboutDubaiMarina() {
</div> </div>
</div> </div>
<div className="flex flex-col items-center gap-[2.222vw] bg-white px-[2.222vw] pb-[8.333vw] max-md:gap-[6.667vw] max-md:px-[4.444vw] max-2xl:gap-[4.167vw]"> <div className="flex flex-col items-center gap-[2.222vw] bg-white px-[2.222vw] pb-[8.333vw] max-md:gap-[6.667vw] max-md:px-[4.444vw] max-2xl:gap-[4.167vw]">
{/* <h1 <h1
className="font-mixcase-unmixed text-[3.889vw] text-[#0D1922] w-[44.861vw] leading-[100%] tracking-[-0.05em] pt-[7.222vw] text-center className="font-mixcase-unmixed text-[3.889vw] text-[#0D1922] w-[44.861vw] leading-[100%] tracking-[-0.05em] pt-[7.222vw] text-center
max-md:text-[6.667vw] max-md:w-full max-2xl:text-[7.292vw] max-2xl:w-[84.115vw]" max-md:text-[6.667vw] max-md:w-full max-2xl:text-[7.292vw] max-2xl:w-[84.115vw]"
> >
@@ -347,31 +350,18 @@ function AboutDubaiMarina() {
living meets modern convenience. Enjoy an energetic lifestyle surrounded by living meets modern convenience. Enjoy an energetic lifestyle surrounded by
trendy cafés, shops, and entertainment options all within reach.`} trendy cafés, shops, and entertainment options all within reach.`}
</p> </p>
<motion.img <div className="w-full 2xl:aspect-[1376/688] max-2xl:aspect-[720/688] max-md:hidden rounded-[1.667vw] overflow-hidden">
ref={mapRef} <GoogleMap
src="/images/about-complex/dubai-marina/central_map.png" markers={GoogleMapData.markers}
alt="central map" mapCenter={GoogleMapData.dubaiMarinaDefaultCenter}
className="rounded-3xl object-cover object-center aspect-[1376/609] max-md:hidden max-2xl:w-[93.75vw] max-2xl:h-[89.583vw] max-2xl:aspect-[720/688] max-2xl:mt-[1.563vw]"
initial={{ width: "47.083vw" }}
animate={
isMapInView ? { width: "95.556vw" } : { width: "47.083vw" }
}
transition={{ duration: 0.6, ease: "easeInOut" }}
/>
<div className="min-md:hidden relative w-[89.167vw] aspect-square overflow-hidden rounded-2xl">
<img
src="/images/about-complex/dubai-marina/central_map.png"
alt="central map"
className="w-full h-full object-cover object-center scale-120"
/> />
<div className="absolute bottom-[1.111vw] right-[1.111vw] w-[11.111vw] h-[11.111vw]"> </div>
<FullScreenButton <div className="w-full">
isFullScreen={false} <GoogleMapMobile
onClick={() => {}} markers={GoogleMapData.markers}
onFullScreenChange={() => {}} mapCenter={GoogleMapData.dubaiMarinaDefaultCenter}
/> />
</div> </div>
</div> */}
</div> </div>
</div> </div>
</motion.section> </motion.section>
+42 -18
View File
@@ -1,21 +1,22 @@
import { import {
marasiDriveFeatures, marasiDriveFeatures,
marasiDriveDescriptionBadges, marasiDriveDescriptionBadges,
marasiDriveMapCards, // marasiDriveMapCards,
} from "../data/aboutMarasiDrive"; } from "../data/aboutMarasiDrive";
import MarariDriveNeighboursSliderDesktop from "./MarasiDriveNeighboursSliderDesktop"; import MarariDriveNeighboursSliderDesktop from "./MarasiDriveNeighboursSliderDesktop";
import MarasiDriveInteriorsSlider from "./MarasiDriveInteriorsSlider"; import MarasiDriveInteriorsSlider from "./MarasiDriveInteriorsSlider";
import MarasiDriveInteriorsSliderMobile from "./MarasiDriveInteriorsSliderMobile"; import MarasiDriveInteriorsSliderMobile from "./MarasiDriveInteriorsSliderMobile";
import MarasiDriveNeighboursSliderMobile from "./MarasiDriveNeighboursSliderMobile"; import MarasiDriveNeighboursSliderMobile from "./MarasiDriveNeighboursSliderMobile";
import MarasiDriveMapMobile from "./MarasiDriveMapMobile"; import GoogleMapMobile from "./google-map/GoogleMapMobile";
import TextBox from "./ui/TextBox"; import TextBox from "./ui/TextBox";
import { useRef } from "react"; import { useRef } from "react";
import { useScroll } from "motion/react"; import { useScroll } from "motion/react";
import MarasiDriveMapCard from "./MarasiDriveMapCard"; // import MarasiDriveMapCard from "./MarasiDriveMapCard";
import GoogleMap from "./google-map/GoogleMap";
import MarasiDriveNeighboursSliderTablet from "./MarasiDriveNeighboursSliderTablet"; import MarasiDriveNeighboursSliderTablet from "./MarasiDriveNeighboursSliderTablet";
import CustomScrollBar from "./ui/ScrollBar"; import CustomScrollBar from "./ui/ScrollBar";
import BrochureButton from "./ui/BrochureButton"; import BrochureButton from "./ui/BrochureButton";
import { GoogleMapData } from "../data/googleMapData";
function AboutMarasiDrive() { function AboutMarasiDrive() {
const target = useRef<HTMLDivElement>(null); const target = useRef<HTMLDivElement>(null);
@@ -98,19 +99,32 @@ function AboutMarasiDrive() {
destination for artful inspiration and cleverly activated spaces.`} destination for artful inspiration and cleverly activated spaces.`}
</p> </p>
</div> </div>
<div ref={homeSliderRef} className="flex max-2xl:flex-wrap max-2xl:justify-center 2xl:gap-[0.556vw] gap-2 md:max-2xl:w-[93.75vw] max-md:flex-row max-md:flex-nowrap max-md:overflow-x-scroll max-md:overflow-y-hidden max-md:justify-start max-md:snap-x max-md:snap-mandatory [&::-webkit-scrollbar]:h-[1.111vw] [&::-webkit-scrollbar]:w-[none] [&::-webkit-scrollbar-thumb]:bg-transparent [&::-webkit-scrollbar-thumb]:w-4 [&::-webkit-scrollbar-thumb]:rounded-full max-md:-mx-4 max-md:px-4"> <div
ref={homeSliderRef}
className="flex max-2xl:flex-wrap max-2xl:justify-center 2xl:gap-[0.556vw] gap-2 md:max-2xl:w-[93.75vw] max-md:flex-row max-md:flex-nowrap max-md:overflow-x-scroll max-md:overflow-y-hidden max-md:justify-start max-md:snap-x max-md:snap-mandatory [&::-webkit-scrollbar]:h-[1.111vw] [&::-webkit-scrollbar]:w-[none] [&::-webkit-scrollbar-thumb]:bg-transparent [&::-webkit-scrollbar-thumb]:w-4 [&::-webkit-scrollbar-thumb]:rounded-full max-md:-mx-4 max-md:px-4"
>
{marasiDriveFeatures.map(({ image, name }) => ( {marasiDriveFeatures.map(({ image, name }) => (
<div key={name} className="relative md:max-2xl:w-[30.208vw] max-md:w-full max-md:max-w-[530px] max-md:flex-shrink-0 max-md:snap-center"> <div
key={name}
className="relative md:max-2xl:w-[30.208vw] max-md:w-full max-md:max-w-[530px] max-md:flex-shrink-0 max-md:snap-center"
>
<img <img
src={image} src={image}
alt={name} alt={name}
className="object-cover object-center 2xl:rounded-[1.667vw] max-md:!aspect-[328/287] max-md:rounded-2xl md:max-2xl:rounded-[3.125vw]" className="object-cover object-center 2xl:rounded-[1.667vw] max-md:!aspect-[328/287] max-md:rounded-2xl md:max-2xl:rounded-[3.125vw]"
/> />
<span className="md:hidden absolute bottom-[16px] text-white text-h5 left-1/2 -translate-x-1/2 text-center text-nowrap">{name}</span> <span className="md:hidden absolute bottom-[16px] text-white text-h5 left-1/2 -translate-x-1/2 text-center text-nowrap">
{name}
</span>
</div> </div>
))} ))}
</div> </div>
<CustomScrollBar containerRef={homeSliderRef} inlinePadding={16} trackStyle="min-md:hidden" thumbStyle="min-md:hidden" /> <CustomScrollBar
containerRef={homeSliderRef}
inlinePadding={16}
trackStyle="min-md:hidden"
thumbStyle="min-md:hidden"
/>
</section> </section>
<section className="bg-white w-full overflow-clip flex flex-col 2xl:gap-[4.444vw] gap-12 2xl:pt-[9.444vw] 2xl:px-[2.222vw] 2xl:pb-[2.222vw] md:max-2xl:pt-[104px] md:max-2xl:px-6 md:max-2xl:pb-8 pt-16 px-4 pb-8"> <section className="bg-white w-full overflow-clip flex flex-col 2xl:gap-[4.444vw] gap-12 2xl:pt-[9.444vw] 2xl:px-[2.222vw] 2xl:pb-[2.222vw] md:max-2xl:pt-[104px] md:max-2xl:px-6 md:max-2xl:pb-8 pt-16 px-4 pb-8">
<div className="flex flex-col 2xl:gap-[2.222vw] md:max-2xl:gap-8 gap-6 items-center"> <div className="flex flex-col 2xl:gap-[2.222vw] md:max-2xl:gap-8 gap-6 items-center">
@@ -165,7 +179,7 @@ function AboutMarasiDrive() {
</p> </p>
</div> </div>
<MarasiDriveInteriorsSlider /> <MarasiDriveInteriorsSlider />
<MarasiDriveInteriorsSliderMobile/> <MarasiDriveInteriorsSliderMobile />
</section> </section>
</div> </div>
<div className="2xl:sticky relative"> <div className="2xl:sticky relative">
@@ -220,22 +234,30 @@ function AboutMarasiDrive() {
trendy cafés, shops, and entertainment options - all within reach.`} trendy cafés, shops, and entertainment options - all within reach.`}
</p> </p>
</div> </div>
<div className="grid grid-cols-6 grid-rows-5 gap-x-[1.111vw] gap-y-[0.556vw] max-2xl:hidden"> {/* <div className="grid grid-cols-6 grid-rows-5 gap-x-[1.111vw] gap-y-[0.556vw] max-2xl:hidden">
{marasiDriveMapCards.map((card) => ( {marasiDriveMapCards.map((card) => (
<MarasiDriveMapCard {...card} key={card.title} /> <MarasiDriveMapCard {...card} key={card.title} />
))} ))}
<div className="col-start-3 col-span-full row-start-1 row-span-full"> <div className="col-start-3 col-span-full row-start-1 row-span-full rounded-[1.667vw] overflow-hidden">
<img <GoogleMap
src="/images/about-complex/marasi-drive/map/map.png" mapCenter={GoogleMapData.marasiDriveDefaultCenter}
alt="" markers={GoogleMapData.markers}
className="object-cover size-full 2xl:rounded-[1.667vw]"
/> />
</div> </div>
</div> */}
<div className="w-full 2xl:aspect-[1376/676] max-2xl:aspect-[720/688] max-md:hidden rounded-[1.667vw] overflow-hidden">
<GoogleMap
mapCenter={GoogleMapData.marasiDriveDefaultCenter}
markers={GoogleMapData.markers}
/>
</div> </div>
<MarasiDriveMapMobile/> <GoogleMapMobile
mapCenter={GoogleMapData.marasiDriveDefaultCenter}
markers={GoogleMapData.markers}
/>
</section> </section>
<section className="bg-white w-full overflow-clip flex items-stretch justify-center gap-[1.111vw] 2xl:px-[10.278vw] 2xl:pt-[9.444vw] 2xl:pb-[15vw] md:max-2xl:py-[104px] md:mx-2xl:px-6 max-md:pt-20 max-md:pb-12 px-4 max-2xl:flex-col-reverse md:max-2xl:gap-[6.25vw] md:max-2xl:px-6"> <section className="bg-white w-full overflow-clip flex items-stretch justify-center gap-[1.111vw] 2xl:px-[10.278vw] 2xl:pt-[9.444vw] 2xl:pb-[15vw] md:max-2xl:py-[104px] md:mx-2xl:px-6 max-md:pt-20 max-md:pb-12 px-4 max-2xl:flex-col-reverse md:max-2xl:gap-[6.25vw] md:max-2xl:px-6">
<div > <div>
<img <img
src="/images/about-complex/marasi-drive/podium.png" src="/images/about-complex/marasi-drive/podium.png"
className="object-cover size-full rounded-[1.667vw] max-md:mt-[48px] max-md:aspect-[328/400] max-md:rounded-[6.667vw] md:max-2xl:rounded-[3.125vw] md:max-2xl:aspect-[720/400]" className="object-cover size-full rounded-[1.667vw] max-md:mt-[48px] max-md:aspect-[328/400] max-md:rounded-[6.667vw] md:max-2xl:rounded-[3.125vw] md:max-2xl:aspect-[720/400]"
@@ -254,7 +276,9 @@ function AboutMarasiDrive() {
</p> </p>
</div> </div>
<div className="space-y-[1.111vw]"> <div className="space-y-[1.111vw]">
<h5 className="text-h5 font-medium max-md:mb-[16px] md:max-2xl:mb-[2.083vw]">Download our brochures</h5> <h5 className="text-h5 font-medium max-md:mb-[16px] md:max-2xl:mb-[2.083vw]">
Download our brochures
</h5>
<div className="space-y-[0.833vw] max-md:space-y-[3.333vw] md:max-2xl:space-y-[1.563vw]"> <div className="space-y-[0.833vw] max-md:space-y-[3.333vw] md:max-2xl:space-y-[1.563vw]">
<BrochureButton <BrochureButton
title={"Main Brochure"} title={"Main Brochure"}
-44
View File
@@ -1,44 +0,0 @@
import { useRef } from "react";
import { marasiDriveMapCards } from "../data/aboutMarasiDrive";
import CustomScrollBar from "./ui/ScrollBar";
function MarasiDriveMapMobile() {
const containerRef = useRef<HTMLDivElement>(null);
return (
<div className="min-2xl:hidden relative flex flex-col">
<div className="max-md:aspect-[328/544] md:max-2xl:aspect[1/1] max-2xl:mb-4">
<img
src="/images/about-complex/marasi-drive/map/map.png"
alt=""
className="object-cover size-full rounded-[6.667vw] md:max-2xl:rounded-[3.125vw]"
/>
</div>
<div
ref={containerRef}
className="flex flex-nowrap overflow-x-scroll scroll-pl-4 gap-x-[16px] overflow-y-hidden justify-start snap-x snap-mandatory [&::-webkit-scrollbar]:h-[1.111vw] [&::-webkit-scrollbar]:w-[none] [&::-webkit-scrollbar-thumb]:bg-[#FFFFFF] [&::-webkit-scrollbar-thumb]:w-4 [&::-webkit-scrollbar-thumb]:rounded-full -mx-4 px-4 "
>
{marasiDriveMapCards.map((card, index) => (
<div key={index} className="snap-start">
<div
className={`rounded-[6.667vw] px-[4.444vw] py-[3.333vw] w-[51.111vw] aspect-[184/122] bg-[#F3F3F2] flex-shrink-0 flex flex-col justify-between relative md:max-2xl:w-[25vw] md:max-2xl:rounded-[3.125vw] md:max-2xl:px-[2.083vw] md:max-2xl:py-[1.563vw]`}
>
<div className="space-y-[0.278vw]">
<p className="text-m">{card.title}</p>
<p className="text-s text-[#73787C]">{`${card.mins} mins`}</p>
</div>
<img
src={card.image}
className="rounded-[0.278vw] size-[13.333vw] object-cover absolute bottom-[4.444vw] right-[3.333vw] md:max-2xl:size-[6.25vw] md:max-2xl:bottom-[1.563vw] md:max-2xl:right-[2.083vw]"
alt={card.title}
/>
</div>
</div>
))}
</div>
<CustomScrollBar containerRef={containerRef} inlinePadding={16} trackStyle="min-2xl:hidden max-2xl:translate-y-5" thumbStyle="min-2xl:hidden"/>
</div>
);
}
export default MarasiDriveMapMobile;
+71
View File
@@ -0,0 +1,71 @@
import IGMapPoi from "../../types/IGMapPoi";
import GoogleMapMarkers from "./GoogleMapMarkers";
import { Map, useMap } from "@vis.gl/react-google-maps";
import { useEffect, useState } from "react";
// import GoogleMapFilterButtons from "./GoogleMapFilterButtons";
interface IGMapProps {
mapCenter: google.maps.LatLngLiteral;
defaultZoom?: number;
markers?: IGMapPoi[];
minZoom?: number;
mobile?: boolean;
mobileActive?: boolean;
}
export type MapFilter = "All" | "Hotels" | "Malls" | "Entertainment" | "Other";
export default function GoogleMap({
mapCenter,
markers,
defaultZoom = 14,
minZoom = 10,
mobile = false,
mobileActive = false,
}: IGMapProps) {
const [mapMarkersFilter, setMapMarkersFilter] = useState<MapFilter>("All");
const [isInteractale, setIsInteractale] = useState(!mobile);
const map = useMap();
useEffect(() => {
if (!map) return;
setIsInteractale(!mobile || (mobile && mobileActive));
if (!mobileActive && mobile) {
map.panTo(mapCenter);
map.setZoom(defaultZoom);
}
}, [mobile, mobileActive, defaultZoom, mapCenter, map]);
useEffect(() => {
if (!mobileActive) setMapMarkersFilter("All");
}, [mobileActive]);
return (
<div
className={`size-[100%] ${
isInteractale ? "pointer-events-auto" : "pointer-events-none"
}`}
>
<Map
mapId={import.meta.env.VITE_GOOGLE_MAP_ID}
defaultCenter={mapCenter}
defaultZoom={defaultZoom}
disableDefaultUI={true}
minZoom={minZoom}
gestureHandling={
mobile ? (mobileActive ? "greedy" : "none") : "cooperative"
}
>
{markers && (
<GoogleMapMarkers data={markers} filter={mapMarkersFilter} />
)}
{/* <GoogleMapFilterButtons
mobile={mobile}
mobileActive={mobileActive}
currentFilter={mapMarkersFilter}
onChange={setMapMarkersFilter}
/> */}
</Map>
</div>
);
}
@@ -0,0 +1,89 @@
import { useState } from "react";
import AllIcon from "../icons/map/AllIcon";
import EntertainmentIcon from "../icons/map/EntertainmentIcon";
import HotelIcon from "../icons/map/HotelIcon";
import MallsIcon from "../icons/map/MallsIcon";
import OtherIcon from "../icons/map/OtherIcon";
import Button from "../ui/Button";
import { AnimatePresence, motion } from "motion/react";
import { MapFilter } from "./GoogleMap";
export default function GoogleMapFilterButtons({
mobile,
mobileActive,
currentFilter,
onChange,
}: {
mobile: boolean;
mobileActive: boolean;
currentFilter: MapFilter;
onChange: (newFilter: MapFilter) => void;
}) {
const filters: MapFilter[] = [
"All",
"Hotels",
"Malls",
"Entertainment",
"Other",
];
const [expanded, setExpanded] = useState(false);
const buttonsVisisble = (expanded && mobile && mobileActive) || !mobile;
const IconsByFilter = {
All: <AllIcon />,
Hotels: <HotelIcon />,
Malls: <MallsIcon />,
Entertainment: <EntertainmentIcon />,
Other: <OtherIcon />,
};
return (
<div
className={`flex gap-[0.556vw] absolute items-center bottom-[1.111vw] left-1/2 -translate-x-1/2 ${
mobile &&
`flex-col gap-[1.111vw] w-full pt-5 translate-y-2 ${
expanded && "backdrop-blur-[1px]"
}`
}`}
>
<AnimatePresence>
{buttonsVisisble &&
filters.map((key) => (
<motion.div
initial={{ translateY: 150, opacity: 0 }}
animate={{ translateY: 0, opacity: 1 }}
exit={{ translateY: 150, opacity: 0 }}
// transition={{ duration: 0.1, delay: index * 0.1 }}
key={key}
>
<Button
onClick={() => onChange(key)}
variant={key === currentFilter ? "cta" : "secondary"}
>
<div className="size-5">{IconsByFilter[key]}</div>
{key}
</Button>
</motion.div>
))}
</AnimatePresence>
{mobile && mobileActive && (
<Button
onClick={() => setExpanded(!expanded)}
variant={expanded ? "cta" : "secondary"}
className="my-4 px-[3.889vw] py-[2.778vw] z-10 h-10 transition-all"
>
{!expanded && <div>{IconsByFilter[currentFilter]}</div>}
{expanded ? (
<span>Apply</span>
) : currentFilter === "All" ? (
<span>Select Category</span>
) : (
<span>{currentFilter}</span>
)}
</Button>
)}
</div>
);
}
@@ -0,0 +1,99 @@
import { useMap } from "@vis.gl/react-google-maps";
import { useEffect, useRef, useState } from "react";
import IGMapPoi from "../../types/IGMapPoi";
import MapMarker from "./MapMarker";
import {
MarkerClusterer,
DefaultRenderer,
Cluster,
Marker,
} from "@googlemaps/markerclusterer";
class CustomMarkerRenderer extends DefaultRenderer {
render({
count,
position,
}: Cluster): google.maps.marker.AdvancedMarkerElement {
const svgElement = document.createElement("div");
svgElement.innerHTML = `
<svg fill="white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="50" height="50">
<circle cx="120" cy="120" opacity="1" r="70" />
</svg>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: black; font-size: 12px; font-weight: bold;">
${count}
</div>`;
const marker = new google.maps.marker.AdvancedMarkerElement({
position,
content: svgElement,
zIndex: Math.max(1000, count),
});
return marker;
}
}
export default function GoogleMapMarkers({
data,
filter,
}: {
data: IGMapPoi[];
filter: string;
}) {
const map = useMap();
const [markers, setMarkers] = useState<{ [key: string]: Marker }>({});
const [selectedMarker, setSelectedMarker] = useState<number | null>(null);
const clusterer = useRef<MarkerClusterer | null>(null);
useEffect(() => {
if (!map) return;
if (!clusterer.current) {
clusterer.current = new MarkerClusterer({
map: map,
renderer: new CustomMarkerRenderer(),
});
}
}, [map]);
useEffect(() => {
clusterer.current?.clearMarkers();
clusterer.current?.addMarkers(Object.values(markers));
}, [markers]);
const setMarkerRef = (marker: Marker | null, key: string) => {
if (marker && markers[key]) return;
if (!marker && !markers[key]) return;
setMarkers((prev) => {
if (prev[key] === marker) return prev;
if (marker) {
return { ...prev, [key]: marker };
} else {
const newMarkers = { ...prev };
delete newMarkers[key];
return newMarkers;
}
});
};
return (
<>
{data.map(
(poi: IGMapPoi, index: number) =>
(filter === poi.type || filter === "All") && (
<MapMarker
key={index}
markerKey={index}
poi={poi}
setMarkerRef={setMarkerRef}
isSelected={selectedMarker === index}
setSelectedMarker={setSelectedMarker}
>
{poi.customMarker}
</MapMarker>
)
)}
</>
);
}
@@ -0,0 +1,101 @@
import { useEffect, useState } from "react";
// import { marasiDriveMapCards } from "../data/aboutMarasiDrive";
// import CustomScrollBar from "./ui/ScrollBar";
// import { GoogleMapData } from "../data/googleMapData";
import GoogleMap from "./GoogleMap";
import Button from "../ui/Button";
import FullScreenIcon from "../icons/FullScreenIcon";
import useModalStore from "../../stores/useModalStore";
import IGMapPoi from "../../types/IGMapPoi";
function GoogleMapMobile({
markers,
mapCenter,
}: {
markers?: IGMapPoi[];
mapCenter: google.maps.LatLngLiteral;
}) {
// const containerRef = useRef<HTMLDivElement>(null);
const [mapActive, setMapActive] = useState(false);
const { modal, setModal } = useModalStore();
useEffect(() => {
if (mapActive) setModal(<MapModal />);
else setModal(null);
}, [mapActive]);
useEffect(() => {
setMapActive(!!modal);
}, [modal]);
const MapModal = () => {
return (
<div
className={`relative max-md:aspect-[360/640] md:max-2xl:aspect[1/1] max-2xl:mb-4 overflow-clip rounded-[4.444vw] h-[calc(100dvh-100px)]`}
>
<GoogleMap
mapCenter={mapCenter}
markers={markers}
mobileActive={true}
mobile={true}
/>
</div>
);
};
return (
<div className="md:hidden relative flex flex-col">
{
<div
className={` relative max-md:aspect-[328/328] scroll-mt-10 md:max-2xl:aspect[1/1] transition-all max-2xl:mb-4 overflow-clip rounded-[4.444vw]`}
>
<GoogleMap
mapCenter={mapCenter}
markers={markers}
mobileActive={false}
mobile={true}
/>
<Button
className={`absolute size-[11.111vw] right-[1.111vw] bottom-[1.111vw] `}
onClick={() => setMapActive(true)}
>
<div className="size-[5.556vw] text-[#0D1922]">
<FullScreenIcon />
</div>
</Button>
</div>
}
{/*
<div
ref={containerRef}
className="flex flex-nowrap overflow-x-scroll scroll-pl-4 gap-x-[16px] overflow-y-hidden justify-start snap-x snap-mandatory [&::-webkit-scrollbar]:h-[1.111vw] [&::-webkit-scrollbar]:w-[none] [&::-webkit-scrollbar-thumb]:bg-[#FFFFFF] [&::-webkit-scrollbar-thumb]:w-4 [&::-webkit-scrollbar-thumb]:rounded-full -mx-4 px-4 "
>
{marasiDriveMapCards.map((card, index) => (
<div key={index} className="snap-start">
<div
className={`rounded-[6.667vw] px-[4.444vw] py-[3.333vw] w-[51.111vw] aspect-[184/122] bg-[#F3F3F2] flex-shrink-0 flex flex-col justify-between relative md:max-2xl:w-[25vw] md:max-2xl:rounded-[3.125vw] md:max-2xl:px-[2.083vw] md:max-2xl:py-[1.563vw]`}
>
<div className="space-y-[0.278vw]">
<p className="text-m">{card.title}</p>
<p className="text-s text-[#73787C]">{`${card.mins} mins`}</p>
</div>
<img
src={card.image}
className="rounded-[0.278vw] size-[13.333vw] object-cover absolute bottom-[4.444vw] right-[3.333vw] md:max-2xl:size-[6.25vw] md:max-2xl:bottom-[1.563vw] md:max-2xl:right-[2.083vw]"
alt={card.title}
/>
</div>
</div>
))}
</div>
<CustomScrollBar
containerRef={containerRef}
inlinePadding={16}
trackStyle="min-2xl:hidden max-2xl:translate-y-5"
thumbStyle="min-2xl:hidden"
/> */}
</div>
);
}
export default GoogleMapMobile;
+67
View File
@@ -0,0 +1,67 @@
import { AdvancedMarker, useMap } from "@vis.gl/react-google-maps";
import type { Marker } from "@googlemaps/markerclusterer";
import { useEffect, useRef, useState } from "react";
import IGMapPoi from "../../types/IGMapPoi";
interface IGMapMarker {
markerKey: number;
poi: IGMapPoi;
setMarkerRef: (marker: Marker | null, key: string) => void | undefined;
children: React.ReactNode;
isSelected: boolean;
setSelectedMarker: React.Dispatch<React.SetStateAction<number | null>>;
}
export default function MapMarker({
poi,
children,
markerKey,
setMarkerRef,
isSelected,
setSelectedMarker,
}: IGMapMarker) {
const map = useMap();
const { location, ignoreClusterization, label } = poi;
const [isHovered, setIsHovered] = useState(false);
const markerRef = useRef(null);
useEffect(() => {
if (!ignoreClusterization) {
setMarkerRef(markerRef.current, markerKey.toString());
}
}, [ignoreClusterization, markerKey, setMarkerRef]);
return (
<AdvancedMarker
key={markerKey}
position={location}
ref={markerRef}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={() => {
map?.panTo({ lat: location.lat, lng: location.lng + 0.001 }); // 0.001 is a small align to fit long labels (generally for mobile devices)
map?.setZoom(17);
setSelectedMarker(markerKey);
}}
>
<div
className={`relative flex items-center gap-x-2 ${
isHovered || isSelected ? "[&>.gmap-img-container]:z-150" : ""
}`}
>
{children}
{label && (
<div
className={`label-container text-black absolute text-s left-0 opacity-0 w-max pointer-events-none transition-[left,opacity] bg-white pl-11 pr-3 py-2.5 rounded-2xl ${
isHovered || isSelected
? "opacity-100 left-[calc(100%-38px)] pointer-events-auto z-140"
: ""
}`}
>
{label}
</div>
)}
</div>
</AdvancedMarker>
);
}
+20
View File
@@ -0,0 +1,20 @@
import { SVGProps } from "react";
const AllIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
width={21}
height={20}
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M6.517 13.97h.028v.024zm0-7.952h.029v.023zm7.971 7.976h.029v.024zm-.029-7.976h.029v.023z"
stroke="currentColor"
strokeWidth={3}
strokeLinejoin="round"
/>
</svg>
);
export default AllIcon;
@@ -0,0 +1,19 @@
import { SVGProps } from "react";
const EntertainmentIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
width={21}
height={20}
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M7.49 6.07a.1.1 0 0 1 .186 0l1.188 3.01a.1.1 0 0 0 .056.057l3.01 1.187a.1.1 0 0 1 0 .186l-3.01 1.188a.1.1 0 0 0-.056.056l-1.188 3.01a.1.1 0 0 1-.186 0l-1.187-3.01a.1.1 0 0 0-.056-.056L3.236 10.51a.1.1 0 0 1 0-.186l3.01-1.187a.1.1 0 0 0 .057-.056zm7.13 4.882a.05.05 0 0 1 .093 0l.77 1.954a.05.05 0 0 0 .029.028l1.953.77a.05.05 0 0 1 0 .093l-1.953.77a.05.05 0 0 0-.028.029l-.77 1.953a.05.05 0 0 1-.094 0l-.77-1.953a.05.05 0 0 0-.028-.028l-1.954-.77a.05.05 0 0 1 0-.094l1.954-.77a.05.05 0 0 0 .028-.028zm-1.25-8.334a.05.05 0 0 1 .093 0l.77 1.954a.05.05 0 0 0 .029.028l1.953.77a.05.05 0 0 1 0 .093l-1.953.77a.05.05 0 0 0-.028.029l-.77 1.953a.05.05 0 0 1-.094 0l-.77-1.953a.05.05 0 0 0-.028-.028l-1.954-.77a.05.05 0 0 1 0-.094l1.954-.77a.05.05 0 0 0 .028-.028z"
stroke="currentColor"
strokeWidth={1.5}
/>
</svg>
);
export default EntertainmentIcon;
+19
View File
@@ -0,0 +1,19 @@
import { SVGProps } from "react";
const HotelIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
width={21}
height={20}
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M15.834 10.247h.333a1 1 0 0 1 1 1v2.19a1 1 0 0 1-1 1H4.834a1 1 0 0 1-1-1v-2.19a1 1 0 0 1 1-1h.333m10.667 0V6.882c0-.579-.448-1.048-1-1.048H10.5m5.334 4.413H10.5m-5.333 0V6.882c0-.579.448-1.048 1-1.048H10.5m-5.333 4.413H10.5m-5.833 4.19v1.397m11.667-1.397v1.397M10.5 10.247V5.834"
stroke="currentColor"
strokeWidth={1.5}
/>
</svg>
);
export default HotelIcon;
+20
View File
@@ -0,0 +1,20 @@
import { SVGProps } from "react";
const MallsIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
width={21}
height={20}
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M8 6.406H6.5a1 1 0 0 0-1 1v7.428a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V7.406a1 1 0 0 0-1-1H13m-5 0v-.572a2.5 2.5 0 0 1 2.5-2.5v0a2.5 2.5 0 0 1 2.5 2.5v.572m-5 0h5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="square"
/>
</svg>
);
export default MallsIcon;
+21
View File
@@ -0,0 +1,21 @@
import { SVGProps } from "react";
const OtherIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
width={21}
height={20}
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M10.5 4.166v11.667M5.45 7.086l10.104 5.834m-10.1 0 10.103-5.833"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default OtherIcon;
+10
View File
@@ -0,0 +1,10 @@
export default function PinIcon() {
return (
<svg fill="none" width={16} height={16}>
<path
d="M6.134 10.5a1 1 0 0 0 1.732 0l5.196-9a1 1 0 0 0-.866-1.5H1.804a1 1 0 0 0-.866 1.5z"
fill="currentColor"
/>
</svg>
);
}
+125
View File
@@ -0,0 +1,125 @@
/* eslint-disable no-loss-of-precision */
import IGMapPoi from "../types/IGMapPoi";
import MarasiMarker from "../../public/images/search/rove_home_marasi_drive.png";
import MarinaMarker from "../../public/images/search/rove_home_dubai_marina.png";
import { marasiDriveMapCards } from "./aboutMarasiDrive";
import PinIcon from "../components/icons/map/PinIcon";
function GetMarkerHTML(name: string, primary: boolean = false) {
const src = !primary
? marasiDriveMapCards.find((item) => item.title === name)?.image
: name;
return (
<>
<div
className={`gmap-img-container z-20 overflow-hidden rounded-full flex justify-center items-center border-2 border-white ${
!primary ? "size-8" : "size-14"
}`}
>
<img src={src} className="" alt="" />
</div>
{/* ADD POINTER FOR PRIMARY POINTS */}
{primary && (
<div className=" size-4 mx-auto text-white absolute -bottom-[20px] left-1/2 -translate-x-1/2">
<PinIcon />
</div>
)}
</>
);
}
export const GoogleMapData: {
marasiDriveDefaultCenter: { lat: number; lng: number };
dubaiMarinaDefaultCenter: { lat: number; lng: number };
markers: IGMapPoi[];
} = {
marasiDriveDefaultCenter: {
lat: 25.183476007744233,
lng: 55.274782084720286,
},
dubaiMarinaDefaultCenter: {
lat: 25.069466431595334,
lng: 55.128736429300375,
},
markers: [
{
//Rove Dubai Marina Hotel
location: { lat: 25.069466431595334, lng: 55.128736429300375 },
type: "Hotels",
ignoreClusterization: true,
customMarker: GetMarkerHTML(MarinaMarker, true),
clickableAreaScale: 3,
label: "Rove Dubai Marina Hotel",
},
{
location: { lat: 25.181371868546396, lng: 55.27515332907251 },
type: "Hotels",
ignoreClusterization: true,
customMarker: GetMarkerHTML(MarasiMarker, true),
clickableAreaScale: 3,
label: "Rove Marasi Drive Hotel",
},
// HOTELS
{
location: { lat: 25.197249056658244, lng: 55.27438894800352 },
type: "Hotels",
customMarker: GetMarkerHTML("Burj Khalifa"),
label: "Burj Khalifa",
},
{
location: { lat: 25.195237825881613, lng: 55.27528478028406 },
type: "Hotels",
customMarker: GetMarkerHTML("Dubai Fountain"),
label: "The Dubai Fountain",
},
{
location: { lat: 25.197748564692787, lng: 55.2802703666763 },
type: "Hotels",
customMarker: GetMarkerHTML("Dubai Mall"),
label: "Dubai Mall",
},
{
location: { lat: 25.195811139364157, lng: 55.27207211658565 },
type: "Hotels",
customMarker: GetMarkerHTML("Dubai Opera"),
label: "Dubai Opera",
},
{
location: { lat: 25.18774242603455, lng: 55.27024382999044 },
type: "Hotels",
customMarker: GetMarkerHTML("Marasi Promenade"),
label: "Marasi Promenade",
},
{
location: { lat: 25.203319687345175, lng: 55.279495993292876 },
type: "Hotels",
customMarker: GetMarkerHTML("Rove Downtown Hotel"),
label: "Rove Downtown Hotel",
},
{
location: { lat: 25.20535369013859, lng: 55.26803667795135 },
type: "Hotels",
customMarker: GetMarkerHTML("Rove City Walk Hotel"),
label: "Rove City Walk Hotel",
},
{
location: { lat: 25.20768362042376, lng: 55.26248238220776 },
type: "Hotels",
customMarker: GetMarkerHTML("City Walk"),
label: "City Walk",
},
{
location: { lat: 25.204504325496778, lng: 55.265693333774536 },
type: "Hotels",
customMarker: GetMarkerHTML("Coca-Cola Arena"),
label: "Coca-Cola Arena",
},
{
location: { lat: 25.26087724994889, lng: 55.372561586751665 },
type: "Hotels",
customMarker: GetMarkerHTML("Dubai International Airport"),
label: "Dubai International Airport",
},
],
};
+9 -6
View File
@@ -1,6 +1,6 @@
// Initialize Eruda for mobile debugging in development // Initialize Eruda for mobile debugging in development
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
import('eruda').then(eruda => eruda.default.init()); import("eruda").then((eruda) => eruda.default.init());
} }
import "./index.css"; import "./index.css";
@@ -24,6 +24,7 @@ import TestPage from "./pages/TestPage.tsx";
import UnitPage from "./pages/UnitPage.tsx"; import UnitPage from "./pages/UnitPage.tsx";
import PopupContainer from "./components/PopupContainer.tsx"; import PopupContainer from "./components/PopupContainer.tsx";
import VirtualTourPage from "./pages/VirtualTourPage.tsx"; import VirtualTourPage from "./pages/VirtualTourPage.tsx";
import { APIProvider } from "@vis.gl/react-google-maps";
const route = createBrowserRouter([ const route = createBrowserRouter([
{ {
@@ -88,10 +89,12 @@ const route = createBrowserRouter([
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<> <>
<QueryClientProvider client={queryClient}> <APIProvider apiKey={import.meta.env.VITE_GOOGLE_MAP_API_KEY}>
<RouterProvider router={route} /> <QueryClientProvider client={queryClient}>
<PopupContainer /> <RouterProvider router={route} />
<ModalContainer /> <PopupContainer />
</QueryClientProvider> <ModalContainer />
</QueryClientProvider>
</APIProvider>
</> </>
); );
+10
View File
@@ -0,0 +1,10 @@
export default interface IGMapPoi {
location: google.maps.LatLngLiteral;
ignoreClusterization?:boolean;
type:"All" | "Hotels" | "Malls" | "Entertainment"| "Other";
// Scale multiplyer for Pin (MapMarker.tsx) component. (To match visible marker size).
clickableAreaScale?:number;
label?:string;
customMarker?: React.ReactNode;
}