Compare commits

..

40 Commits
main ... main

Author SHA1 Message Date
akash
46fb03b392 reposnive text are updated 2026-02-17 11:20:32 +05:30
akash
3456b6e2b5 prevent blank page on refresh for Next.js static export 2025-12-20 15:54:03 +05:30
akash
d833c56ddd about new strucure updated 2025-12-13 15:32:07 +05:30
akash
37984cfea8 new strcure about 2025-12-13 14:47:05 +05:30
akash
005dee4672 new about scoll updated 2025-12-13 13:44:49 +05:30
akash
109941612b about section updated 2025-12-13 11:49:16 +05:30
akash
1b0ac9fbe9 about section 2025-12-12 23:22:08 +05:30
akash
68b535a5d8 about effect 2025-12-12 22:34:28 +05:30
akash
90b3683593 header import corrected 2025-12-12 21:28:21 +05:30
akash
cbb953d622 HeaderClient updated 2025-12-12 18:55:05 +05:30
akash
152022c68f loading issue conetnt top detals card updated 2025-12-12 12:40:34 +05:30
akash
b8a2d91728 Merge branch 'main' of https://git.metatroncube.in/MetatroncubeSoftwareSolutions/sky-and-soil 2025-12-12 09:47:04 +05:30
akash
6981041d45 new content updated 2025-12-12 09:46:27 +05:30
Alaguraj0361
e97cfaffb4 feat: add selenium-webdriver dependency 2025-12-12 09:41:03 +05:30
akash
2e2b776085 loading issue fixed new structure updayed 2025-12-11 23:01:02 +05:30
akash
5bb4efd6b8 about section parllex effect updated 2025-12-10 14:20:01 +05:30
akash
92e2b8e210 Seo updated 2025-12-09 23:29:52 +05:30
akash
12b2e77449 about new iamge updated 2025-12-09 17:47:30 +05:30
akash
3f53ee1ebd Animation & Responsive are updated 2025-12-09 17:30:51 +05:30
akash
ce24e8e7fc faq hover color updated 2025-12-09 14:33:06 +05:30
akash
253a7aa5d2 Mobile responisve are fixed 2025-12-08 23:20:50 +05:30
akash
01d4df18a4 loader issue fixed 2025-12-08 19:24:12 +05:30
akash
d6d14902c9 property detail page map scoll optimmized 2025-12-08 15:10:31 +05:30
akash
0c3f4a1553 About section image updated 2025-12-08 11:39:21 +05:30
akash
2f5aee6d4b About section image update 2025-12-08 11:26:34 +05:30
akash
725590574e header trnsprent increased 2025-12-06 23:30:20 +05:30
akash
3c6a8ac5ff correction are fixed 2025-12-06 21:58:04 +05:30
akash
8a1c1c414d nav issue fixed 2025-12-06 19:03:15 +05:30
akash
2a9404d1e5 Map integration updated 2025-12-05 23:20:44 +05:30
akash
1b4729145a new strucure are updated 2025-12-05 21:43:53 +05:30
akash
debfdc8e2b Property detail page update 2025-12-05 20:23:05 +05:30
akash
04c6ff164e home page about setion new image format update 2025-12-05 15:39:36 +05:30
70f2797703 details page updated 2025-11-29 19:44:34 +05:30
bcd517eb05 meta title and description updated 2025-11-29 19:16:28 +05:30
5e48dad541 hero banner black overlay removed 2025-11-28 13:54:32 +05:30
ace556fa59 banner images updated 2025-11-26 19:08:32 +05:30
0713049d30 pages updated 2025-11-26 19:01:02 +05:30
9249b1fa29 faq tab updated 2025-11-26 13:54:46 +05:30
b75f15bc22 projects images are updated 2025-11-25 22:11:07 +05:30
943f440eae contact integration, images, seo test, sitemap updated 2025-11-25 20:18:07 +05:30
105 changed files with 6755 additions and 1015 deletions

34
build_log_sky.txt Normal file
View File

@ -0,0 +1,34 @@
> antigravity_dev@0.1.0 build
> next build && node script/copy-server-config.cjs
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
▲ Next.js 16.0.3 (Turbopack)
Creating an optimized production build ...
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
✓ Compiled successfully in 2.8s
Running TypeScript ...
Collecting page data using 11 workers ...
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
Generating static pages using 11 workers (0/17) ...
[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
Generating static pages using 11 workers (4/17)
Generating static pages using 11 workers (8/17)
Error occurred prerendering page "/residential-real-estate/godrej-woods". Read more: https://nextjs.org/docs/messages/prerender-error
TypeError: Cannot read properties of undefined (reading 'id')
at q (C:\Users\start\sky-and-soil\.next\server\chunks\ssr\_997a77a8._.js:17:32367) {
digest: '1668255941'
}
Export encountered an error on /residential-real-estate/[slug]/page: /residential-real-estate/godrej-woods, exiting the build.
Next.js build worker exited with code: 1 and signal: null

530
package-lock.json generated
View File

@ -8,11 +8,21 @@
"name": "antigravity_dev",
"version": "0.1.0",
"dependencies": {
"@types/leaflet": "^1.9.21",
"@types/react-google-recaptcha": "^2.1.9",
"axios": "^1.13.2",
"framer-motion": "^12.23.25",
"leaflet": "^1.9.4",
"lucide-react": "^0.554.0",
"next": "16.0.3",
"next-themes": "^0.4.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"tailwindcss-animate": "^1.0.7"
"react-google-recaptcha": "^3.1.0",
"react-leaflet": "^5.0.0",
"sitemap": "^9.0.0",
"tailwindcss-animate": "^1.0.7",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@ -21,6 +31,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.3",
"selenium-webdriver": "^4.38.0",
"tailwindcss": "^4",
"typescript": "^5"
}
@ -278,6 +289,13 @@
"node": ">=6.9.0"
}
},
"node_modules/@bazel/runfiles": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.5.0.tgz",
"integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@emnapi/core": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
@ -1228,6 +1246,17 @@
"node": ">=12.4.0"
}
},
"node_modules/@react-leaflet/core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
"integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
"license": "Hippocratic-2.1",
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@ -1533,6 +1562,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -1547,11 +1582,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/leaflet": {
"version": "1.9.21",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/node": {
"version": "20.19.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@ -1561,7 +1604,6 @@
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@ -1577,6 +1619,24 @@
"@types/react": "^19.2.0"
}
},
"node_modules/@types/react-google-recaptcha": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.9.tgz",
"integrity": "sha512-nT31LrBDuoSZJN4QuwtQSF3O89FVHC4jLhM+NtKEmVF5R1e8OY0Jo4//x2Yapn2aNHguwgX5doAq8Zo+Ehd0ug==",
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/sax": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz",
"integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz",
@ -2203,6 +2263,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@ -2397,6 +2463,12 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -2423,6 +2495,17 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@ -2531,7 +2614,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -2631,6 +2713,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -2645,6 +2739,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2664,7 +2765,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/damerau-levenshtein": {
@ -2789,6 +2889,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@ -2816,7 +2925,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@ -2928,7 +3036,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -2938,7 +3045,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -2976,7 +3082,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@ -2989,7 +3094,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -3604,6 +3708,26 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -3620,11 +3744,53 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/framer-motion": {
"version": "12.23.25",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.25.tgz",
"integrity": "sha512-gUHGl2e4VG66jOcH0JHhuJQr6ZNwrET9g31ZG0xdXzT0CznP7fHX4P8Bcvuc4MiUB90ysNnWX2ukHRIggkl6hQ==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -3685,7 +3851,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@ -3710,7 +3875,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@ -3798,7 +3962,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -3877,7 +4040,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -3890,7 +4052,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@ -3906,7 +4067,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@ -3932,6 +4092,15 @@
"hermes-estree": "0.25.1"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -3942,6 +4111,13 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"dev": true,
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@ -3969,6 +4145,13 @@
"node": ">=0.8.19"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@ -4445,7 +4628,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@ -4524,6 +4706,19 @@
"node": ">=4.0"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dev": true,
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -4554,6 +4749,12 @@
"node": ">=0.10"
}
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@ -4568,6 +4769,16 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
@ -4856,7 +5067,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@ -4875,6 +5085,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.554.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.554.0.tgz",
"integrity": "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@ -4889,7 +5108,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -4919,6 +5137,27 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -4942,6 +5181,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -5091,7 +5345,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -5278,6 +5531,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true,
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -5386,11 +5646,17 @@
"node": ">= 0.8.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true,
"license": "MIT"
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@ -5398,6 +5664,12 @@
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -5438,6 +5710,19 @@
"node": ">=0.10.0"
}
},
"node_modules/react-async-script": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz",
"integrity": "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==",
"license": "MIT",
"dependencies": {
"hoist-non-react-statics": "^3.3.0",
"prop-types": "^15.5.0"
},
"peerDependencies": {
"react": ">=16.4.1"
}
},
"node_modules/react-dom": {
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
@ -5450,10 +5735,59 @@
"react": "^19.2.0"
}
},
"node_modules/react-google-recaptcha": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz",
"integrity": "sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.5.0",
"react-async-script": "^1.2.0"
},
"peerDependencies": {
"react": ">=16.4.1"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-leaflet": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
"integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
"license": "Hippocratic-2.1",
"dependencies": {
"@react-leaflet/core": "^3.0.0"
},
"peerDependencies": {
"leaflet": "^1.9.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true,
"license": "MIT"
},
@ -5597,6 +5931,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"license": "MIT"
},
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@ -5632,12 +5973,44 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sax": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
"license": "BlueOak-1.0.0"
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/selenium-webdriver": {
"version": "4.38.0",
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.38.0.tgz",
"integrity": "sha512-5/UXXFSQmn7FGQkbcpAqvfhzflUdMWtT7QqpEgkFD6Q6rDucxB5EUfzgjmr6JbUj30QodcW3mDXehzoeS/Vy5w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/SeleniumHQ"
},
{
"type": "opencollective",
"url": "https://opencollective.com/selenium"
}
],
"license": "Apache-2.0",
"dependencies": {
"@bazel/runfiles": "^6.3.1",
"jszip": "^3.10.1",
"tmp": "^0.2.5",
"ws": "^8.18.3"
},
"engines": {
"node": ">= 20.0.0"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@ -5697,6 +6070,13 @@
"node": ">= 0.4"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"dev": true,
"license": "MIT"
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@ -5854,6 +6234,40 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sitemap": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.0.tgz",
"integrity": "sha512-J/SU27FJ+I52TcDLKZzPRRVQUMj0Pp1i/HLb2lrkU+hrMLM+qdeRjdacrNxnSW48Waa3UcEOGOdX1+0Lob7TgA==",
"license": "MIT",
"dependencies": {
"@types/node": "^24.9.2",
"@types/sax": "^1.2.1",
"arg": "^5.0.0",
"sax": "^1.4.1"
},
"bin": {
"sitemap": "dist/esm/cli.js"
},
"engines": {
"node": ">=20.19.5",
"npm": ">=10.8.2"
}
},
"node_modules/sitemap/node_modules/@types/node": {
"version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/sitemap/node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -5884,6 +6298,16 @@
"node": ">= 0.4"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@ -6146,6 +6570,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tmp": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.14"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -6356,7 +6790,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unrs-resolver": {
@ -6435,6 +6868,13 @@
"punycode": "^2.1.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -6550,6 +6990,50 @@
"node": ">=0.10.0"
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml2js": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@ -4,16 +4,27 @@
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"build": "next build && node script/copy-server-config.cjs",
"start": "next start",
"lint": "eslint"
"lint": "eslint",
"sitemap": "node script/generate-sitemap.cjs"
},
"dependencies": {
"@types/leaflet": "^1.9.21",
"@types/react-google-recaptcha": "^2.1.9",
"axios": "^1.13.2",
"framer-motion": "^12.23.25",
"leaflet": "^1.9.4",
"lucide-react": "^0.554.0",
"next": "16.0.3",
"next-themes": "^0.4.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"tailwindcss-animate": "^1.0.7"
"react-google-recaptcha": "^3.1.0",
"react-leaflet": "^5.0.0",
"sitemap": "^9.0.0",
"tailwindcss-animate": "^1.0.7",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@ -22,6 +33,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.3",
"selenium-webdriver": "^4.38.0",
"tailwindcss": "^4",
"typescript": "^5"
}

72
public/.htaccess Normal file
View File

@ -0,0 +1,72 @@
# ----------------------------------------------------------------------
# | Best Match for Next.js Static Export with Trailing Slash |
# ----------------------------------------------------------------------
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# --------------------------------------------------------------------
# | 1. Force HTTPS |
# --------------------------------------------------------------------
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# --------------------------------------------------------------------
# | 2. Handle Trailing Slashes (Consistent with next.config.ts) |
# --------------------------------------------------------------------
# If request does NOT end in slash and creates a valid directory, redirect
# This matches Next.js "trailingSlash: true" behavior
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !index.html
RewriteCond %{REQUEST_URI} !/$
RewriteRule ^(.*)$ $1/ [L,R=301]
# --------------------------------------------------------------------
# | 3. Route to index.html within directories |
# --------------------------------------------------------------------
# If the request points to a directory that has an index.html, serve it
RewriteCond %{REQUEST_FILENAME} -d
RewriteCond %{REQUEST_FILENAME}/index.html -f
RewriteRule ^(.*)/$ $1/index.html [L]
# --------------------------------------------------------------------
# | 4. Fallback for SPA-like refreshing (The "White Screen" Fix) |
# --------------------------------------------------------------------
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
# ----------------------------------------------------------------------
# | Performance: Compression (Gzip) |
# ----------------------------------------------------------------------
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json application/xml image/svg+xml
</IfModule>
# ----------------------------------------------------------------------
# | Performance: Browser Caching |
# ----------------------------------------------------------------------
<IfModule mod_expires.c>
ExpiresActive On
# Images: 1 Month
ExpiresByType image/jpg "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
# CSS/JS: 1 Year (Immutable if hashed, safe for Next.js)
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
# Fonts: 1 Year
ExpiresByType font/woff2 "access plus 1 year"
# HTML: Short cache to ensure updates are seen
ExpiresByType text/html "access plus 5 minutes"
</IfModule>

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

4
public/robots.txt Normal file
View File

@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://skyandsoil.metatronnest.com/sitemap.xml

1
public/sitemap.xml Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"><url><loc>https://skyandsoil.metatronnest.com/</loc></url><url><loc>https://skyandsoil.metatronnest.com/about/</loc></url><url><loc>https://skyandsoil.metatronnest.com/projects/</loc></url><url><loc>https://skyandsoil.metatronnest.com/residential-real-estate/</loc></url><url><loc>https://skyandsoil.metatronnest.com/lifestyle/</loc></url><url><loc>https://skyandsoil.metatronnest.com/contact/</loc></url><url><loc>https://skyandsoil.metatronnest.com/compare/</loc></url><url><loc>https://skyandsoil.metatronnest.com/privacy-policy/</loc></url><url><loc>https://skyandsoil.metatronnest.com/terms-of-service/</loc></url><url><loc>https://skyandsoil.metatronnest.com/residential-real-estate/barca-at-godrej-msr-city/</loc></url><url><loc>https://skyandsoil.metatronnest.com/residential-real-estate/godrej-woods/</loc></url><url><loc>https://skyandsoil.metatronnest.com/residential-real-estate/godrej-hoskote/</loc></url><url><loc>https://skyandsoil.metatronnest.com/residential-real-estate/godrej-lakeside-orchard/</loc></url><url><loc>https://skyandsoil.metatronnest.com/residential-real-estate/godrej-tiara/</loc></url></urlset>

67
public/web.config Normal file
View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<directoryBrowse enabled="false" />
<defaultDocument>
<files>
<clear />
<add value="index.html" />
</files>
</defaultDocument>
<staticContent>
<remove fileExtension=".json" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<remove fileExtension=".webp" />
<mimeMap fileExtension=".webp" mimeType="image/webp" />
<remove fileExtension=".woff" />
<mimeMap fileExtension=".woff" mimeType="font/woff" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".woff2" mimeType="font/woff2" />
</staticContent>
<httpErrors errorMode="Custom">
<remove statusCode="404" />
<error statusCode="404" path="/404.html" responseMode="ExecuteURL" />
</httpErrors>
<rewrite>
<rules>
<!-- 1. Force HTTPS -->
<rule name="Force HTTPS" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" ignoreCase="true" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule>
<!-- 2. Trailing Slash Enforcement (Matches next.config.ts) -->
<rule name="Add Trailing Slash" stopProcessing="true">
<match url="(.*[^/])$" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.html$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.xml$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.txt$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.json$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.png$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.jpg$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.jpeg$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.svg$" negate="true" />
</conditions>
<action type="Redirect" url="{R:1}/" redirectType="Permanent" />
</rule>
<!-- 3. Handle HTML Extension Fallback -->
<rule name="HtmlExtension" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}.html" matchType="IsFile" />
</conditions>
<action type="Rewrite" url="{R:1}.html" />
</rule>
</rules>
</rewrite>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>

View File

@ -0,0 +1,30 @@
const fs = require('fs');
const path = require('path');
// Configuration
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
const OUT_DIR = path.join(__dirname, '..', 'out');
const FILES_TO_COPY = ['.htaccess', 'web.config'];
// Ensure out directory exists
if (!fs.existsSync(OUT_DIR)) {
console.warn('⚠ out directory does not exist. Make sure "next build" (static export) has run.');
process.exit(1);
}
FILES_TO_COPY.forEach(filename => {
const source = path.join(PUBLIC_DIR, filename);
const destination = path.join(OUT_DIR, filename);
try {
if (fs.existsSync(source)) {
fs.copyFileSync(source, destination);
console.log(`${filename} copied to out directory`);
} else {
console.warn(`${filename} not found in public directory`);
}
} catch (error) {
console.error(`Error copying ${filename}:`, error.message);
process.exit(1);
}
});

