Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46fb03b392 | ||
|
|
3456b6e2b5 | ||
|
|
d833c56ddd | ||
|
|
37984cfea8 | ||
|
|
005dee4672 | ||
|
|
109941612b | ||
|
|
1b0ac9fbe9 | ||
|
|
68b535a5d8 | ||
|
|
90b3683593 | ||
|
|
cbb953d622 | ||
|
|
152022c68f | ||
|
|
b8a2d91728 | ||
|
|
6981041d45 | ||
|
|
e97cfaffb4 | ||
|
|
2e2b776085 | ||
|
|
5bb4efd6b8 | ||
|
|
92e2b8e210 | ||
|
|
12b2e77449 | ||
|
|
3f53ee1ebd | ||
|
|
ce24e8e7fc | ||
|
|
253a7aa5d2 | ||
|
|
01d4df18a4 | ||
|
|
d6d14902c9 | ||
|
|
0c3f4a1553 | ||
|
|
2f5aee6d4b | ||
|
|
725590574e | ||
|
|
3c6a8ac5ff | ||
|
|
8a1c1c414d | ||
|
|
2a9404d1e5 | ||
|
|
1b4729145a | ||
|
|
debfdc8e2b | ||
|
|
04c6ff164e | ||
| 70f2797703 | |||
| bcd517eb05 | |||
| 5e48dad541 | |||
| ace556fa59 | |||
| 0713049d30 | |||
| 9249b1fa29 | |||
| b75f15bc22 | |||
| 943f440eae |
34
build_log_sky.txt
Normal 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
@ -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",
|
||||
|
||||
18
package.json
@ -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
@ -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>
|
||||
BIN
public/assets/images/about/about-banner.webp
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/assets/images/about/contact-banner.webp
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
public/assets/images/about/lifestyle-banner.webp
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
public/assets/images/about/privacy-banner.webp
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
public/assets/images/about/terms-banner.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
public/assets/images/home/about.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
public/assets/images/home/back.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/assets/images/home/bottom.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/assets/images/home/experience.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
public/assets/images/home/faq-2.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
public/assets/images/home/faq-3.webp
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
public/assets/images/home/faq.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/assets/images/home/front.webp
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/assets/images/home/left.webp
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
public/assets/images/home/ready.webp
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
public/assets/images/home/right.webp
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
public/assets/images/home/top.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
public/assets/images/home/top.webp
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
public/assets/images/home/where.webp
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
public/assets/images/home/whereNew.jpg
Normal file
|
After Width: | Height: | Size: 328 KiB |
BIN
public/assets/images/home/whereNew.webp
Normal file
|
After Width: | Height: | Size: 399 KiB |
BIN
public/assets/images/home/why/customer-first.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/assets/images/home/why/location.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/assets/images/home/why/smart-home.webp
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/assets/images/home/why/transparent-deals.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/assets/images/image.png
Normal file
|
After Width: | Height: | Size: 913 KiB |
BIN
public/assets/images/map-placeholder.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
BIN
public/assets/images/map-placeholder.webp
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
public/assets/images/master-plan.png
Normal file
|
After Width: | Height: | Size: 295 KiB |
BIN
public/assets/images/projects/barca.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/assets/images/projects/details/barca-1.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/assets/images/projects/details/barca-2.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/assets/images/projects/details/barca-3.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/assets/images/projects/details/hoskote-1.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/assets/images/projects/details/hoskote-2.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/assets/images/projects/details/hoskote-3.webp
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
public/assets/images/projects/details/lakeside-1.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/assets/images/projects/details/lakeside-2.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/assets/images/projects/details/lakeside-3.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/assets/images/projects/details/tiara-1.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/assets/images/projects/details/tiara-2.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/assets/images/projects/details/tiara-3.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/assets/images/projects/details/woods-1.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/assets/images/projects/details/woods-2.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/assets/images/projects/details/woods-3.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/assets/images/projects/godrej-hoskote.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/assets/images/projects/godrej-lakeside.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/assets/images/projects/godrej-tiara.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/assets/images/projects/godrej-woods.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/assets/images/projects/projects-banner.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/assets/images/projects/residential-real-estate.jpg
Normal file
|
After Width: | Height: | Size: 45 KiB |
4
public/robots.txt
Normal file
@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://skyandsoil.metatronnest.com/sitemap.xml
|
||||
1
public/sitemap.xml
Normal 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
@ -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>
|
||||
30
script/copy-server-config.cjs
Normal 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);
|
||||
}
|
||||
});
|
||||
72
script/generate-sitemap.cjs
Normal 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();
|
||||
1
script/image_alt_issues.csv
Normal file
@ -0,0 +1 @@
|
||||
Page URL,Image Src,Alt Text,Issue Type
|
||||
|
261
script/seo-test-selenium.cjs
Normal 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}`);
|
||||
})();
|
||||
@ -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 />
|
||||
|
||||
16
src/app/buy/property-for-sale-in-bengaluru/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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 />
|
||||
|
||||
162
src/app/privacy-policy/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
39
src/app/residential-real-estate/[slug]/page.tsx
Normal 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} />;
|
||||
}
|
||||
11
src/app/residential-real-estate/page.tsx
Normal 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 />;
|
||||
}
|
||||
173
src/app/terms-of-service/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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 >
|
||||
);
|
||||
}
|
||||
|
||||
65
src/components/BengaluruHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
13
src/components/BengaluruPropertiesClientWrapper.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
312
src/components/ConnectivityMap.tsx
Normal 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: '© <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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
62
src/components/ContactCards.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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>© {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>
|
||||
|
||||
@ -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)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
11
src/components/HeaderClient.tsx
Normal 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 />;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
88
src/components/ProjectsContent.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
2502
src/components/PropertyDetailClient.tsx
Normal 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">
|
||||
|
||||
@ -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>
|
||||
|
||||
)}
|
||||
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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}
|
||||
|
||||
@ -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"
|
||||
|
||||