View File

@ -0,0 +1,72 @@
const fs = require("fs");
const path = require("path");
const { SitemapStream, streamToPromise } = require("sitemap");
const hostname = "https://skyandsoil.metatronnest.com";
const addTrailingSlash = true;
const shouldAddSlash = (url) => {
if (url === "/") return false;
if (/\.[a-z0-9]{2,6}(\?.*)?$/i.test(url)) return false;
return true;
};
const formatUrl = (url) => {
if (!url.startsWith("/")) url = "/" + url;
if (addTrailingSlash && shouldAddSlash(url) && !url.endsWith("/")) return url + "/";
if (!addTrailingSlash && url.endsWith("/") && url !== "/") return url.slice(0, -1);
return url;
};
// ✅ Static pages
const staticLinks = [
{ url: "/" },
{ url: "/about/" },
{ url: "/projects/" },
{ url: "/residential-real-estate/" },
{ url: "/lifestyle/" },
{ url: "/contact/" },
{ url: "/compare/" },
{ url: "/privacy-policy/" },
{ url: "/terms-of-service/" },
];
// ✅ Dynamic property pages (manual slugs)
const propertyPages = [
{ slug: "/residential-real-estate/barca-at-godrej-msr-city/" },
{ slug: "/residential-real-estate/godrej-woods/" },
{ slug: "/residential-real-estate/godrej-hoskote/" },
{ slug: "/residential-real-estate/godrej-lakeside-orchard/" },
{ slug: "/residential-real-estate/godrej-tiara/" },
];
const propertyLinks = propertyPages.map(page => ({ url: page.slug }));
// Combine static + property links
const allLinks = [...staticLinks, ...propertyLinks].map(link => ({
url: formatUrl(link.url),
}));
async function generateSitemap() {
try {
const sitemap = new SitemapStream({ hostname });
const writeStream = fs.createWriteStream(path.resolve(__dirname, "../public/sitemap.xml"));
sitemap.pipe(writeStream);
console.log("📦 Writing URLs to sitemap:");
allLinks.forEach(link => {
console.log(" -", hostname + link.url);
sitemap.write(link);
});
sitemap.end();
await streamToPromise(sitemap);
console.log("✅ sitemap.xml created successfully!");
} catch (error) {
console.error("❌ Error creating sitemap.xml:", error);
}
}
generateSitemap();

View File

@ -0,0 +1 @@
Page URL,Image Src,Alt Text,Issue Type
1 Page URL Image Src Alt Text Issue Type

View File

@ -0,0 +1,261 @@
// 🚀 Full SEO + Broken Link + 404 + Accessibility + Image Alt CSV Export
// Run with: node seo_full_audit.js
const { Builder, By } = require("selenium-webdriver");
const chrome = require("selenium-webdriver/chrome");
const axios = require("axios");
const xml2js = require("xml2js");
const fs = require("fs");
const path = require("path");
// CSV file for Image Alt issues
const csvPath = path.join(__dirname, "image_alt_issues.csv");
fs.writeFileSync(csvPath, "Page URL,Image Src,Alt Text,Issue Type\n", "utf8");
// ==========================
// 1⃣ Fetch URLs from sitemap.xml
// ==========================
async function getUrlsFromSitemap(sitemapUrl) {
try {
const res = await axios.get(sitemapUrl);
const parsed = await xml2js.parseStringPromise(res.data);
return parsed.urlset.url.map((u) => u.loc[0]);
} catch (err) {
console.error("❌ Failed to load sitemap:", err.message);
return [];
}
}
// ==========================
// 2⃣ Check HTTP Status
// ==========================
async function checkLinkStatus(url) {
try {
const res = await axios.get(url, {
timeout: 10000,
validateStatus: () => true,
});
if (res.status === 200 && res.data.toLowerCase().includes("page not found")) {
return "Soft 404";
}
return res.status;
} catch (err) {
return err.response ? err.response.status : "❌ No Response";
}
}
// ==========================
// 3⃣ Main SEO + Accessibility + Image Alt Audit
// ==========================
async function checkSEO(url, siteDomain) {
const options = new chrome.Options();
options.addArguments("--headless", "--no-sandbox", "--disable-gpu");
const driver = await new Builder()
.forBrowser("chrome")
.setChromeOptions(options)
.build();
try {
const pageStatus = await checkLinkStatus(url);
if (pageStatus === 404 || pageStatus === "Soft 404") {
console.log(`\n🚫 ${url} → ❌ Page not found (${pageStatus})`);
return;
}
await driver.get(url);
const pageSource = await driver.getPageSource();
// Basic SEO Elements
const title = await driver.getTitle();
const descElem = await driver.findElements(By.css('meta[name="description"]'));
const canonicalElem = await driver.findElements(By.css('link[rel="canonical"]'));
const robotsElem = await driver.findElements(By.css('meta[name="robots"]'));
const viewportElem = await driver.findElements(By.css('meta[name="viewport"]'));
const charset = await driver.findElements(By.css('meta[charset]'));
const htmlTag = await driver.findElement(By.css("html"));
const langAttr = await htmlTag.getAttribute("lang").catch(() => "");
const h1Tags = await driver.findElements(By.css("h1"));
const h2Tags = await driver.findElements(By.css("h2"));
// Meta Description
let descContent = descElem.length > 0 ? await descElem[0].getAttribute("content") : "";
const descLength = descContent.length;
const descStatus =
descLength === 0
? "❌ Missing"
: descLength < 50
? `⚠️ Too short (${descLength})`
: descLength > 160
? `⚠️ Too long (${descLength})`
: "✅ Perfect";
// Title length check
const titleLength = title.length;
const titleStatus =
titleLength === 0
? "❌ Missing"
: titleLength < 30
? `⚠️ Too short (${titleLength})`
: titleLength > 65
? `⚠️ Too long (${titleLength})`
: "✅ Perfect";
// Canonical
const canonicalURL =
canonicalElem.length > 0 ? await canonicalElem[0].getAttribute("href") : "❌ Missing";
// 🖼️ Image Accessibility Audit
const imgs = await driver.findElements(By.css("img"));
let missingAlt = 0;
let emptyAlt = 0;
let duplicateAlt = [];
const altTextMap = new Map();
for (const img of imgs) {
const src = await img.getAttribute("src");
const alt = (await img.getAttribute("alt"))?.trim() ?? null;
if (alt === null) {
missingAlt++;
fs.appendFileSync(csvPath, `"${url}","${src}","","Missing Alt"\n`, "utf8");
continue;
}
if (alt === "") {
emptyAlt++;
fs.appendFileSync(csvPath, `"${url}","${src}","(empty)","Empty Alt"\n`, "utf8");
}
if (altTextMap.has(alt)) {
altTextMap.set(alt, altTextMap.get(alt) + 1);
} else {
altTextMap.set(alt, 1);
}
}
for (const [altText, count] of altTextMap.entries()) {
if (altText && count > 1) {
duplicateAlt.push({ altText, count });
fs.appendFileSync(
csvPath,
`"${url}","","${altText}","Duplicate Alt (${count} times)"\n`,
"utf8"
);
}
}
// Detect tracking & schema tags
const hasGTM = pageSource.includes("googletagmanager.com/gtm.js");
const hasClarity = pageSource.includes("clarity.ms/tag");
const hasFBPixel = pageSource.includes("fbevents.js") || pageSource.includes("fbq(");
const hasAnalytics = pageSource.includes("www.googletagmanager.com/gtag/js");
const ogTags = await driver.findElements(By.css("meta[property^='og:']"));
const twitterTags = await driver.findElements(By.css("meta[name^='twitter:']"));
const schemaScripts = await driver.findElements(By.css('script[type="application/ld+json"]'));
// Links check
const anchorTags = await driver.findElements(By.css("a[href]"));
const brokenLinks = [];
for (const a of anchorTags) {
const href = await a.getAttribute("href");
if (!href || href.startsWith("#") || href.startsWith("mailto:")) continue;
const fullUrl = href.startsWith("http")
? href
: `${siteDomain}${href.startsWith("/") ? href : `/${href}`}`;
if (fullUrl.includes(siteDomain)) {
const status = await checkLinkStatus(fullUrl);
if (status === 404 || status === "Soft 404" || status === "❌ No Response") {
brokenLinks.push({ link: fullUrl, status });
}
}
}
// Lazy loading check
const images = await driver.findElements(By.css("img, video, iframe"));
const lazyLoadCount = await Promise.all(
images.map(async (img) => {
const loading = await img.getAttribute("loading");
return loading === "lazy";
})
);
const lazyLoaded = lazyLoadCount.filter((v) => v).length;
// Console Summary
console.log(`\n🔍 Checking: ${url}`);
console.log("-------------------------------------------");
console.log("Title:", titleStatus);
console.log("Meta Description:", descStatus);
console.log("Canonical URL:", canonicalURL);
console.log("Meta Robots:", robotsElem.length > 0 ? "✅ Found" : "⚠️ Missing");
console.log("Viewport:", viewportElem.length > 0 ? "✅ Found" : "⚠️ Missing");
console.log("Charset:", charset.length > 0 ? "✅ Found" : "❌ Missing");
console.log("HTML lang:", langAttr ? `${langAttr}` : "⚠️ Missing");
console.log("H1 Tags:", h1Tags.length > 0 ? `${h1Tags.length}` : "❌ Missing");
console.log("H2 Tags:", h2Tags.length > 0 ? ` ${h2Tags.length}` : "⚠️ None");
console.log("Images:", imgs.length);
console.log(
"Missing Alt:",
missingAlt > 0 ? `${missingAlt}` : "✅ None"
);
console.log(
"Empty Alt:",
emptyAlt > 0 ? `⚠️ ${emptyAlt}` : "✅ None"
);
console.log(
"Duplicate Alt:",
duplicateAlt.length > 0 ? `⚠️ ${duplicateAlt.length}` : "✅ None"
);
console.log("Lazy Loaded Images:", lazyLoaded > 0 ? `${lazyLoaded}` : "⚠️ None");
console.log("Open Graph Tags:", ogTags.length > 0 ? "✅ Found" : "⚠️ Missing");
console.log("Twitter Tags:", twitterTags.length > 0 ? "✅ Found" : "⚠️ Missing");
console.log("Schema Markup:", schemaScripts.length > 0 ? "✅ Found" : "⚠️ Missing");
console.log("Google Analytics:", hasAnalytics ? "✅ Found" : "⚠️ Missing");
console.log("GTM:", hasGTM ? "✅ Found" : "⚠️ Missing");
console.log("Clarity:", hasClarity ? "✅ Found" : "⚠️ Missing");
console.log("Facebook Pixel:", hasFBPixel ? "✅ Found" : "⚠️ Missing");
if (brokenLinks.length > 0) {
console.log("\n❌ Broken Links:");
brokenLinks.forEach((b) => console.log(`${b.link} [${b.status}]`));
} else {
console.log("✅ No broken links found.");
}
} catch (err) {
console.error(`❌ Error on ${url}:`, err.message);
} finally {
await driver.quit();
}
}
// ==========================
// 4⃣ Run Full Site Audit
// ==========================
(async () => {
const sitemapUrl = "http://localhost:3000/sitemap.xml"; // your sitemap
const siteDomain = "https://skyandsoil.metatronnest.com"; // your domain
console.log("📄 Fetching URLs from sitemap...");
const urls = await getUrlsFromSitemap(sitemapUrl);
if (urls.length === 0) {
console.error("❌ No URLs found in sitemap.");
return;
}
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
console.log("🚀 Starting Full SEO + Accessibility + Broken Link Audit...");
for (const url of urls) {
await checkSEO(url, siteDomain);
}
console.log("\n✅ Full SEO Audit Completed!");
console.log(`📁 CSV Report: ${csvPath}`);
})();

View File

@ -1,4 +1,3 @@
import Header from "@/components/Header";
import InnerBanner from "@/components/InnerBanner";
import About from "@/components/About";
import WhyChooseUs from "@/components/WhyChooseUs";
@ -14,7 +13,6 @@ export const metadata: Metadata = {
export default function AboutPage() {
return (
<main className="min-h-screen bg-white dark:bg-black">
<Header />
<InnerBanner
title="About Us"
subtitle="Discover our journey in creating exceptional living spaces"
@ -22,6 +20,7 @@ export default function AboutPage() {
{ label: "Home", href: "/" },
{ label: "About" }
]}
backgroundImage="/assets/images/about/about-banner.webp"
/>
<div>
<About />

View File

@ -0,0 +1,16 @@
import BengaluruPropertiesClientWrapper from "@/components/BengaluruPropertiesClientWrapper";
import { Metadata } from "next";
import { Suspense } from "react";
export const metadata: Metadata = {
title: "Property for Sale in Bengaluru | Sky and Soil",
description: "Browse our exclusive collection of premium apartments, villas, and plots in Bengaluru.",
};
export default function PropertyForSalePage() {
return (
<Suspense fallback={<div className="min-h-screen bg-gray-50 flex items-center justify-center">Loading...</div>}>
<BengaluruPropertiesClientWrapper />
</Suspense>
);
}

View File

@ -1,5 +1,5 @@
import Header from "@/components/Header";
import InnerBanner from "@/components/InnerBanner";
import ContactCards from "@/components/ContactCards";
import ContactCTA from "@/components/ContactCTA";
import Footer from "@/components/Footer";
import { Metadata } from "next";
@ -12,7 +12,6 @@ export const metadata: Metadata = {
export default function ContactPage() {
return (
<main className="min-h-screen bg-white dark:bg-black">
<Header />
<InnerBanner
title="Contact Us"
subtitle="Get in touch with our team for any inquiries"
@ -20,9 +19,13 @@ export default function ContactPage() {
{ label: "Home", href: "/" },
{ label: "Contact" }
]}
backgroundImage="/assets/images/about/contact-banner.webp"
/>
<div>
<ContactCTA />
<div className="relative bg-[#f3f1e6] dark:bg-gray-900 pt-20">
<ContactCards />
<div className="-mt-12">
<ContactCTA />
</div>
</div>
<Footer />
</main>

View File

@ -17,6 +17,7 @@
--color-noir-slate: #1C1C1C;
--color-porcelain-white: #FFFCF0;
--color-limestone: #F3F1E6;
--color-gray-900: lab(8.11897% .811279 -12.254);
--font-sans: var(--font-inter);

View File

@ -8,18 +8,33 @@ const inter = Inter({
});
export const metadata: Metadata = {
metadataBase: new URL("https://skyandsoil.metatronnest.com"),
title: {
default: "Sky and Soil | Premium Real Estate in North Bengaluru",
template: "%s | Sky and Soil"
},
description: "Discover luxury apartments, villas, and plots in North Bengaluru. Sky and Soil connects you with nature-inspired living spaces and Godrej Properties.",
keywords: ["Real Estate", "Bengaluru", "Luxury Homes", "Godrej Properties", "North Bengaluru", "Villas", "Apartments"],
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
},
},
alternates: {
canonical: "./",
},
};
import { ThemeProvider } from "@/components/ThemeProvider";
import { CompareProvider } from "@/context/CompareContext";
import CompareBar from "@/components/CompareBar";
import MouseAnimation from "@/components/MouseAnimation";
import HeaderClient from "@/components/HeaderClient";
// import HeaderWrapper from "@/components/HeaderWrapper";
export default function RootLayout({
children,
@ -28,8 +43,16 @@ export default function RootLayout({
}>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
{/* ✅ Meta Robots */}
<meta name="robots" content="index, follow" />
{/* Canonical handled by metadata above */}
</head>
<body
className={`${inter.variable} antialiased`}
suppressHydrationWarning
>
<ThemeProvider
attribute="class"
@ -38,9 +61,10 @@ export default function RootLayout({
disableTransitionOnChange
>
<CompareProvider>
<HeaderClient />
{children}
<CompareBar />
<MouseAnimation />
{/* <MouseAnimation /> */}
</CompareProvider>
</ThemeProvider>
</body>

View File

@ -1,4 +1,3 @@
import Header from "@/components/Header";
import InnerBanner from "@/components/InnerBanner";
import Lifestyle from "@/components/Lifestyle";
import Footer from "@/components/Footer";
@ -12,7 +11,6 @@ export const metadata: Metadata = {
export default function LifestylePage() {
return (
<main className="min-h-screen bg-white dark:bg-black">
<Header />
<InnerBanner
title="Lifestyle"
subtitle="Experience the perfect blend of comfort and luxury"
@ -20,6 +18,7 @@ export default function LifestylePage() {
{ label: "Home", href: "/" },
{ label: "Lifestyle" }
]}
backgroundImage="/assets/images/about/lifestyle-banner.webp"
/>
<div>
<Lifestyle />

33
src/app/loading.tsx Normal file
View File

@ -0,0 +1,33 @@
export default function Loading() {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
backgroundColor: "#f5f5f5",
}}
>
<div className="loader"></div>
<style>{`
.loader {
border: 4px solid #e0e0e0;
border-top: 4px solid #4a90e2; /* Matching a standard blue/brand color or adjust if needed */
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`}</style>
</div>
);
}

View File

@ -1,4 +1,3 @@
import Header from "@/components/Header";
import Hero from "@/components/Hero";
import About from "@/components/About";
import WhyChooseUs from "@/components/WhyChooseUs";
@ -19,7 +18,6 @@ export const metadata: Metadata = {
export default function Home() {
return (
<main className="min-h-screen bg-white dark:bg-black">
<Header />
<Hero />
<About />
<WhyChooseUs />

View File

@ -0,0 +1,162 @@
import Link from "next/link";
import Footer from "@/components/Footer";
import InnerBanner from "@/components/InnerBanner";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Privacy Policy | Your Data, Security & Rights",
description: "Understand how your personal information is collected, used, and protected. Our Privacy Policy ensures transparency and your data rights.",
};
export default function PrivacyPolicy() {
return (
<div className="min-h-screen bg-white dark:bg-black">
{/* Inner Banner */}
<InnerBanner
title="Privacy Policy"
subtitle="Learn how we protect and manage your data"
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "Privacy Policy" }
]}
backgroundImage="/assets/images/about/privacy-banner.webp"
/>
{/* Page Content */}
<div className="max-w-4xl mx-auto px-6 py-20">
<h1 className="text-4xl md:text-5xl font-bold text-foreground mb-8">
Privacy Policy
</h1>
<div className="prose prose-lg dark:prose-invert max-w-none">
<p className="text-gray-600 dark:text-gray-400 mb-6">
Last updated: {new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
</p>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">1. Introduction</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Welcome to Sky and Soil. We respect your privacy and are committed to protecting your personal data.
This privacy policy will inform you about how we look after your personal data when you visit our website
and tell you about your privacy rights and how the law protects you.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">2. Information We Collect</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
We may collect, use, store and transfer different kinds of personal data about you which we have grouped together as follows:
</p>
<ul className="list-disc pl-6 text-gray-600 dark:text-gray-400 space-y-2">
<li><strong>Identity Data:</strong> includes first name, last name, username or similar identifier.</li>
<li><strong>Contact Data:</strong> includes email address, telephone numbers, and mailing address.</li>
<li><strong>Technical Data:</strong> includes internet protocol (IP) address, browser type and version, time zone setting and location, browser plug-in types and versions, operating system and platform.</li>
<li><strong>Usage Data:</strong> includes information about how you use our website and services.</li>
<li><strong>Marketing and Communications Data:</strong> includes your preferences in receiving marketing from us and your communication preferences.</li>
</ul>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">3. How We Use Your Information</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
We will only use your personal data when the law allows us to. Most commonly, we will use your personal data in the following circumstances:
</p>
<ul className="list-disc pl-6 text-gray-600 dark:text-gray-400 space-y-2">
<li>To provide and maintain our services</li>
<li>To notify you about changes to our services</li>
<li>To provide customer support</li>
<li>To gather analysis or valuable information so that we can improve our services</li>
<li>To monitor the usage of our services</li>
<li>To detect, prevent and address technical issues</li>
<li>To provide you with news, special offers and general information about other goods, services and events which we offer</li>
</ul>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">4. Data Security</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
We have put in place appropriate security measures to prevent your personal data from being accidentally lost,
used or accessed in an unauthorized way, altered or disclosed. In addition, we limit access to your personal data
to those employees, agents, contractors and other third parties who have a business need to know.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">5. Data Retention</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
We will only retain your personal data for as long as necessary to fulfil the purposes we collected it for,
including for the purposes of satisfying any legal, accounting, or reporting requirements.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">6. Your Legal Rights</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Under certain circumstances, you have rights under data protection laws in relation to your personal data, including the right to:
</p>
<ul className="list-disc pl-6 text-gray-600 dark:text-gray-400 space-y-2">
<li>Request access to your personal data</li>
<li>Request correction of your personal data</li>
<li>Request erasure of your personal data</li>
<li>Object to processing of your personal data</li>
<li>Request restriction of processing your personal data</li>
<li>Request transfer of your personal data</li>
<li>Right to withdraw consent</li>
</ul>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">7. Cookies</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Our website uses cookies to distinguish you from other users of our website. This helps us to provide you
with a good experience when you browse our website and also allows us to improve our site.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">8. Third-Party Links</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Our website may include links to third-party websites, plug-ins and applications. Clicking on those links
or enabling those connections may allow third parties to collect or share data about you. We do not control
these third-party websites and are not responsible for their privacy statements.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">9. Contact Us</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
If you have any questions about this Privacy Policy, please contact us:
</p>
<ul className="list-none text-gray-600 dark:text-gray-400 space-y-2">
<li>Email: hello@skyandsoil.com</li>
<li>Phone: +91 80 1234 5678</li>
<li>Address: Bangalore, Karnataka</li>
</ul>
</section>
</div>
<div className="mt-12">
<Link
href="/"
className="inline-flex items-center text-primary hover:underline"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5 mr-2"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
Back to Home
</Link>
</div>
</div>
{/* Footer */}
<Footer />
</div>
);
}

View File

@ -1,11 +1,29 @@
import PropertiesClient from "@/components/PropertiesClient";
import Footer from "@/components/Footer";
import InnerBanner from "@/components/InnerBanner";
import ProjectsContent from "@/components/ProjectsContent";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Our Properties | Sky and Soil Real Estate",
description: "Browse our exclusive collection of premium apartments, villas, and plots in North Bengaluru. Find your perfect home with Sky and Soil.",
title: "Our Projects | Premium Residential & Commercial Spaces",
description: "Explore premium residential and commercial spaces built for comfort, convenience, and superior lifestyle experiences.",
};
export default function PropertiesPage() {
return <PropertiesClient />;
export default function ProjectsPage() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-black">
<InnerBanner
title="Our Projects"
subtitle="Explore our diverse portfolio of real estate projects"
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "Projects" }
]}
/>
<ProjectsContent />
<Footer />
</div>
);
}

View File

@ -1,294 +0,0 @@
import { use } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import Image from "next/image";
import PropertyGallery from "@/components/PropertyGallery";
import PropertyNav from "@/components/PropertyNav";
import InnerBanner from "@/components/InnerBanner";
import { properties } from "@/data/properties";
import { notFound } from "next/navigation";
import { Metadata } from "next";
// Required for static site generation with dynamic routes
export function generateStaticParams() {
return properties.map((property) => ({
id: property.id.toString(),
}));
}
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const resolvedParams = await params;
const property = properties.find(p => p.id === parseInt(resolvedParams.id));
if (!property) {
return {
title: "Property Not Found | Sky and Soil",
description: "The requested property could not be found."
};
}
return {
title: `${property.title} | Sky and Soil Real Estate`,
description: `Explore ${property.title} in ${property.location}. ${property.overview.bhk} ${property.category} starting at ${property.price}.`,
};
}
const sections = [
{ id: "overview", label: "Overview" },
{ id: "about", label: "About" },
{ id: "amenities", label: "Amenities" },
{ id: "floor-plans", label: "Floor Plans" },
{ id: "location", label: "Location" },
{ id: "pricing", label: "Pricing" },
];
export default async function PropertyDetailPage({ params }: { params: Promise<{ id: string }> }) {
const resolvedParams = await params;
const property = properties.find(p => p.id === parseInt(resolvedParams.id));
if (!property) {
notFound();
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-black">
<Header />
<InnerBanner
title={property.title}
subtitle={property.location}
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "Properties", href: "/projects" },
{ label: property.title }
]}
backgroundImage={property.image}
/>
<div>
{/* Sticky Navigation */}
<PropertyNav sections={sections} />
<div className="max-w-7xl mx-auto px-6 py-8">
{/* Property Header */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 mb-8 shadow-sm border border-gray-200 dark:border-gray-800">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<div>
<div className="flex items-center gap-3 mb-3">
<h1 className="text-3xl md:text-4xl font-bold text-foreground">{property.title}</h1>
<span className="px-4 py-1.5 bg-gradient-to-r from-green-500 to-emerald-500 text-white rounded-full text-sm font-semibold shadow-md">
{property.status}
</span>
</div>
<div className="flex items-center text-gray-600 dark:text-gray-400">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
{property.location}
</div>
</div>
<div className="text-right">
<div className="text-sm text-gray-500 dark:text-gray-400 mb-1">Starting from</div>
<div className="text-4xl font-bold bg-gradient-to-r from-primary to-blue-600 bg-clip-text text-transparent">
{property.price}
</div>
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-6 border-t border-gray-200 dark:border-gray-800">
<div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-2xl font-bold text-primary mb-1">{property.overview.bhk}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Configuration</div>
</div>
<div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-2xl font-bold text-primary mb-1">{property.overview.size}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Area</div>
</div>
<div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-2xl font-bold text-primary mb-1">{property.overview.possession}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Possession</div>
</div>
<div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-2xl font-bold text-primary mb-1">{property.overview.totalUnits}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Total Units</div>
</div>
</div>
</div>
{/* Image Gallery */}
<PropertyGallery images={property.images} title={property.title} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main Content */}
<div className="lg:col-span-2 space-y-8">
{/* Overview Section */}
<div id="overview" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
<div className="w-1 h-8 bg-primary rounded-full"></div>
Overview
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-gray-500 dark:text-gray-400 text-sm mb-2">Property Type</div>
<div className="text-lg font-semibold text-foreground">{property.category}</div>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-gray-500 dark:text-gray-400 text-sm mb-2">RERA Status</div>
<div className="text-lg font-semibold text-green-600">Approved</div>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-gray-500 dark:text-gray-400 text-sm mb-2">Availability</div>
<div className="text-lg font-semibold text-foreground">Available</div>
</div>
</div>
</div>
{/* About Section */}
<div id="about" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
<div className="w-1 h-8 bg-primary rounded-full"></div>
About this Property
</h2>
<p className="text-gray-700 dark:text-gray-300 leading-relaxed text-lg">{property.description}</p>
</div>
{/* Amenities Section */}
<div id="amenities" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
<div className="w-1 h-8 bg-primary rounded-full"></div>
Amenities
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{property.amenities.map((amenity, idx) => (
<div key={idx} className="flex items-center gap-3 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-xl hover:shadow-md transition-shadow">
<div className="w-10 h-10 bg-primary rounded-full flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="text-gray-700 dark:text-gray-300 font-medium">{amenity}</span>
</div>
))}
</div>
</div>
{/* Floor Plans Section */}
<div id="floor-plans" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
<div className="w-1 h-8 bg-primary rounded-full"></div>
Floor Plans
</h2>
<div className="bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900 rounded-2xl p-12 text-center">
<svg className="w-20 h-20 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p className="text-gray-600 dark:text-gray-400 text-lg mb-4">Floor plans available on request</p>
<button className="px-6 py-3 bg-primary text-white rounded-lg hover:bg-blue-700 transition-colors font-medium">
Request Floor Plans
</button>
</div>
</div>
{/* Location Section */}
<div id="location" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
<div className="w-1 h-8 bg-primary rounded-full"></div>
Location
</h2>
<div className="bg-gray-100 dark:bg-gray-800 rounded-xl p-8 text-center">
<svg className="w-16 h-16 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
<p className="text-gray-600 dark:text-gray-400">Interactive map coming soon</p>
</div>
</div>
{/* Pricing Section */}
<div id="pricing" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
<div className="w-1 h-8 bg-primary rounded-full"></div>
Pricing Details
</h2>
<div className="space-y-4">
<div className="flex justify-between items-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<span className="text-gray-700 dark:text-gray-300">Base Price</span>
<span className="text-xl font-bold text-primary">{property.price}</span>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">* Prices are subject to change. Please contact us for the latest pricing and offers.</p>
</div>
</div>
</div>
{/* Sidebar */}
<div className="lg:col-span-1">
<div className="sticky top-32 space-y-6">
{/* Contact Form */}
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 shadow-lg">
<h3 className="text-xl font-bold text-foreground mb-4">Get in Touch</h3>
<form className="space-y-4">
<input
type="text"
placeholder="Your Name"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground transition-all"
/>
<input
type="email"
placeholder="Email Address"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground transition-all"
/>
<input
type="tel"
placeholder="Phone Number"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground transition-all"
/>
<textarea
rows={4}
placeholder="Message (Optional)"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground transition-all"
/>
<button
type="submit"
className="w-full bg-gradient-to-r from-primary to-blue-600 text-white py-3 rounded-lg font-semibold hover:shadow-lg transition-all transform hover:scale-105"
>
Request Callback
</button>
</form>
</div>
{/* Quick Actions */}
<div className="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 rounded-2xl p-6 border border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-bold text-foreground mb-4">Quick Actions</h3>
<div className="space-y-3">
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg hover:shadow-md transition-all text-foreground">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
</svg>
Share Property
</button>
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg hover:shadow-md transition-all text-foreground">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
Save to Wishlist
</button>
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg hover:shadow-md transition-all text-foreground">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
</svg>
Print Details
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<Footer />
</div>
);
}

View File

@ -0,0 +1,39 @@
import { notFound } from "next/navigation";
import PropertyDetailClient from "@/components/PropertyDetailClient";
import { properties } from "@/data/properties";
import { Metadata } from "next";
// Generate static params for all properties
export async function generateStaticParams() {
return properties.map((property) => ({
slug: property.slug,
}));
}
// Generate metadata for SEO
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const property = properties.find((p) => p.slug === slug);
if (!property) {
return {
title: "Property Not Found",
};
}
return {
title: property.metaTitle || property.title,
description: property.metaDescription || property.description,
};
}
export default async function PropertyDetailPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const property = properties.find((p) => p.slug === slug);
if (!property) {
notFound();
}
return <PropertyDetailClient property={property} />;
}

View File

@ -0,0 +1,11 @@
import PropertiesClient from "@/components/PropertiesClient";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Residential Real Estate | Sky and Soil",
description: "Browse our exclusive collection of premium apartments, villas, and plots in North Bengaluru.",
};
export default function ResidentialRealEstatePage() {
return <PropertiesClient />;
}

View File

@ -0,0 +1,173 @@
import Link from "next/link";
import Footer from "@/components/Footer";
import InnerBanner from "@/components/InnerBanner";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Terms of Service | Website Use & User Guidelines",
description: "Read the Terms of Service to know your rights, responsibilities, and the rules for using our website safely and effectively.",
};
export default function TermsOfService() {
return (
<div className="min-h-screen bg-white dark:bg-black">
<InnerBanner
title="Terms of Service"
subtitle="Please read our terms and conditions carefully"
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "Terms of Service" }
]}
backgroundImage="/assets/images/about/terms-banner.webp"
/>
<div className="max-w-4xl mx-auto px-6 py-20">
<h1 className="text-4xl md:text-5xl font-bold text-foreground mb-8">
Terms of Service
</h1>
<div className="prose prose-lg dark:prose-invert max-w-none">
<p className="text-gray-600 dark:text-gray-400 mb-6">
Last updated: {new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
</p>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">1. Agreement to Terms</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
By accessing and using the Sky and Soil website, you accept and agree to be bound by the terms and
provision of this agreement. If you do not agree to abide by the above, please do not use this service.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">2. Use License</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Permission is granted to temporarily access the materials (information or software) on Sky and Soil's
website for personal, non-commercial transitory viewing only. This is the grant of a license, not a
transfer of title, and under this license you may not:
</p>
<ul className="list-disc pl-6 text-gray-600 dark:text-gray-400 space-y-2">
<li>Modify or copy the materials</li>
<li>Use the materials for any commercial purpose or for any public display (commercial or non-commercial)</li>
<li>Attempt to decompile or reverse engineer any software contained on Sky and Soil's website</li>
<li>Remove any copyright or other proprietary notations from the materials</li>
<li>Transfer the materials to another person or "mirror" the materials on any other server</li>
</ul>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">3. Property Information Disclaimer</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
All property information, including but not limited to descriptions, photographs, floor plans, pricing,
and availability, is provided for informational purposes only and is subject to change without notice.
While we strive to ensure accuracy, we make no warranties or representations regarding the completeness
or accuracy of such information.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">4. User Responsibilities</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
As a user of this website, you agree to:
</p>
<ul className="list-disc pl-6 text-gray-600 dark:text-gray-400 space-y-2">
<li>Provide accurate and complete information when submitting inquiries or contact forms</li>
<li>Not use the website for any unlawful purpose or to solicit others to perform unlawful acts</li>
<li>Not infringe on the intellectual property rights of others</li>
<li>Not transmit any viruses, malware, or other malicious code</li>
<li>Not attempt to gain unauthorized access to any portion of the website</li>
</ul>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">5. Intellectual Property</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
The content, organization, graphics, design, compilation, magnetic translation, digital conversion,
and other matters related to the website are protected under applicable copyrights, trademarks, and
other proprietary rights. The copying, redistribution, use, or publication by you of any such matters
or any part of the website is strictly prohibited.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">6. Limitation of Liability</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
In no event shall Sky and Soil or its suppliers be liable for any damages (including, without limitation,
damages for loss of data or profit, or due to business interruption) arising out of the use or inability
to use the materials on Sky and Soil's website, even if Sky and Soil or a Sky and Soil authorized
representative has been notified orally or in writing of the possibility of such damage.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">7. Accuracy of Materials</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
The materials appearing on Sky and Soil's website could include technical, typographical, or photographic
errors. Sky and Soil does not warrant that any of the materials on its website are accurate, complete, or
current. Sky and Soil may make changes to the materials contained on its website at any time without notice.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">8. Links to Third-Party Sites</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Sky and Soil has not reviewed all of the sites linked to its website and is not responsible for the
contents of any such linked site. The inclusion of any link does not imply endorsement by Sky and Soil
of the site. Use of any such linked website is at the user's own risk.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">9. Modifications to Terms</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Sky and Soil may revise these terms of service for its website at any time without notice. By using
this website you are agreeing to be bound by the then current version of these terms of service.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">10. Governing Law</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
These terms and conditions are governed by and construed in accordance with the laws of India, and you
irrevocably submit to the exclusive jurisdiction of the courts in Bangalore, Karnataka.
</p>
</section>
<section className="mb-8">
<h2 className="text-2xl font-semibold text-foreground mb-4">11. Contact Information</h2>
<p className="text-gray-600 dark:text-gray-400 mb-4">
If you have any questions about these Terms of Service, please contact us:
</p>
<ul className="list-none text-gray-600 dark:text-gray-400 space-y-2">
<li>Email: hello@skyandsoil.com</li>
<li>Phone: +91 80 1234 5678</li>
<li>Address: Bangalore, Karnataka</li>
</ul>
</section>
</div>
<div className="mt-12">
<Link
href="/"
className="inline-flex items-center text-primary hover:underline"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5 mr-2"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
Back to Home
</Link>
</div>
</div>
<Footer />
</div>
);
}

View File

@ -1,45 +1,11 @@
"use client";
import { useState, useRef, MouseEvent } from "react";
import Image from "next/image";
import Link from "next/link";
import { FloatingHouse, RotatingKey, GrowingBuilding } from "./PropertyAnimations";
import { motion } from "framer-motion";
import styles from "@/styles/aboutSection.module.css";
export default function About() {
const [rotation, setRotation] = useState({ x: -10, y: 0 });
const [isHovering, setIsHovering] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const width = rect.width;
const height = rect.height;
// Calculate rotation based on mouse position
// Y-axis rotation (horizontal mouse movement): -180 to 180 degrees
const rotateY = ((x / width) * 360) - 180;
// X-axis rotation (vertical mouse movement): -90 (top view) to 90 (bottom view)
// Inverting Y so top of screen corresponds to seeing the top face
const rotateX = -(((y / height) * 180) - 90);
setRotation({ x: rotateX, y: rotateY });
};
const handleMouseEnter = () => {
setIsHovering(true);
};
const handleMouseLeave = () => {
setIsHovering(false);
setRotation({ x: -10, y: 0 }); // Reset to default angle
};
return (
<section id="about" className="py-24 bg-white dark:bg-black relative overflow-hidden">
@ -55,9 +21,15 @@ export default function About() {
</div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
{/* Text Content */}
<div className="space-y-8 z-10">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
{/* Text Content - Takes 5 columns */}
<motion.div
className="space-y-8 z-10 lg:col-span-5"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.7 }}
>
<h2 className="text-4xl font-bold tracking-tight text-foreground">
Where the Sky Meets <br />
<span className="text-accent dark:text-accent">The Soil.</span>
@ -72,136 +44,20 @@ export default function About() {
</p>
</div>
<button className="text-primary font-medium hover:text-blue-700 transition-colors flex items-center gap-2 group">
<Link href="/about" className="text-primary font-medium hover:text-blue-700 transition-colors flex items-center gap-2 group">
Learn More About Us
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-4 h-4 group-hover:translate-x-1 transition-transform">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
</svg>
</button>
</div>
</Link>
</motion.div>
{/* 3D Cube Container */}
<div
className="h-[500px] w-full flex items-center justify-center perspective-container cursor-move"
ref={containerRef}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div
className={`cube relative w-64 h-64 md:w-80 md:h-80 transform-style-3d ${!isHovering ? 'animate-spin-slow' : ''}`}
style={isHovering ? { transform: `rotateX(${rotation.x}deg) rotateY(${rotation.y}deg)` } : {}}
>
{/* Front Face */}
<div className="absolute inset-0 transform translate-z-32 md:translate-z-40 bg-white shadow-xl border border-white/20">
<Image
src="/assets/images/front-side.jfif"
alt="Front View"
fill
className="object-cover"
/>
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Front View</div>
</div>
{/* Back Face */}
<div className="absolute inset-0 transform rotate-y-90 translate-z-32 md:translate-z-40 bg-white shadow-xl border border-white/20">
<Image
src="/assets/images/back-side.jfif"
alt="Back View"
fill
className="object-cover"
/>
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Back View</div>
</div>
{/* Right Face */}
<div className="absolute inset-0 transform rotate-y-90 translate-z-32 md:translate-z-40 bg-white shadow-xl border border-white/20">
<Image
src="/assets/images/right-side.jfif"
alt="Right View"
fill
className="object-cover"
/>
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Right View</div>
</div>
{/* Left Face */}
<div className="absolute inset-0 transform -rotate-y-90 translate-z-32 md:translate-z-40 bg-white shadow-xl border border-white/20">
<Image
src="/assets/images/left-side.jfif"
alt="Left View"
fill
className="object-cover"
/>
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Left View</div>
</div>
{/* Top Face */}
<div className="absolute inset-0 transform rotate-x-90 translate-z-32 md:translate-z-40 bg-gray-100 border border-white/20 flex items-center justify-center">
<Image
src="/assets/images/top-side.jfif"
alt="Top View"
fill
className="object-cover"
/>
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Top View</div>
</div>
{/* Bottom Face */}
<div className="absolute inset-0 transform -rotate-x-90 translate-z-32 md:translate-z-40 bg-gray-100 border border-white/20 flex items-center justify-center shadow-2xl">
<Image
src="/assets/images/bottom-side.jfif"
alt="Bottom View"
fill
className="object-cover"
/>
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Bottom View</div>
</div>
</div>
{/* Parallax Image Container - Takes 7 columns - MODIFIED for Fixed Effect */}
<div className="lg:col-span-7 h-[400px] lg:h-[600px] w-full rounded-2xl overflow-hidden relative">
<div className={styles.parallaxImage} style={{ borderRadius: '1rem', height: '100%', width: '100%' }}></div>
</div>
</div>
</div>
<style jsx global>{`
.perspective-container {
perspective: 1000px;
}
.transform-style-3d {
transform-style: preserve-3d;
transition: transform 0.1s ease-out; /* Smooth transition for mouse movement */
}
.translate-z-32 {
transform: rotateY(0deg) translateZ(8rem);
}
.translate-z-40 {
transform: rotateY(0deg) translateZ(10rem);
}
/* Custom transforms for faces */
.cube > div:nth-child(1) { transform: rotateY(0deg) translateZ(128px); }
.cube > div:nth-child(2) { transform: rotateY(180deg) translateZ(128px); }
.cube > div:nth-child(3) { transform: rotateY(90deg) translateZ(128px); }
.cube > div:nth-child(4) { transform: rotateY(-90deg) translateZ(128px); }
.cube > div:nth-child(5) { transform: rotateX(90deg) translateZ(128px); }
.cube > div:nth-child(6) { transform: rotateX(-90deg) translateZ(128px); }
@media (min-width: 768px) {
.cube > div:nth-child(1) { transform: rotateY(0deg) translateZ(160px); }
.cube > div:nth-child(2) { transform: rotateY(180deg) translateZ(160px); }
.cube > div:nth-child(3) { transform: rotateY(90deg) translateZ(160px); }
.cube > div:nth-child(4) { transform: rotateY(-90deg) translateZ(160px); }
.cube > div:nth-child(5) { transform: rotateX(90deg) translateZ(160px); }
.cube > div:nth-child(6) { transform: rotateX(-90deg) translateZ(160px); }
}
@keyframes spin-slow {
from { transform: rotateX(-10deg) rotateY(0deg); }
to { transform: rotateX(-10deg) rotateY(360deg); }
}
.animate-spin-slow {
animation: spin-slow 20s linear infinite;
}
`}</style>
</section>
</section >
);
}

View File

@ -0,0 +1,65 @@
"use client";
import { useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
interface BengaluruHeaderProps {
filteredCount: number;
totalCount: number;
}
export default function BengaluruHeader({ filteredCount, totalCount }: BengaluruHeaderProps) {
const [isExpanded, setIsExpanded] = useState(false);
const router = useRouter();
const searchParams = useSearchParams();
const currentSearch = searchParams.get("search");
const fullText = "Browse real estate for sale in Bengaluru. Properties including flats, villas, and plots from trusted builders. Whether you're looking for luxurious or budget-friendly property in Bengaluru, sky&soil offers a wide range of options to suit every need. Find properties for sale in Bengaluru. Residential options with verified details, photos, and competitive pricing. Prices ₹1.5Cr - ₹5.0Cr. RERA-registered homes in gated communities, with amenities such as Swimming Pool, Gym, Clubhouse, Kids' Play Area, 24x7 Security, and Power Backup. Legal due diligence support available. 356+ options across top communities. Prime locations with IT connectivity and social infrastructure. View accurate prices, configurations, photos, and neighbourhood insights to compare options and book confidently with sky&soil.";
const toggleReadMore = () => setIsExpanded(!isExpanded);
const handleModernSpaacesClick = () => {
// Toggle: if already selected, clear it (optional, but good UX). Or just set it.
// User asked for "click that button morden this ... show morden space preprtis only filter"
router.push("?search=Modern+Spaaces", { scroll: false });
};
const isModernSpaacesActive = currentSearch === "Modern Spaaces";
return (
<div className="mb-8">
<div className="flex flex-col md:flex-row md:items-center justify-between mb-4">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Property for Sale in Bengaluru
</h1>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-2 md:mt-0">
Showing 1-{filteredCount} of {totalCount} Properties | Last Updated: {new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}
</div>
</div>
<div className="mb-6">
<p className="text-gray-600 dark:text-gray-300 text-sm leading-relaxed inline">
{isExpanded ? fullText : `${fullText.substring(0, 230)}...`}
</p>
<button
onClick={toggleReadMore}
className="ml-2 text-orange-500 hover:text-orange-600 font-medium text-sm inline-block focus:outline-none"
>
{isExpanded ? "Show Less" : "Read more"}
</button>
</div>
<div className="flex gap-4 justify-center">
<button
onClick={handleModernSpaacesClick}
className={`px-6 py-2 rounded-full border transition-all text-sm font-medium ${isModernSpaacesActive
? "bg-gray-900 text-white border-gray-900 dark:bg-white dark:text-black"
: "bg-white text-gray-700 border-gray-300 hover:border-gray-900 dark:bg-black dark:text-gray-300 dark:border-gray-700"
}`}
>
Modern Spaaces
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
"use client";
import PropertiesClient from "@/components/PropertiesClient";
import BengaluruHeader from "@/components/BengaluruHeader";
export default function BengaluruPropertiesClientWrapper() {
return (
<PropertiesClient
headerRenderer={(props) => <BengaluruHeader {...props} />}
bannerTitle="Properties for Sale in Bengaluru"
/>
);
}

View File

@ -15,13 +15,24 @@ export default function CompareBar() {
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-4 flex-1 overflow-x-auto hide-scrollbar">
<div className="flex items-center gap-2 flex-shrink-0">
<svg className="w-6 h-6 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
<svg
className="w-7 h-7 text-primary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
d="M4 7h11m0 0l-4-4m4 4l-4 4M20 17H9m0 0l4-4m-4 4l4 4"
/>
</svg>
<span className="font-semibold text-foreground">Compare Properties ({compareList.length}/4)</span>
</div>
<div className="flex gap-3">
<div className="flex gap-3 pt-3 pr-3">
{compareList.map((property) => (
<div key={property.id} className="relative group flex-shrink-0">
<div className="w-20 h-20 rounded-lg overflow-hidden border-2 border-gray-200 dark:border-gray-700">
@ -60,8 +71,8 @@ export default function CompareBar() {
<Link
href="/compare"
className={`px-6 py-3 rounded-lg font-semibold transition-all ${compareList.length >= 2
? "bg-primary text-white hover:bg-blue-700 shadow-lg hover:shadow-xl"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
? "bg-primary text-white hover:bg-blue-700 shadow-lg hover:shadow-xl"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
}`}
onClick={(e) => {
if (compareList.length < 2) {

View File

@ -2,7 +2,6 @@
import { useCompare } from "@/context/CompareContext";
import { properties } from "@/data/properties";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import InnerBanner from "@/components/InnerBanner";
import Image from "next/image";
@ -25,7 +24,6 @@ export default function CompareClient() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-black">
<Header />
<InnerBanner
title="Compare Properties"
@ -59,177 +57,180 @@ export default function CompareClient() {
</Link>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-white dark:bg-gray-900 sticky top-20 z-10 shadow-md">
<th className="p-4 text-left font-semibold text-foreground border-b-2 border-gray-200 dark:border-gray-800 w-48">
Features
</th>
{compareList.map((property) => (
<th key={property.id} className="p-4 border-b-2 border-gray-200 dark:border-gray-800 min-w-[280px]">
<div className="relative">
<>
<h2 className="text-3xl font-bold text-foreground mb-8">Property Comparison</h2>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-white dark:bg-gray-900 sticky top-20 z-10 shadow-md">
<th className="p-4 text-left font-semibold text-foreground border-b-2 border-gray-200 dark:border-gray-800 w-48">
Features
</th>
{compareList.map((property) => (
<th key={property.id} className="p-4 border-b-2 border-gray-200 dark:border-gray-800 min-w-[280px]">
<div className="relative">
<button
onClick={() => removeFromCompare(property.id)}
className="absolute -top-2 -right-2 w-8 h-8 bg-red-500 text-white rounded-full flex items-center justify-center hover:bg-red-600 transition-colors shadow-md z-10"
aria-label="Remove from comparison"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="relative h-40 rounded-xl overflow-hidden mb-3">
<Image
src={property.image}
alt={property.title}
fill
className="object-cover"
/>
</div>
<h3 className="font-bold text-foreground text-lg mb-1">{property.title}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">{property.location}</p>
</div>
</th>
))}
{/* Add Property Slot */}
{compareList.length < 4 && (
<th className="p-4 border-b-2 border-gray-200 dark:border-gray-800 min-w-[280px]">
<button
onClick={() => removeFromCompare(property.id)}
className="absolute -top-2 -right-2 w-8 h-8 bg-red-500 text-white rounded-full flex items-center justify-center hover:bg-red-600 transition-colors shadow-md z-10"
aria-label="Remove from comparison"
onClick={() => setShowAddModal(true)}
className="w-full h-40 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-xl flex items-center justify-center hover:border-primary hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all group"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
<div className="text-center">
<svg className="w-12 h-12 mx-auto text-gray-400 group-hover:text-primary mb-2 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
<p className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-primary font-medium transition-colors">Add Property</p>
</div>
</button>
<div className="relative h-40 rounded-xl overflow-hidden mb-3">
<Image
src={property.image}
alt={property.title}
fill
className="object-cover"
/>
</div>
<h3 className="font-bold text-foreground text-lg mb-1">{property.title}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">{property.location}</p>
</div>
</th>
))}
{/* Add Property Slot */}
{compareList.length < 4 && (
<th className="p-4 border-b-2 border-gray-200 dark:border-gray-800 min-w-[280px]">
<button
onClick={() => setShowAddModal(true)}
className="w-full h-40 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-xl flex items-center justify-center hover:border-primary hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all group"
>
<div className="text-center">
<svg className="w-12 h-12 mx-auto text-gray-400 group-hover:text-primary mb-2 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
<p className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-primary font-medium transition-colors">Add Property</p>
</div>
</button>
</th>
)}
</tr>
</thead>
<tbody>
{/* Price */}
<tr className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Price</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center">
<span className="text-2xl font-bold text-primary">{property.price}</span>
</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Configuration */}
<tr className="bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Configuration</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.overview.bhk}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Area */}
<tr className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Area</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.overview.size}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Possession */}
<tr className="bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Possession</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.overview.possession}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Total Units */}
<tr className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Total Units</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.overview.totalUnits}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Category */}
<tr className="bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Property Type</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.category}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Status */}
<tr className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Status</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center">
<span className={`inline-block px-3 py-1 rounded-full text-sm font-semibold ${property.status === "Sold Out"
? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
: property.status === "New Launch"
? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
}`}>
{property.status}
</span>
</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Amenities Header */}
<tr className="bg-gray-100 dark:bg-gray-800">
<td colSpan={compareList.length + (compareList.length < 4 ? 2 : 1)} className="p-4 font-bold text-foreground text-lg">
Amenities
</td>
</tr>
{/* Amenities - Get all unique amenities */}
{Array.from(new Set(compareList.flatMap(p => p.amenities))).map((amenity, idx) => (
<tr key={idx} className={idx % 2 === 0 ? "bg-white dark:bg-gray-900" : "bg-gray-50 dark:bg-gray-900/50"}>
<td className="p-4 text-foreground">{amenity}</td>
</th>
)}
</tr>
</thead>
<tbody>
{/* Price */}
<tr className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Price</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center">
{property.amenities.includes(amenity) ? (
<svg className="w-6 h-6 text-green-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-6 h-6 text-red-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
<span className="text-2xl font-bold text-primary">{property.price}</span>
</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
))}
{/* View Details */}
<tr className="bg-white dark:bg-gray-900">
<td className="p-4 font-semibold text-foreground">Actions</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center">
<Link
href={`/properties/${property.id}`}
className="inline-block px-6 py-2 bg-primary text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
>
View Details
</Link>
{/* Configuration */}
<tr className="bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Configuration</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.overview.bhk}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Area */}
<tr className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Area</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.overview.size}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Possession */}
<tr className="bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Possession</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.overview.possession}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Total Units */}
<tr className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Total Units</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.overview.totalUnits}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Category */}
<tr className="bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Property Type</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.category}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Status */}
<tr className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Status</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center">
<span className={`inline-block px-3 py-1 rounded-full text-sm font-semibold ${property.status === "Sold Out"
? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
: property.status === "New Launch"
? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
}`}>
{property.status}
</span>
</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Amenities Header */}
<tr className="bg-gray-100 dark:bg-gray-800">
<td colSpan={compareList.length + (compareList.length < 4 ? 2 : 1)} className="p-4 font-bold text-foreground text-lg">
Amenities
</td>
</tr>
{/* Amenities - Get all unique amenities */}
{Array.from(new Set(compareList.flatMap(p => p.amenities))).map((amenity, idx) => (
<tr key={idx} className={idx % 2 === 0 ? "bg-white dark:bg-gray-900" : "bg-gray-50 dark:bg-gray-900/50"}>
<td className="p-4 text-foreground">{amenity}</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center">
{property.amenities.includes(amenity) ? (
<svg className="w-6 h-6 text-green-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-6 h-6 text-red-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
</tbody>
</table>
</div>
{/* View Details */}
<tr className="bg-white dark:bg-gray-900">
<td className="p-4 font-semibold text-foreground">Actions</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center">
<Link
href={`/properties/${property.id}`}
className="inline-block px-6 py-2 bg-primary text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
>
View Details
</Link>
</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
</tbody>
</table>
</div>
</>
)}
</div>
</div>

View File

@ -0,0 +1,312 @@
"use client";
import { useState, useEffect, useMemo, useRef } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
// Fix for default markers in Next.js
const DefaultIcon = L.icon({
iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
shadowUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41]
});
L.Marker.prototype.options.icon = DefaultIcon;
// Custom Icons
const createCustomIcon = (color: string, number?: string) => {
return L.divIcon({
className: 'custom-icon',
html: `<div style="
background-color: ${color};
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid white;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
color: white;
font-weight: bold;
font-size: 14px;
">${number || ''}</div>`,
iconSize: [36, 36],
iconAnchor: [18, 18],
});
};
const homeIcon = L.divIcon({
className: 'home-icon',
html: `<div style="
background-color: #7C3AED;
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 4px solid white;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
color: white;
">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" style="width: 28px; height: 28px;">
<path d="M11.47 3.84a.75.75 0 011.06 0l8.69 8.69a.75.75 0 101.06-1.06l-8.689-8.69a2.25 2.25 0 00-3.182 0l-8.69 8.69a.75.75 0 001.061 1.06l8.69-8.69z" />
<path d="M12 5.432l8.159 8.159c.03.03.06.058.091.086v6.198c0 1.035-.84 1.875-1.875 1.875H15a.75.75 0 01-.75-.75v-4.5a.75.75 0 00-.75-.75h-3a.75.75 0 00-.75.75V21a.75.75 0 01-.75.75H5.625a1.875 1.875 0 01-1.875-1.875v-6.198a2.29 2.29 0 00.091-.086L12 5.43z" />
</svg>
</div>`,
iconSize: [48, 48],
iconAnchor: [24, 24],
});
// Mock Data
type LocationData = {
category: string;
items: {
id: number;
name: string;
dist: string;
time: string;
lat: number;
lng: number;
count: number;
}[];
};
const PROPERTY_LOCATION: [number, number] = [12.9385, 77.7297]; // Approximate Varthur
const MOCK_DATA: Record<string, LocationData['items']> = {
'Commute': [
{ id: 1, name: "Dommasandra Circle Metro Station", dist: "3.62 Km", time: "7 mins", lat: 12.9250, lng: 77.7450, count: 6 },
{ id: 2, name: "Sompura Metro Station", dist: "7.05 Km", time: "15 mins", lat: 12.9100, lng: 77.7600, count: 4 },
{ id: 3, name: "Sarjapur Metro Station", dist: "8.28 Km", time: "18 mins", lat: 12.8900, lng: 77.7800, count: 3 },
{ id: 4, name: "Ambedkar nagar Metro Station", dist: "8.89 Km", time: "18 mins", lat: 12.9550, lng: 77.7100, count: 5 },
],
'Education': [
{ id: 5, name: "Whitefield Global School", dist: "2.5 Km", time: "6 mins", lat: 12.9550, lng: 77.7350, count: 8 },
{ id: 6, name: "Greenwood High", dist: "4.1 Km", time: "10 mins", lat: 12.9150, lng: 77.7550, count: 5 },
],
'Hospitals': [
{ id: 7, name: "Manipal Hospital Varthur", dist: "1.2 Km", time: "4 mins", lat: 12.9420, lng: 77.7320, count: 2 },
],
'Work': [
{ id: 8, name: "RGA Tech Park", dist: "5.5 Km", time: "12 mins", lat: 12.9050, lng: 77.7150, count: 12 },
],
'Entertainment': [
{ id: 9, name: "Nexus Whitefield", dist: "3.2 Km", time: "9 mins", lat: 12.9600, lng: 77.7400, count: 7 },
],
};
function MapController({ center }: { center: [number, number] }) {
const map = useMap();
useEffect(() => {
map.setView(center, 13);
}, [center, map]);
return null;
}
function ZoomHandler({ zoomIn, zoomOut }: { zoomIn: () => void, zoomOut: () => void }) {
return (
<div className="absolute top-4 left-4 flex flex-col gap-2 z-[400]">
<button
onClick={zoomIn}
className="w-8 h-8 bg-white dark:bg-gray-800 rounded shadow-md flex items-center justify-center hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<span className="text-gray-700 dark:text-gray-300 font-bold text-lg">+</span>
</button>
<button
onClick={zoomOut}
className="w-8 h-8 bg-white dark:bg-gray-800 rounded shadow-md flex items-center justify-center hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
style={{ lineHeight: '0' }}
>
<span className="text-gray-700 dark:text-gray-300 font-bold text-lg"></span>
</button>
</div>
);
}
function CustomTileLayer() {
const map = useMap();
useEffect(() => {
const layer = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
// Patch createTile to add unique alt text
const originalCreateTile = (layer as any).createTile;
(layer as any).createTile = function (coords: any, done: any) {
// @ts-ignore - createTile expects a different signature in some type definitions but this is standard Leaflet
const tile = originalCreateTile.call(this, coords, done);
if (tile instanceof HTMLImageElement) {
tile.alt = `Map tile location ${coords.x}, ${coords.y} zoom level ${coords.z}`;
}
return tile;
};
layer.addTo(map);
return () => {
layer.removeFrom(map);
};
}, [map]);
return null;
}
export default function ConnectivityMap() {
const [activeTab, setActiveTab] = useState("Commute");
const tabsRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (activeTab && tabsRef.current) {
const activeBtn = document.getElementById(`connectivity-tab-${activeTab}`);
if (activeBtn) {
const container = tabsRef.current;
const activeBtnRect = activeBtn.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const scrollLeft = container.scrollLeft + (activeBtnRect.left - containerRect.left) - (container.offsetWidth / 2) + (activeBtn.offsetWidth / 2);
container.scrollTo({
left: scrollLeft,
behavior: "smooth"
});
}
}
}, [activeTab]);
const [searchQuery, setSearchQuery] = useState("");
const [mapZoom, setMapZoom] = useState(13);
const [mapRef, setMapRef] = useState<L.Map | null>(null);
const activeData = useMemo(() => {
let data = activeTab === "Search"
? Object.values(MOCK_DATA).flat()
: MOCK_DATA[activeTab] || [];
if (searchQuery) {
data = data.filter(item =>
item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}
return data;
}, [activeTab, searchQuery]);
const handleZoomIn = () => {
if (mapRef) mapRef.zoomIn();
};
const handleZoomOut = () => {
if (mapRef) mapRef.zoomOut();
};
return (
<div className="relative z-0 bg-white dark:bg-gray-900 rounded-xl overflow-hidden border border-gray-200 dark:border-gray-700">
{/* Tabs */}
<div ref={tabsRef} className="flex overflow-x-auto border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 scrollbar-hide">
{["Commute", "Education", "Hospitals", "Work", "Entertainment", "Search"].map((tab) => (
<button
key={tab}
id={`connectivity-tab-${tab}`}
onClick={() => {
setActiveTab(tab);
setSearchQuery(""); // Clear search when switching tabs
}}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap flex items-center gap-2 transition-colors border-b-2 ${activeTab === tab
? "text-orange-500 border-orange-500 bg-orange-50/30 dark:bg-orange-900/10"
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 border-transparent"
}`}
>
{/* Icons */}
{tab === "Commute" && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4"><path d="M6.5 3c-1.051 0-2.093.04-3.125.117A1.49 1.49 0 002 4.607V10.5h9V4.606c0-.771-.59-1.43-1.375-1.489A41.568 41.568 0 006.5 3zM2 12v2.5A1.5 1.5 0 003.5 16h.041a3 3 0 015.918 0h.791a.75.75 0 00.75-.75V12H2z" /><path d="M6.5 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM13.25 5a.75.75 0 00-.75.75v8.514a3.001 3.001 0 014.893 1.44c.37-.275.61-.719.595-1.227a24.905 24.905 0 00-1.784-8.549A1.486 1.486 0 0014.823 5H13.25zM14.5 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3z" /></svg>}
{tab === "Education" && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4"><path d="M10.75 16.82A7.462 7.462 0 0115 15.5c.71 0 1.396.098 2.046.282A.75.75 0 0018 15.06v-11a.75.75 0 00-.546-.721A9.006 9.006 0 0015 3a8.963 8.963 0 00-4.25 1.065V16.82zM9.25 4.065A8.963 8.963 0 005 3c-.85 0-1.673.118-2.454.339A.75.75 0 002 4.06v11a.75.75 0 00.954.721A7.506 7.506 0 015 15.5c1.579 0 3.042.487 4.25 1.32V4.065z" /></svg>}
{tab === "Hospitals" && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4"><path fillRule="evenodd" d="M10 2a6 6 0 00-6 6c0 1.887-.454 3.665-1.257 5.234a.75.75 0 00.515 1.076 32.91 32.91 0 003.256.508 3.5 3.5 0 006.972 0 32.903 32.903 0 003.256-.508.75.75 0 00.515-1.076A11.448 11.448 0 0116 8a6 6 0 00-6-6zM8.05 14.943a33.54 33.54 0 003.9 0 2 2 0 01-3.9 0z" clipRule="evenodd" /></svg>}
{tab === "Work" && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4"><path fillRule="evenodd" d="M4 16.5v-13h-.25a.75.75 0 010-1.5h12.5a.75.75 0 010 1.5H16v13h.25a.75.75 0 010 1.5h-3.5a.75.75 0 01-.75-.75v-2.5a.75.75 0 00-.75-.75h-2.5a.75.75 0 00-.75.75v2.5a.75.75 0 01-.75.75h-3.5a.75.75 0 010-1.5H4zm3-11a.5.5 0 01.5-.5h1a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-1zM7.5 9a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-1zM11 5.5a.5.5 0 01.5-.5h1a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-1a.5.5 0 01-.5-.5v-1zm.5 3.5a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h1a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-1z" clipRule="evenodd" /></svg>}
{tab === "Entertainment" && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4"><path fillRule="evenodd" d="M6 5v1H4.667a1.75 1.75 0 00-1.743 1.598l-.826 9.5A1.75 1.75 0 003.84 19H16.16a1.75 1.75 0 001.743-1.902l-.826-9.5A1.75 1.75 0 0015.333 6H14V5a4 4 0 00-8 0zm4-2.5A2.5 2.5 0 007.5 5v1h5V5A2.5 2.5 0 0010 2.5zM7.5 10a2.5 2.5 0 005 0V8.75a.75.75 0 011.5 0V10a4 4 0 01-8 0V8.75a.75.75 0 011.5 0V10z" clipRule="evenodd" /></svg>}
{tab === "Search" && <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-4 h-4"><path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" /></svg>}
{tab}
</button>
))}
</div>
<div className="flex flex-col lg:flex-row h-[450px] relative">
{/* Map Area */}
<div className="flex-1 h-full relative">
<MapContainer
center={PROPERTY_LOCATION}
zoom={13}
style={{ height: '100%', width: '100%' }}
zoomControl={false}
ref={setMapRef}
>
<CustomTileLayer />
<MapController center={PROPERTY_LOCATION} />
<ZoomHandler zoomIn={handleZoomIn} zoomOut={handleZoomOut} />
{/* Property Marker */}
<Marker position={PROPERTY_LOCATION} icon={homeIcon} />
{/* POI Markers */}
{activeData.map((item) => (
<Marker
key={item.id}
position={[item.lat, item.lng]}
icon={createCustomIcon('#F97316', item.count.toString())}
>
<Popup>{item.name}</Popup>
</Marker>
))}
</MapContainer>
{/* Layer Toggle Button (Bottom Left) */}
<div className="absolute bottom-4 left-4 z-[400]">
<button className="w-10 h-10 bg-white dark:bg-gray-800 rounded shadow-md flex items-center justify-center hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-5 h-5 text-gray-600 dark:text-gray-400">
<path fillRule="evenodd" d="M3.25 3A2.25 2.25 0 001 5.25v9.5A2.25 2.25 0 003.25 17h9.5A2.25 2.25 0 0015 14.75v-9.5A2.25 2.25 0 0012.75 3h-9.5zm9.5 1.5a.75.75 0 00-.75.75V8a.75.75 0 001.5 0V5.25a.75.75 0 00-.75-.75zm0 5.5a.75.75 0 00-.75.75v2.75a.75.75 0 001.5 0v-2.75a.75.75 0 00-.75-.75zM10 7a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 7zm-3.25.75a.75.75 0 00-1.5 0v4.5a.75.75 0 001.5 0v-4.5z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
{/* Floating List Card (Right Side) */}
<div className="absolute top-4 right-4 bottom-4 w-full max-w-sm z-[400] flex flex-col pointer-events-none">
<div className="rounded-lg shadow-xl overflow-hidden flex flex-col h-full pointer-events-auto border border-gray-100 dark:border-gray-800">
{/* Search Input for Search Tab */}
{activeTab === "Search" && (
<div className="p-3 border-b border-gray-100 dark:border-gray-800">
<input
type="text"
placeholder="Search location..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-4 py-2 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
</div>
)}
<div className="flex-1 overflow-y-auto custom-scrollbar p-2">
{activeData.length > 0 ? (
activeData.map((item) => (
<div key={item.id} className="p-4 mb-2 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 rounded-lg cursor-pointer transition-all border border-gray-100 dark:border-gray-800 shadow-sm hover:shadow-md">
<h4 className="font-semibold text-gray-900 dark:text-white text-base mb-2">{item.name}</h4>
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
<span className="font-medium">{item.dist}</span>
<span className="text-orange-300">|</span>
<span className="text-gray-500 font-medium">{item.time}</span>
</div>
</div>
))
) : (
<div className="p-8 text-center text-gray-400">
No results found
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,60 +1,255 @@
export default function ContactCTA() {
return (
<section id="contact" className="py-24 bg-secondary dark:bg-gray-900">
<div className="max-w-4xl mx-auto px-6">
<div className="bg-white dark:bg-gray-800 rounded-3xl shadow-xl overflow-hidden">
<div className="p-8 md:p-12 text-center">
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
Ready to Visit Aurora Springs?
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-8">
Schedule a private tour or request a call back from our relationship manager.
</p>
"use client";
<form className="space-y-4 max-w-md mx-auto">
<div>
import { User, Phone, Mail, MapPin, Send } from "lucide-react";
import Image from "next/image";
import { useState, ChangeEvent, FormEvent, useEffect } from "react";
import ReCAPTCHA from "react-google-recaptcha";
import axios from "axios";
import { motion } from "framer-motion";
interface FormData {
name: string;
phone: string;
email: string;
location: string;
}
interface FormErrors {
name?: string;
phone?: string;
email?: string;
location?: string;
captcha?: string;
}
export default function ContactCTA() {
const [formData, setFormData] = useState<FormData>({
name: "",
phone: "",
email: "",
location: "",
});
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [formErrors, setFormErrors] = useState<FormErrors>({});
const [alert, setAlert] = useState<{ show: boolean; type: string; message: string }>({
show: false,
type: "",
message: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
// Clear error when user types
if (formErrors[name as keyof FormErrors]) {
setFormErrors(prev => ({ ...prev, [name]: undefined }));
}
};
const handleCaptchaChange = (token: string | null) => {
setCaptchaToken(token);
if (token && formErrors.captcha) {
setFormErrors(prev => ({ ...prev, captcha: undefined }));
}
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const errors: FormErrors = {};
if (!formData.name.trim()) errors.name = "Name is required.";
if (!formData.phone.trim()) errors.phone = "Phone number is required.";
if (!formData.email.trim()) errors.email = "Email is required.";
if (!formData.location) errors.location = "Please select a location.";
if (!captchaToken) errors.captcha = "Please verify the CAPTCHA.";
setFormErrors(errors);
if (Object.keys(errors).length > 0) return;
setIsSubmitting(true);
const emailData = {
name: formData.name,
phone: formData.phone,
email: formData.email,
subject: `New Inquiry from ${formData.name} - ${formData.location}`,
message: `Name: ${formData.name}<br />Phone: ${formData.phone}<br />Email: ${formData.email}<br />Location Interest: ${formData.location}`,
to: "hello@skyandsoil.com",
senderName: "Sky and Soil Contact Form",
recaptchaToken: captchaToken,
};
try {
const res = await axios.post("https://mailserver.metatronnest.com/send", emailData, {
headers: { "Content-Type": "application/json" },
});
setAlert({
show: true,
type: "success",
message: res?.data?.message || "Message sent successfully! We will contact you soon.",
});
setFormData({ name: "", phone: "", email: "", location: "" });
setCaptchaToken(null);
setFormErrors({});
} catch (error) {
setAlert({
show: true,
type: "danger",
message: "Failed to send message. Please try again later.",
});
} finally {
setIsSubmitting(false);
}
};
useEffect(() => {
if (alert.show) {
const timer = setTimeout(() => {
setAlert(prev => ({ ...prev, show: false }));
}, 5000);
return () => clearTimeout(timer);
}
}, [alert.show]);
return (
<section id="contact" className="relative pb-24 pt-24 dark:bg-transparent">
<div className="relative z-10 max-w-6xl mx-auto px-6">
<motion.div
className="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl overflow-hidden border border-gray-100 dark:border-gray-700 flex flex-col md:flex-row"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.7 }}
>
{/* Left Side - Form */}
<div className="w-full md:w-1/2 p-8 md:p-12">
<div className="text-left mb-8">
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
Ready to Visit Aurora Springs?
</h2>
<p className="text-gray-600 dark:text-gray-400">
Schedule a private tour or request a call back from our relationship manager.
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
{alert.show && (
<div className={`p-4 rounded-xl text-sm font-medium ${alert.type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-700 border border-red-200'}`}>
{alert.message}
</div>
)}
<div className="relative group">
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-primary transition-colors">
<User size={20} />
</div>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Your Name"
className="w-full px-4 py-3 rounded-xl bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 text-foreground placeholder-gray-400 dark:placeholder-gray-500 focus:border-primary focus:ring-2 focus:ring-blue-100 dark:focus:ring-blue-900 outline-none transition-all"
className={`w-full pl-12 pr-4 py-4 rounded-xl bg-gray-50 dark:bg-gray-700/50 border ${formErrors.name ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'} text-foreground placeholder-gray-400 dark:placeholder-gray-500 focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all duration-300`}
/>
{formErrors.name && <small className="text-red-500 text-xs ml-1">{formErrors.name}</small>}
</div>
<div>
<div className="relative group">
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-primary transition-colors">
<Phone size={20} />
</div>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleChange}
placeholder="Phone Number"
className="w-full px-4 py-3 rounded-xl bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 text-foreground placeholder-gray-400 dark:placeholder-gray-500 focus:border-primary focus:ring-2 focus:ring-blue-100 dark:focus:ring-blue-900 outline-none transition-all"
className={`w-full pl-12 pr-4 py-4 rounded-xl bg-gray-50 dark:bg-gray-700/50 border ${formErrors.phone ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'} text-foreground placeholder-gray-400 dark:placeholder-gray-500 focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all duration-300`}
/>
{formErrors.phone && <small className="text-red-500 text-xs ml-1">{formErrors.phone}</small>}
</div>
<div>
<div className="relative group">
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-primary transition-colors">
<Mail size={20} />
</div>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email Address"
className="w-full px-4 py-3 rounded-xl bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 text-foreground placeholder-gray-400 dark:placeholder-gray-500 focus:border-primary focus:ring-2 focus:ring-blue-100 dark:focus:ring-blue-900 outline-none transition-all"
className={`w-full pl-12 pr-4 py-4 rounded-xl bg-gray-50 dark:bg-gray-700/50 border ${formErrors.email ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'} text-foreground placeholder-gray-400 dark:placeholder-gray-500 focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all duration-300`}
/>
{formErrors.email && <small className="text-red-500 text-xs ml-1">{formErrors.email}</small>}
</div>
<div>
<select className="w-full px-4 py-3 rounded-xl bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 focus:border-primary focus:ring-2 focus:ring-blue-100 dark:focus:ring-blue-900 outline-none transition-all">
<div className="relative group">
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 group-focus-within:text-primary transition-colors">
<MapPin size={20} />
</div>
<select
name="location"
value={formData.location}
onChange={handleChange}
className={`w-full pl-12 pr-4 py-4 rounded-xl bg-gray-50 dark:bg-gray-700/50 border ${formErrors.location ? 'border-red-500' : 'border-gray-200 dark:border-gray-600'} text-gray-600 dark:text-gray-300 focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all duration-300 appearance-none`}
>
<option value="">Select Preferred Location</option>
<option value="north-bengaluru">North Bengaluru</option>
<option value="whitefield">Whitefield</option>
<option value="sarjapur">Sarjapur</option>
<option value="North Bengaluru">North Bengaluru</option>
<option value="Whitefield">Whitefield</option>
<option value="Sarjapur">Sarjapur</option>
</select>
<div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none">
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
{formErrors.location && <small className="text-red-500 text-xs ml-1">{formErrors.location}</small>}
</div>
<div className="mt-2">
<ReCAPTCHA
sitekey="6Lea8ZYrAAAAAHaghaLjDx_K084IFATZby7Rzqhl"
onChange={handleCaptchaChange}
/>
{formErrors.captcha && <small className="text-red-500 text-xs ml-1">{formErrors.captcha}</small>}
</div>
<button
type="submit"
className="w-full py-3.5 text-base font-medium text-white bg-primary rounded-xl hover:bg-blue-600 transition-all shadow-lg hover:shadow-xl active:scale-95"
disabled={isSubmitting}
className={`w-full py-4 text-base font-semibold text-white bg-primary rounded-xl hover:bg-blue-600 transition-all shadow-lg hover:shadow-primary/30 active:scale-[0.98] flex items-center justify-center gap-2 group ${isSubmitting ? 'opacity-70 cursor-not-allowed' : ''}`}
>
Request a Call Back
<span>{isSubmitting ? 'Sending...' : 'Request a Call Back'}</span>
{!isSubmitting && <Send size={18} className="group-hover:translate-x-1 transition-transform" />}
</button>
</form>
<p className="mt-6 text-xs text-gray-400 dark:text-gray-500">
<p className="mt-6 text-xs text-gray-400 dark:text-gray-500 flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
We respect your privacy. No spam, ever.
</p>
</div>
</div>
{/* Right Side - Image */}
<div className="w-full md:w-1/2 relative min-h-[400px] md:min-h-full">
<Image
src="/assets/images/home/ready.webp"
alt="Luxury Interior"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent md:bg-gradient-to-l" />
</div>
</motion.div>
</div>
</section>
);

View File

@ -0,0 +1,62 @@
"use client";
import { Phone, Mail, MapPin } from "lucide-react";
import { motion } from "framer-motion";
export default function ContactCards() {
const cards = [
{
icon: <Phone size={24} className="text-white" />,
title: "Quick Contact",
details: ["+91-95388-34444"],
type: "phone"
},
{
icon: <Mail size={24} className="text-white" />,
title: "Email Address",
details: ["support@skyandsoil.com"],
type: "email"
},
{
icon: <MapPin size={24} className="text-white" />,
title: "Mailing Address",
details: [
"HSR Layout, Bengaluru, Karnataka 560102"
],
type: "address"
}
];
return (
<section className="relative z-20 px-6 max-w-6xl mx-auto pb-24">
<div className="grid grid-cols-1 lg:grid-cols-3 md:grid-cols-3 gap-6">
{cards.map((card, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-5 flex flex-col items-start hover:-translate-y-1 transition-transform duration-300"
>
<div className="w-14 h-14 rounded-full bg-primary flex items-center justify-center mb-6 shadow-lg shadow-primary/30">
{card.icon}
</div>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
{card.title}
</h3>
<div className="space-y-1">
{card.details.map((line, i) => (
<p key={i} className="text-gray-600 dark:text-gray-300 font-medium">
{line}
</p>
))}
</div>
</motion.div>
))}
</div>
</section>
);
}

View File

@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import Image from "next/image";
const faqs = [
@ -27,9 +28,9 @@ const faqs = [
];
const carouselImages = [
{ src: "/1-f63fe3ad.png", label: "Building Exterior" },
{ src: "/hero-image.jpg", label: "Luxury Amenities" },
{ src: "/1-f63fe3ad.png", label: "Modern Architecture" }
{ src: "/assets/images/home/faq.webp", label: "Building Exterior" },
{ src: "/assets/images/home/faq-2.webp", label: "Luxury Amenities" },
{ src: "/assets/images/home/faq-3.webp", label: "Modern Architecture" }
];
export default function FAQ() {
@ -38,6 +39,85 @@ export default function FAQ() {
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [isHovering, setIsHovering] = useState(false);
// Derive activeTab from activeCard to ensure sync
const activeTab = carouselImages[activeCard].label;
// Reset open FAQ when active card changes
useEffect(() => {
setOpenIndex(null);
}, [activeCard]);
const faqs = [
// Building Exterior
{
category: "Building Exterior",
question: "What materials are used for the exterior?",
answer: "We use high-grade, weather-resistant materials including natural stone cladding and premium texture paints to ensure durability and elegance."
},
{
category: "Building Exterior",
question: "Is the building earthquake resistant?",
answer: "Yes, all our structures are RCC framed and designed to be earthquake resistant, adhering to the highest safety standards and seismic zones."
},
{
category: "Building Exterior",
question: "How is the exterior maintenance handled?",
answer: "The society association manages exterior maintenance, including regular cleaning and painting, ensuring the building retains its pristine look for years."
},
{
category: "Building Exterior",
question: "Are there eco-friendly features in the design?",
answer: "Absolutely. We incorporate vertical gardens, solar reflective paints, and rainwater harvesting systems into the building's exterior design."
},
// Luxury Amenities
{
category: "Luxury Amenities",
question: "What recreational facilities are available?",
answer: "Residents enjoy access to a fully equipped clubhouse, temperature-controlled swimming pool, indoor games room, and a dedicated yoga deck."
},
{
category: "Luxury Amenities",
question: "Is there a gym within the premises?",
answer: "Yes, we provide a state-of-the-art gymnasium with modern cardio and strength training equipment, open 24/7 for residents."
},
{
category: "Luxury Amenities",
question: "Are there play areas for children?",
answer: "We have safe, designated play zones for children of all ages, featuring modern play equipment and soft flooring for safety."
},
{
category: "Luxury Amenities",
question: "Do you offer concierge services?",
answer: "Yes, our premium properties feature a concierge desk to assist residents with daily tasks, guest management, and facility bookings."
},
// Modern Architecture
{
category: "Modern Architecture",
question: "What is the architectural style of the project?",
answer: "Our projects feature contemporary architecture with clean lines, open floor plans, and large windows to maximize natural light and ventilation."
},
{
category: "Modern Architecture",
question: "Who are the architects behind the design?",
answer: "We collaborate with award-winning international architects who specialize in creating sustainable and aesthetically stunning living spaces."
},
{
category: "Modern Architecture",
question: "Are the homes Vastu compliant?",
answer: "Yes, the majority of our units are designed in accordance with Vastu Shastra principles to ensure harmony and positive energy flow."
},
{
category: "Modern Architecture",
question: "How is privacy ensured in the design?",
answer: "The layout is thoughtfully designed to ensure no two apartments look into each other, providing maximum privacy for all residents."
}
];
const categories = carouselImages.map(img => img.label);
const filteredFaqs = faqs.filter(faq => faq.category === activeTab);
const toggleFAQ = (index: number) => {
setOpenIndex(openIndex === index ? null : index);
};
@ -125,8 +205,8 @@ export default function FAQ() {
key={index}
onClick={() => setActiveCard(index)}
className={`w-2 h-2 rounded-full transition-all duration-300 ${index === activeCard
? 'bg-primary w-8'
: 'bg-gray-400 hover:bg-gray-600'
? 'bg-primary w-8'
: 'bg-gray-400 hover:bg-gray-600'
}`}
aria-label={`Go to slide ${index + 1}`}
/>
@ -136,17 +216,42 @@ export default function FAQ() {
{/* Right Column: FAQ Content */}
<div>
<div className="mb-12">
<motion.div
className="mb-8"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
Frequently Asked Questions
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8">
Everything you need to know about our properties and services.
</p>
</div>
{/* Tabs */}
<div className="flex flex-wrap gap-2 mb-8">
{categories.map((category) => (
<button
key={category}
onClick={() => {
const index = categories.indexOf(category);
setActiveCard(index);
}}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-300 ${activeTab === category
? "bg-primary text-white shadow-lg scale-105"
: "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 border border-gray-200 dark:border-gray-700"
}`}
>
{category}
</button>
))}
</div>
</motion.div>
<div className="space-y-4">
{faqs.map((faq, index) => (
{filteredFaqs.map((faq, index) => (
<div
key={index}
className="bg-white dark:bg-black border border-gray-200 dark:border-gray-800 rounded-2xl overflow-hidden transition-all duration-300 hover:shadow-md"

View File

@ -4,14 +4,14 @@ import Image from "next/image";
export default function Footer() {
return (
<footer className="bg-gray-50 dark:bg-black pt-16 pb-8 border-t border-gray-200 dark:border-gray-800">
<footer className="bg-gray-50 dark:bg-black pt-20 pb-10 border-t border-gray-200 dark:border-gray-800">
<div className="max-w-7xl mx-auto px-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-12 mb-12">
<div className="col-span-1 md:col-span-1">
<Link href="/" className="inline-block relative w-48 h-16">
<Image
src="/assets/images/blue-logo.png"
alt="Sky and Soil Logo"
alt="Sky and Soil Footer Logo"
fill
className="object-contain"
/>
@ -24,20 +24,18 @@ export default function Footer() {
<div>
<h4 className="font-semibold text-foreground mb-4">Quick Links</h4>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<li><Link href="#about" className="hover:text-primary transition-colors">About Us</Link></li>
<li><Link href="#projects" className="hover:text-primary transition-colors">Projects</Link></li>
<li><Link href="#lifestyle" className="hover:text-primary transition-colors">Lifestyle</Link></li>
<li><Link href="#contact" className="hover:text-primary transition-colors">Contact</Link></li>
<li><Link href="/about" className="hover:text-primary transition-colors">About Us</Link></li>
<li><Link href="/projects" className="hover:text-primary transition-colors">Projects</Link></li>
<li><Link href="/lifestyle" className="hover:text-primary transition-colors">Lifestyle</Link></li>
<li><Link href="/contact" className="hover:text-primary transition-colors">Contact</Link></li>
</ul>
</div>
<div>
<h4 className="font-semibold text-foreground mb-4">Legal</h4>
<ul className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<li><Link href="#" className="hover:text-primary transition-colors">Privacy Policy</Link></li>
<li><Link href="#" className="hover:text-primary transition-colors">Terms of Service</Link></li>
<li><Link href="#" className="hover:text-primary transition-colors">Disclaimer</Link></li>
<li><Link href="#" className="hover:text-primary transition-colors">RERA Compliance</Link></li>
<li><Link href="/privacy-policy" className="hover:text-primary transition-colors">Privacy Policy</Link></li>
<li><Link href="/terms-of-service" className="hover:text-primary transition-colors">Terms of Service</Link></li>
</ul>
</div>
@ -51,9 +49,20 @@ export default function Footer() {
</div>
</div>
<div className="border-t border-gray-200 dark:border-gray-800 pt-8 flex flex-col md:flex-row items-center justify-between text-xs text-gray-400 dark:text-gray-500">
<p>&copy; {new Date().getFullYear()} Sky and Soil. All rights reserved.</p>
<p>Designed with precision.</p>
<div className="border-t border-gray-200 dark:border-gray-800 pt-8 flex flex-col items-center justify-center text-xs text-gray-400 dark:text-gray-500 text-center gap-2">
<p>
© {new Date().getFullYear()} Sky and Soil | Powered by
<a
href="https://metatroncubesolutions.com/"
target="_blank"
rel="noopener noreferrer"
className="ml-1"
style={{ color: "#11147e", textDecoration: "none" }}
>
MetatronCube
</a>
All Rights Reserved
</p>
</div>
</div>
</footer>

View File

@ -1,15 +1,20 @@
"use client";
import { useState, useEffect } from "react";
import { useTheme } from "next-themes";
import Link from "next/link";
import Image from "next/image";
import Sidebar from "@/components/Sidebar";
export default function Header() {
const { resolvedTheme } = useTheme();
const [isScrolled, setIsScrolled] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
useEffect(() => {
// Set initial scroll state
setIsScrolled(window.scrollY > 50);
const handleScroll = () => {
setIsScrolled(window.scrollY > 50);
};
@ -18,22 +23,52 @@ export default function Header() {
return () => window.removeEventListener("scroll", handleScroll);
}, []);
// Prevent hydration mismatch
// if (!mounted) {
// return (
// <header className="fixed top-0 left-0 right-0 z-50 bg-transparent py-6">
// <div className="w-full px-[50px] max-[375px]:px-[25px] flex items-center justify-between">
// <Link href="/" className="flex items-center gap-3 group">
// <div className="relative w-60 h-20 -left-[83px]">
// <Image
// src="/assets/images/white-logo.png"
// alt="Sky and Soil Logo"
// fill
// className="object-contain"
// />
// </div>
// </Link>
// <div className="flex items-center gap-4">
// <button className="p-2 -mr-2 text-white">
// <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
// <path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
// </svg>
// </button>
// </div>
// </div>
// </header>
// );
// }
const isLight = resolvedTheme === "light";
return (
<>
<header
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isScrolled
? "bg-white/80 dark:bg-black/80 backdrop-blur-md shadow-sm py-4"
? "bg-white/25 dark:bg-black/25 backdrop-blur-md shadow-sm py-4"
: "bg-transparent py-6"
}`}
>
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between">
<div className="w-full px-[50px] max-[375px]:px-[25px] flex items-center justify-between">
<Link href="/" className="flex items-center gap-3 group">
<div className="relative w-60 h-20 transition-transform group-hover:scale-105">
<div className="relative w-60 h-20 transition-transform group-hover:scale-105 -left-[83px]">
<Image
src={isScrolled ? "/assets/images/blue-logo.png" : "/assets/images/white-logo.png"}
alt="Sky and Soil Logo"
src={(isScrolled && isLight) ? "/assets/images/blue-logo.png" : "/assets/images/white-logo.png"}
alt="Sky and Soil Header Logo"
fill
className="object-contain"
priority
/>
</div>
{/* <span className={`text-2xl font-semibold tracking-tight group-hover:text-primary transition-colors ${isScrolled ? "text-foreground" : "text-white"
@ -46,14 +81,25 @@ export default function Header() {
<button
className={`p-2 -mr-2 transition-colors ${isScrolled ? "text-foreground" : "text-white"
}`}
className={`p-2 -mr-2 ${(isScrolled && isLight) ? "text-primary" : "text-white"}`}
onClick={() => setIsSidebarOpen(true)}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
</button>
</div>
</div>
</header>
@ -61,4 +107,4 @@ export default function Header() {
<Sidebar isOpen={isSidebarOpen} onClose={() => setIsSidebarOpen(false)} />
</>
);
}
}

View File

@ -0,0 +1,11 @@
"use client";
import dynamic from "next/dynamic";
const Header = dynamic(() => import("@/components/Header"), {
ssr: false,
});
export default function HeaderClient() {
return <Header />;
}

View File

@ -1,4 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
export default function Hero() {
return (
@ -12,19 +15,25 @@ export default function Hero() {
playsInline
className="absolute inset-0 w-full h-full object-cover"
poster="/assets/images/aerial-view.mov" // Fallback image
aria-label="Aerial view of Bangalore showcasing premium real estate locations"
>
<source src="/assets/images/aerial-view.mov" type="video/mp4" />
{/* Note: This is a placeholder video. Replace with your Bangalore drone shot. */}
</video>
{/* Overlay */}
<div className="absolute inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-[1px]"></div>
</div>
{/* Location Pins (Decorative) */}
<div className="absolute inset-0 z-10 pointer-events-none hidden md:block">
{/* Pin 1: Hebbal */}
<div className="absolute top-[30%] left-[20%] animate-bounce" style={{ animationDuration: '3s' }}>
<div className="flex flex-col items-center">
<motion.div
className="absolute top-[30%] left-[20%]"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 1.2 }}
>
<div className="flex flex-col items-center animate-bounce" style={{ animationDuration: '3s' }}>
<div className="bg-white/90 backdrop-blur-md px-3 py-1 rounded-lg shadow-lg mb-2">
<span className="text-xs font-bold text-black">Hebbal</span>
</div>
@ -33,11 +42,16 @@ export default function Hero() {
</div>
<div className="h-16 w-0.5 bg-gradient-to-b from-white/50 to-transparent"></div>
</div>
</div>
</motion.div>
{/* Pin 2: Airport */}
<div className="absolute top-[20%] right-[25%] animate-bounce" style={{ animationDuration: '4s', animationDelay: '1s' }}>
<div className="flex flex-col items-center">
<motion.div
className="absolute top-[20%] right-[25%]"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 1.5 }}
>
<div className="flex flex-col items-center animate-bounce" style={{ animationDuration: '4s', animationDelay: '1s' }}>
<div className="bg-white/90 backdrop-blur-md px-3 py-1 rounded-lg shadow-lg mb-2">
<span className="text-xs font-bold text-black">Airport</span>
</div>
@ -46,11 +60,16 @@ export default function Hero() {
</div>
<div className="h-24 w-0.5 bg-gradient-to-b from-white/50 to-transparent"></div>
</div>
</div>
</motion.div>
{/* Pin 3: Whitefield */}
<div className="absolute bottom-[30%] right-[15%] animate-bounce" style={{ animationDuration: '3.5s', animationDelay: '0.5s' }}>
<div className="flex flex-col items-center">
<motion.div
className="absolute bottom-[30%] right-[15%]"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 1.8 }}
>
<div className="flex flex-col items-center animate-bounce" style={{ animationDuration: '3.5s', animationDelay: '0.5s' }}>
<div className="bg-white/90 backdrop-blur-md px-3 py-1 rounded-lg shadow-lg mb-2">
<span className="text-xs font-bold text-black">Whitefield</span>
</div>
@ -59,11 +78,16 @@ export default function Hero() {
</div>
<div className="h-12 w-0.5 bg-gradient-to-b from-white/50 to-transparent"></div>
</div>
</div>
</motion.div>
</div>
<div className="relative z-20 max-w-5xl mx-auto px-6 text-center">
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold tracking-tight text-white mb-6 animate-fade-in drop-shadow-lg">
<motion.div
className="relative z-20 max-w-5xl mx-auto px-6 text-center"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.3 }}
>
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold tracking-tight text-white mb-6 drop-shadow-lg">
Sky and Soil <br className="hidden md:block" />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-300">
Bangalore.
@ -75,14 +99,22 @@ export default function Hero() {
</p> */}
</div>
</motion.div>
{/* Scroll Indicator */}
<div className="absolute bottom-10 left-1/2 transform -translate-x-1/2 animate-bounce z-20">
<motion.div
className="absolute bottom-10 left-1/2 transform -translate-x-1/2 z-20"
initial={{ opacity: 0 }}
animate={{ opacity: 0.8, y: [0, 10, 0] }}
transition={{
opacity: { duration: 1, delay: 1.5 },
y: { duration: 1.5, repeat: Infinity, ease: "easeInOut" }
}}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6 text-white/80">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</div>
</motion.div>
</section>
);
}

View File

@ -16,12 +16,12 @@ export default function InnerBanner({
backgroundImage = "/hero-image.jpg"
}: InnerBannerProps) {
return (
<div className="relative h-[450px] w-full overflow-hidden">
<div className="relative h-[450px] w-full overflow-hidden pt-20">
{/* Background Image */}
<div className="absolute inset-0">
<Image
src={backgroundImage}
alt={title}
alt={`Hero image of ${title} - Sky and Soil Real Estate`}
fill
className="object-cover"
priority
@ -35,9 +35,9 @@ export default function InnerBanner({
{/* Breadcrumbs */}
{breadcrumbs && breadcrumbs.length > 0 && (
<nav className="mb-4">
<ol className="flex items-center gap-2 text-sm text-white/80">
<ol className="flex flex-wrap items-center gap-x-2 gap-y-2 text-sm text-white/80">
{breadcrumbs.map((crumb, index) => (
<li key={index} className="flex items-center gap-2">
<li key={index} className="flex items-center gap-1 md:gap-2">
{crumb.href ? (
<a
href={crumb.href}

View File

@ -2,7 +2,7 @@
import { useState, useRef, MouseEvent } from "react";
import Image from "next/image";
import { motion } from "framer-motion";
import { FloatingHouse, RotatingKey } from "./PropertyAnimations";
export default function Lifestyle() {
@ -37,15 +37,19 @@ export default function Lifestyle() {
<div className="max-w-7xl mx-auto px-6 relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
{/* Image Side with Zoom Effect */}
<div
<motion.div
className="order-2 lg:order-1 relative h-[600px] rounded-3xl overflow-hidden shadow-2xl group cursor-crosshair"
ref={imageRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.7 }}
>
<Image
src="/hero-image.jpg"
alt="Lifestyle"
src="/assets/images/home/experience.webp"
alt="Aurora Lifestyle Clubhouse and Amenities"
fill
className={`object-cover transition-transform duration-200 ease-out ${zoomProps.show ? 'scale-[2.5]' : 'scale-100'}`}
style={{
@ -55,14 +59,20 @@ export default function Lifestyle() {
{/* Overlay Text - Hidden on Hover to see details */}
<div className={`absolute inset-0 bg-gradient-to-tr from-gray-800/60 to-transparent flex items-center justify-center pointer-events-none transition-opacity duration-300 ${zoomProps.show ? 'opacity-0' : 'opacity-100'}`}>
<span className="text-xl font-light tracking-widest uppercase text-white border border-white/30 px-6 py-2 rounded-full backdrop-blur-sm">
<span className="text-sm font-light tracking-widest uppercase text-white border border-white/30 px-6 py-2 rounded-full backdrop-blur-sm">
Clubhouse & Amenities
</span>
</div>
</div>
</motion.div>
{/* Content Side */}
<div className="order-1 lg:order-2 space-y-8">
<motion.div
className="order-1 lg:order-2 space-y-8"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.7, delay: 0.2 }}
>
<h2 className="text-4xl md:text-5xl font-bold tracking-tight text-foreground">
Experience the <br />
<span className="text-primary">Aurora Lifestyle.</span>
@ -100,7 +110,7 @@ export default function Lifestyle() {
</li>
))}
</ul>
</div>
</motion.div>
</div>
</div>
</section>

View File

@ -0,0 +1,88 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { motion } from "framer-motion";
const categories = [
{
id: 1,
title: "Residential Real Estate",
description: "Discover our premium residential properties featuring modern architecture, luxury amenities, and prime locations. From spacious apartments to exclusive villas, find your dream home with world-class facilities and exceptional living experiences.",
image: "/assets/images/projects/residential-real-estate.jpg",
href: "/residential-real-estate",
properties: "Residential Real Estate"
}
];
export default function ProjectsContent() {
return (
<div className="max-w-7xl mx-auto px-6 py-24">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
Project Categories
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
Browse through our carefully curated collection of properties
</p>
</motion.div>
<div className="space-y-8">
{categories.map((category, index) => (
<motion.div
key={category.id}
className="group bg-white dark:bg-gray-900 rounded-2xl overflow-hidden border border-gray-200 dark:border-gray-800 hover:border-primary dark:hover:border-primary shadow-lg hover:shadow-2xl transition-all duration-300"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.7, delay: index * 0.1 }}
>
<div className="grid grid-cols-1 md:grid-cols-12 gap-0">
{/* Image Section - Left */}
<div className="md:col-span-5 relative h-64 md:h-96 overflow-hidden">
<Image
src={category.image}
alt={category.title}
fill
className="object-cover group-hover:scale-110 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent md:bg-gradient-to-r md:from-transparent md:to-black/10" />
{/* Badge */}
<div className="absolute top-4 left-4 bg-primary text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg">
{category.properties}
</div>
</div>
{/* Content Section - Right */}
<div className="md:col-span-7 p-6 md:p-10 lg:p-12 flex flex-col justify-center">
<h3 className="text-2xl md:text-3xl lg:text-4xl font-bold text-foreground mb-4 group-hover:text-primary transition-colors">
{category.title}
</h3>
<p className="text-gray-600 dark:text-gray-400 text-sm md:text-base lg:text-lg mb-8 leading-relaxed">
{category.description}
</p>
<Link
href={category.href}
className="inline-flex items-center gap-2 px-8 py-4 bg-gradient-to-r from-primary to-blue-600 text-white rounded-xl font-semibold hover:shadow-xl transition-all duration-300 transform hover:scale-105 w-fit"
>
View More
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</Link>
</div>
</div>
</motion.div>
))}
</div>
</div>
);
}

View File

@ -3,6 +3,7 @@
import { useState, useRef } from "react";
import Link from "next/link";
import { properties } from "@/data/properties";
import { motion } from "framer-motion";
type Category = "Apartments" | "Premium Homes" | "Luxury";
@ -29,14 +30,20 @@ export default function Properties({ layout = "slider" }: PropertiesProps) {
return (
<section id="projects" className="py-24 bg-white dark:bg-black overflow-hidden">
<div className="max-w-7xl mx-auto px-6">
<div className="text-center mb-12">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6 }}
>
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
Our Signature Projects
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
Discover a home that complements your lifestyle.
</p>
</div>
</motion.div>
{/* Tabs */}
<div className="flex justify-center mb-12">
@ -45,7 +52,7 @@ export default function Properties({ layout = "slider" }: PropertiesProps) {
<button
key={category}
onClick={() => setActiveTab(category)}
className={`px-6 py-2.5 rounded-full text-sm font-medium transition-all duration-300 ${activeTab === category
className={`px-4 py-2.5 rounded-full text-sm font-medium transition-all duration-300 ${activeTab === category
? "bg-white dark:bg-gray-700 text-foreground shadow-sm"
: "text-gray-500 dark:text-gray-400 hover:text-foreground"
}`}
@ -92,22 +99,26 @@ export default function Properties({ layout = "slider" }: PropertiesProps) {
if (activeTab !== "All" && index > 0) return null;
return (
<div
<motion.div
key={property.id}
className={activeTab === "All"
? "min-w-[300px] md:min-w-[380px] snap-center"
? "flex-shrink-0 snap-center w-full md:w-[calc(50%-12px)] lg:w-[calc(33.333%-16px)]"
: "flex-shrink-0 w-full max-w-md mx-auto snap-center"
}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.6, delay: index * 0.15 }}
>
<Link
href={`/properties/${property.id}`}
href={`/residential-real-estate/${property.slug}`}
className="group/card block bg-white dark:bg-gray-900 rounded-2xl overflow-hidden border border-gray-100 dark:border-gray-800 hover:border-gray-200 dark:hover:border-gray-700 shadow-lg hover:shadow-2xl transition-all duration-300 h-full flex flex-col"
>
{/* Image */}
<div className={activeTab === "All" ? "h-64 w-full relative overflow-hidden" : "h-80 w-full relative overflow-hidden"}>
<div className={`absolute inset-0 ${property.image.startsWith('/') ? '' : property.image}`} />
{property.image.startsWith('/') && (
<img src={property.image} alt={property.title} className="w-full h-full object-cover transition-transform duration-500 group-hover/card:scale-110" />
<img src={property.image} alt={`Main view of ${property.title}`} className="w-full h-full object-cover transition-transform duration-500 group-hover/card:scale-110" />
)}
<div className="absolute top-4 left-4 bg-white/90 dark:bg-black/80 backdrop-blur-sm px-3 py-1 rounded-full text-xs font-semibold text-foreground uppercase tracking-wider">
@ -144,7 +155,7 @@ export default function Properties({ layout = "slider" }: PropertiesProps) {
</div>
</div>
</Link>
</div>
</motion.div>
);
})}
</div>
@ -154,7 +165,7 @@ export default function Properties({ layout = "slider" }: PropertiesProps) {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredProperties.map((property) => (
<Link
href={`/properties/${property.id}`}
href={`/residential-real-estate/${property.slug}`}
key={property.id}
className="group bg-white dark:bg-gray-900 rounded-2xl overflow-hidden border border-gray-100 dark:border-gray-800 hover:border-gray-200 dark:hover:border-gray-700 shadow-sm hover:shadow-xl transition-all duration-300 flex flex-col"
>
@ -162,7 +173,7 @@ export default function Properties({ layout = "slider" }: PropertiesProps) {
<div className={`h-64 w-full relative overflow-hidden`}>
<div className={`absolute inset-0 ${property.image.startsWith('/') ? '' : property.image}`} />
{property.image.startsWith('/') && (
<img src={property.image} alt={property.title} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
<img src={property.image} alt={`Main view of ${property.title}`} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
)}
<div className="absolute top-4 left-4 bg-white/90 dark:bg-black/80 backdrop-blur-sm px-3 py-1 rounded-full text-xs font-semibold text-foreground uppercase tracking-wider">

View File

@ -1,15 +1,62 @@
"use client";
import { useState } from "react";
import Header from "@/components/Header";
import { useState, useEffect } from "react";
import { useSearchParams } from "next/navigation";
import Footer from "@/components/Footer";
import InnerBanner from "@/components/InnerBanner";
import PropertyFilters, { FilterState } from "@/components/PropertyFilters";
import PropertyCard from "@/components/PropertyCard";
import { properties } from "@/data/properties";
import { motion } from "framer-motion";
export default function PropertiesClient() {
interface PropertiesClientProps {
initialFilters?: Partial<FilterState>;
headerRenderer?: (props: { filteredCount: number; totalCount: number }) => React.ReactNode;
bannerTitle?: string;
bannerSubtitle?: string;
bannerBackgroundImage?: string;
}
export default function PropertiesClient({
initialFilters,
headerRenderer,
bannerTitle = "Our Properties",
bannerSubtitle = "Discover your dream home from our exclusive collection of premium properties",
bannerBackgroundImage = "/assets/images/projects/projects-banner.webp"
}: PropertiesClientProps = {}) {
const searchParams = useSearchParams();
const [filteredProperties, setFilteredProperties] = useState(properties);
const [initializedFilters, setInitializedFilters] = useState<Partial<FilterState> | undefined>(undefined);
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
const search = searchParams.get("search") || "";
const type = searchParams.get("type") || "all";
const initial: Partial<FilterState> = {
search,
type,
budgetMin: "",
budgetMax: "",
bhk: "all",
sortBy: "popularity"
};
setInitializedFilters(initial);
// Initial filter run
const filters = {
search,
type,
budgetMin: "",
budgetMax: "",
bhk: "all",
sortBy: "popularity"
};
handleFilterChange(filters);
setIsInitialized(true);
}, [searchParams]);
const handleFilterChange = (filters: FilterState) => {
let filtered = [...properties];
@ -18,7 +65,8 @@ export default function PropertiesClient() {
if (filters.search) {
filtered = filtered.filter(p =>
p.title.toLowerCase().includes(filters.search.toLowerCase()) ||
p.location.toLowerCase().includes(filters.search.toLowerCase())
p.location.toLowerCase().includes(filters.search.toLowerCase()) ||
p.builder?.name.toLowerCase().includes(filters.search.toLowerCase())
);
}
@ -61,30 +109,53 @@ export default function PropertiesClient() {
setFilteredProperties(filtered);
};
if (!isInitialized) return null; // Logic to wait for params
return (
<div className="min-h-screen bg-gray-50 dark:bg-black">
<Header />
<InnerBanner
title="Our Properties"
subtitle="Discover your dream home from our exclusive collection of premium properties"
title={bannerTitle}
subtitle={bannerSubtitle}
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "Properties" }
]}
backgroundImage={bannerBackgroundImage}
/>
<div>
<PropertyFilters onFilterChange={handleFilterChange} />
<PropertyFilters onFilterChange={handleFilterChange} initialFilters={initializedFilters} />
<div className="max-w-7xl mx-auto px-6 py-12">
{headerRenderer ? (
headerRenderer({ filteredCount: filteredProperties.length, totalCount: properties.length })
) : (
<motion.h2
className="text-3xl font-bold text-foreground mb-8"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
Browse Our Properties
</motion.h2>
)}
{/* Property Grid */}
{filteredProperties.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredProperties.map((property) => (
<PropertyCard key={property.id} property={property} />
))}
</div>
{filteredProperties.map((property, index) => (
<motion.div
key={property.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<PropertyCard property={property} />
</motion.div>
))} </div>
) : (
<div className="text-center py-20">
<svg className="w-20 h-20 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View File

@ -62,97 +62,109 @@ export default function PropertyCard({ property }: PropertyCardProps) {
setIsWishlisted(false);
} else {
// Add to wishlist
wishlist.push(property.id);
localStorage.setItem("wishlist", JSON.stringify(wishlist));
const updated = [...wishlist, property.id];
localStorage.setItem("wishlist", JSON.stringify(updated));
setIsWishlisted(true);
}
};
return (
<Link href={`/properties/${property.id}`}>
<div className="group bg-white dark:bg-gray-900 rounded-2xl overflow-hidden shadow-md hover:shadow-2xl transition-all duration-300 border border-gray-200 dark:border-gray-800">
{/* Image Section */}
<div className="relative h-64 overflow-hidden">
<Image
src={property.image}
alt={property.title}
fill
className="object-cover group-hover:scale-110 transition-transform duration-500"
/>
<Link
href={`/residential-real-estate/${property.slug}`}
className="group block bg-white dark:bg-gray-900 rounded-2xl overflow-hidden border border-gray-100 dark:border-gray-800 hover:border-gray-200 dark:hover:border-gray-700 shadow-sm hover:shadow-xl transition-all duration-300"
>
<div className="relative h-48 md:h-64 w-full overflow-hidden">
<Image
src={property.image}
alt={`Property view of ${property.title}`}
fill
className="object-cover group-hover:scale-110 transition-transform duration-500"
/>
{/* Status Badge */}
{property.status && (
<div className={`absolute top-4 left-4 px-3 py-1 rounded-full text-xs font-semibold ${property.status === "Sold Out"
? "bg-red-500 text-white"
: property.status === "New Launch"
? "bg-green-500 text-white"
: "bg-white/90 text-gray-900"
}`}>
{property.status}
</div>
)}
{/* Action Buttons */}
<div className="absolute top-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={handleCompareClick}
className={`p-2 rounded-full shadow-lg transition-all ${inCompare
? "bg-primary text-white scale-110"
: "bg-white hover:bg-gray-100 text-gray-700"
}`}
aria-label={inCompare ? "Remove from compare" : "Add to compare"}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</button>
<button
onClick={handleShareClick}
className="p-2 bg-white rounded-full shadow-lg hover:bg-gray-100 transition-colors"
aria-label="Share"
>
<svg className="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
</svg>
</button>
<button
onClick={handleWishlistClick}
className={`p-2 rounded-full shadow-lg transition-all ${isWishlisted
? "bg-red-500 text-white scale-110"
: "bg-white hover:bg-gray-100 text-gray-700"
}`}
aria-label={isWishlisted ? "Remove from wishlist" : "Add to wishlist"}
>
<svg className="w-5 h-5" fill={isWishlisted ? "currentColor" : "none"} stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
</button>
{/* Status Badge */}
{property.status && (
<div className={`absolute top-3 left-3 md:top-4 md:left-4 px-2 py-0.5 md:px-3 md:py-1 rounded-full text-[10px] md:text-xs font-semibold ${property.status === "Sold Out"
? "bg-red-500 text-white"
: property.status === "New Launch"
? "bg-green-500 text-white"
: "bg-white/90 text-gray-900"
}`}>
{property.status}
</div>
)}
{/* Action Buttons */}
<div className="absolute top-3 right-3 md:top-4 md:right-4 flex gap-1.5 md:gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={handleCompareClick}
className={`p-1.5 md:p-2 rounded-full shadow-lg transition-all ${inCompare
? "bg-primary text-white scale-110"
: "bg-white hover:bg-gray-100 text-gray-700"
}`}
aria-label={inCompare ? "Remove from compare" : "Add to compare"}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-4 h-4 md:w-5 md:h-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8 7h8v10H8zM4 11h8v10H4z"
/>
</svg>
</button>
<button
onClick={handleShareClick}
className="p-1.5 md:p-2 bg-white rounded-full shadow-lg hover:bg-gray-100 transition-colors"
aria-label="Share"
>
<svg className="w-4 h-4 md:w-5 md:h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
</svg>
</button>
<button
onClick={handleWishlistClick}
className={`p-1.5 md:p-2 rounded-full shadow-lg transition-all ${isWishlisted
? "bg-red-500 text-white scale-110"
: "bg-white hover:bg-gray-100 text-gray-700"
}`}
aria-label={isWishlisted ? "Remove from wishlist" : "Add to wishlist"}
>
<svg className="w-4 h-4 md:w-5 md:h-5" fill={isWishlisted ? "currentColor" : "none"} stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
</button>
</div>
</div>
{/* Content Section */}
<div className="p-4 md:p-6">
<h3 className="text-lg md:text-xl font-bold text-foreground mb-1 md:mb-2 group-hover:text-primary transition-colors line-clamp-1">
{property.title}
</h3>
<div className="flex items-center text-gray-600 dark:text-gray-400 text-xs md:text-sm mb-3 md:mb-4">
<svg className="w-3.5 h-3.5 md:w-4 md:h-4 mr-1 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span className="truncate">{property.location}</span>
</div>
{/* Content Section */}
<div className="p-6">
<h3 className="text-xl font-bold text-foreground mb-2 group-hover:text-primary transition-colors">
{property.title}
</h3>
<div className="flex items-center text-gray-600 dark:text-gray-400 text-sm mb-4">
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
{property.location}
<div className="flex items-center justify-between pt-3 md:pt-4 border-t border-gray-200 dark:border-gray-800">
<div className="min-w-0">
<div className="text-lg md:text-2xl font-bold text-primary whitespace-nowrap">{property.price}</div>
<div className="text-[10px] md:text-sm text-gray-500 dark:text-gray-400 truncate">{property.overview.size}</div>
</div>
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-800">
<div>
<div className="text-2xl font-bold text-primary">{property.price}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">{property.overview.size}</div>
</div>
<div className="text-right">
<div className="text-sm font-semibold text-foreground">{property.overview.bhk}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Possession: {property.overview.possession}</div>
</div>
<div className="text-right flex-shrink-0">
<div className="text-xs md:text-sm font-semibold text-foreground">{property.overview.bhk}</div>
<div className="text-[10px] md:text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">Poss: {property.overview.possession.split(' ').pop()}</div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ import { useState } from "react";
interface PropertyFiltersProps {
onFilterChange: (filters: FilterState) => void;
initialFilters?: Partial<FilterState>;
}
export interface FilterState {
@ -15,14 +16,14 @@ export interface FilterState {
sortBy: string;
}
export default function PropertyFilters({ onFilterChange }: PropertyFiltersProps) {
export default function PropertyFilters({ onFilterChange, initialFilters }: PropertyFiltersProps) {
const [filters, setFilters] = useState<FilterState>({
search: "",
type: "all",
budgetMin: "",
budgetMax: "",
bhk: "all",
sortBy: "popularity"
search: initialFilters?.search || "",
type: initialFilters?.type || "all",
budgetMin: initialFilters?.budgetMin || "",
budgetMax: initialFilters?.budgetMax || "",
bhk: initialFilters?.bhk || "all",
sortBy: initialFilters?.sortBy || "popularity"
});
const [showFilters, setShowFilters] = useState(false);
@ -49,10 +50,10 @@ export default function PropertyFilters({ onFilterChange }: PropertyFiltersProps
const hasActiveFilters = filters.search || filters.type !== "all" || filters.budgetMin || filters.budgetMax || filters.bhk !== "all" || filters.sortBy !== "popularity";
return (
<div className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 sticky top-0 z-40">
<div className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 lg:sticky lg:top-28 lg:z-40">
<div className="max-w-7xl mx-auto px-6 py-4">
{/* Main Filter Bar */}
<div className="flex flex-col md:flex-row gap-4 items-center">
<div className="flex flex-col [@media(min-width:920px)]:flex-row gap-4 items-center">
{/* Search */}
<div className="flex-1 w-full">
<div className="relative">

View File

@ -1,151 +1,313 @@
"use client";
import { useState } from "react";
import Image from "next/image";
interface PropertyGalleryProps {
images: string[];
title: string;
}
export default function PropertyGallery({ images, title }: PropertyGalleryProps) {
const [activeImage, setActiveImage] = useState(0);
const [showLightbox, setShowLightbox] = useState(false);
const [lightboxIndex, setLightboxIndex] = useState(0);
const openLightbox = (index: number) => {
setLightboxIndex(index);
setShowLightbox(true);
document.body.style.overflow = 'hidden';
};
const closeLightbox = () => {
setShowLightbox(false);
document.body.style.overflow = 'unset';
};
// Ensure we have at least 5 images for the gallery (1 big + 4 small)
const displayImages = [...images];
while (displayImages.length < 5) {
displayImages.push("/assets/images/projects/details/barca-2.webp");
}
const nextImage = () => {
setLightboxIndex((prev) => (prev + 1) % images.length);
setLightboxIndex((prev) => (prev + 1) % displayImages.length);
};
const prevImage = () => {
setLightboxIndex((prev) => (prev - 1 + images.length) % images.length);
setLightboxIndex((prev) => (prev - 1 + displayImages.length) % displayImages.length);
};
return (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div className="md:col-span-2 relative h-[500px] rounded-2xl overflow-hidden group cursor-pointer" onClick={() => openLightbox(activeImage)}>
<Image
src={images[activeImage]}
src={displayImages[activeImage]}
alt={title}
fill
className="object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-300" />
<div className="absolute bottom-4 right-4 bg-black/70 text-white px-3 py-1.5 rounded-lg text-sm opacity-0 group-hover:opacity-100 transition-opacity">
Click to enlarge
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-1 gap-4">
{images.slice(0, 2).map((img, idx) => (
<div className="grid grid-cols-2 grid-rows-2 gap-4 h-[500px]">
{displayImages.slice(0, 3).map((img, idx) => (
<div
key={idx}
onClick={() => setActiveImage(idx)}
className={`relative h-[160px] rounded-xl overflow-hidden cursor-pointer transition-all duration-300 ${activeImage === idx ? 'ring-4 ring-primary scale-105' : 'hover:scale-105'
className={`relative w-full h-full rounded-xl overflow-hidden cursor-pointer transition-all duration-300 ${activeImage === idx ? 'ring-4 ring-primary scale-95' : 'hover:scale-95'
}`}
>
<Image src={img} alt={`View ${idx + 1}`} fill className="object-cover" />
<Image src={img} alt={`View detail ${idx + 1} of ${title}`} fill className="object-cover" />
</div>
))}
{/* View All Photos Button */}
{/* View All Photos Button (4th slot) */}
<div
onClick={() => openLightbox(0)}
className="relative h-[160px] rounded-xl overflow-hidden cursor-pointer group"
className="relative w-full h-full rounded-xl overflow-hidden cursor-pointer group"
>
<Image
src={images[2] || images[0]}
alt="View all photos"
src={displayImages[3]}
alt={`View all photos of ${title}`}
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-black/60 group-hover:bg-black/70 transition-colors flex flex-col items-center justify-center text-white">
<svg className="w-10 h-10 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span className="font-semibold text-lg">View All Photos</span>
<span className="text-sm opacity-90">({images.length} images)</span>
<span className="text-sm opacity-90">({displayImages.length} images)</span>
</div>
</div>
</div>
</div>
{/* Lightbox Modal */}
{showLightbox && (
<div className="fixed inset-0 z-50 bg-black/95 flex items-center justify-center">
{/* Close Button */}
<button
onClick={closeLightbox}
className="absolute top-4 right-4 text-white hover:text-gray-300 transition-colors z-10"
aria-label="Close gallery"
>
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/* Image Counter */}
<div className="absolute top-4 left-1/2 -translate-x-1/2 text-white text-lg font-medium bg-black/50 px-4 py-2 rounded-full">
{lightboxIndex + 1} / {images.length}
{lightboxIndex + 1} / {displayImages.length}
</div>
{/* Previous Button */}
<button
onClick={prevImage}
className="absolute left-4 text-white hover:text-gray-300 transition-colors p-2 bg-black/50 rounded-full hover:bg-black/70"
aria-label="Previous image"
>
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
{/* Main Image */}
<div className="relative w-full h-full max-w-6xl max-h-[90vh] mx-4">
<Image
src={images[lightboxIndex]}
src={displayImages[lightboxIndex]}
alt={`${title} - Image ${lightboxIndex + 1}`}
fill
className="object-contain"
/>
</div>
{/* Next Button */}
<button
onClick={nextImage}
className="absolute right-4 text-white hover:text-gray-300 transition-colors p-2 bg-black/50 rounded-full hover:bg-black/70"
aria-label="Next image"
>
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Thumbnail Strip */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 overflow-x-auto max-w-[90vw] px-4 py-2 bg-black/50 rounded-lg hide-scrollbar">
{images.map((img, idx) => (
{displayImages.map((img, idx) => (
<div
key={idx}
onClick={() => setLightboxIndex(idx)}
className={`relative w-20 h-20 flex-shrink-0 rounded-lg overflow-hidden cursor-pointer transition-all ${lightboxIndex === idx ? 'ring-4 ring-white scale-110' : 'opacity-60 hover:opacity-100'
}`}
>
<Image src={img} alt={`Thumbnail ${idx + 1}`} fill className="object-cover" />
<Image src={img} alt={`Thumbnail ${idx + 1} of ${title}`} fill className="object-cover" />
</div>
))}
</div>
</div>
)}
</>
);
}
}

View File

@ -1,6 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
interface PropertyNavProps {
sections: { id: string; label: string }[];
@ -8,6 +8,7 @@ interface PropertyNavProps {
export default function PropertyNav({ sections }: PropertyNavProps) {
const [activeSection, setActiveSection] = useState(sections[0]?.id || "");
const navRef = useRef<HTMLElement>(null);
useEffect(() => {
const handleScroll = () => {
@ -29,6 +30,19 @@ export default function PropertyNav({ sections }: PropertyNavProps) {
return () => window.removeEventListener("scroll", handleScroll);
}, [sections]);
useEffect(() => {
if (activeSection && navRef.current) {
const activeBtn = document.getElementById(`nav-btn-${activeSection}`);
if (activeBtn) {
activeBtn.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
}
}, [activeSection]);
const scrollToSection = (id: string) => {
const element = document.getElementById(id);
if (element) {
@ -44,16 +58,18 @@ export default function PropertyNav({ sections }: PropertyNavProps) {
};
return (
<div className="sticky top-20 z-30 bg-white/95 dark:bg-gray-900/95 backdrop-blur-md border-b border-gray-200 dark:border-gray-800 shadow-sm">
<div className="sticky top-28 z-30 bg-white/95 dark:bg-gray-900/95 backdrop-blur-md border-b border-gray-200 dark:border-gray-800 shadow-sm">
<div className="max-w-7xl mx-auto px-6">
<nav className="flex gap-1 overflow-x-auto hide-scrollbar py-3">
<nav ref={navRef} className="flex gap-1 overflow-x-auto hide-scrollbar py-3">
{sections.map((section) => (
<button
key={section.id}
id={`nav-btn-${section.id}`}
onClick={() => scrollToSection(section.id)}
className={`px-6 py-2 rounded-lg font-medium whitespace-nowrap transition-all duration-200 ${activeSection === section.id
? "bg-primary text-white shadow-md"
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
? "bg-primary text-white shadow-md"
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
}`}
>
{section.label}

View File

@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import Image from "next/image";
import { ThemeToggle } from "@/components/ThemeToggle";
@ -11,6 +11,8 @@ interface SidebarProps {
}
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
const [isProjectsOpen, setIsProjectsOpen] = useState(false);
// Prevent scrolling when sidebar is open
useEffect(() => {
if (isOpen) {
@ -42,7 +44,7 @@ export default function Sidebar({ isOpen, onClose }: SidebarProps) {
<div className="relative w-40 h-12">
<Image
src="/assets/images/blue-logo.png"
alt="Sky and Soil Logo"
alt="Sky and Soil Sidebar Logo"
fill
className="object-contain"
/>
@ -76,13 +78,45 @@ export default function Sidebar({ isOpen, onClose }: SidebarProps) {
>
About
</Link>
<Link
href="/projects"
className="text-lg font-medium text-gray-600 dark:text-gray-300 hover:text-primary transition-colors"
onClick={onClose}
>
Projects
</Link>
<div className="flex flex-col space-y-2">
<div className="flex items-center justify-between">
<Link
href="/projects"
className="text-lg font-medium text-gray-600 dark:text-gray-300 hover:text-primary transition-colors"
onClick={onClose}
>
Projects
</Link>
<button
onClick={() => setIsProjectsOpen(!isProjectsOpen)}
className="p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={`w-5 h-5 transition-transform duration-200 ${isProjectsOpen ? "rotate-180" : ""}`}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
/>
</svg>
</button>
</div>
<div className={`overflow-hidden transition-all duration-300 ${isProjectsOpen ? "max-h-40 opacity-100" : "max-h-0 opacity-0"}`}>
<Link
href="/residential-real-estate"
className="block text-base font-medium text-gray-500 dark:text-gray-400 hover:text-primary transition-colors pl-4 py-2"
onClick={onClose}
>
Residential Real Estate
</Link>
</div>
</div>
<Link
href="/lifestyle"
className="text-lg font-medium text-gray-600 dark:text-gray-300 hover:text-primary transition-colors"

Some files were not shown because too many files have changed in this diff Show More