Compare commits
2 Commits
355762fdff
...
b75f15bc22
| Author | SHA1 | Date | |
|---|---|---|---|
| b75f15bc22 | |||
| 943f440eae |
423
package-lock.json
generated
@ -8,11 +8,18 @@
|
||||
"name": "antigravity_dev",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@types/react-google-recaptcha": "^2.1.9",
|
||||
"axios": "^1.13.2",
|
||||
"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",
|
||||
"selenium-webdriver": "^4.38.0",
|
||||
"sitemap": "^9.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
@ -278,6 +285,12 @@
|
||||
"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==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
|
||||
@ -1551,7 +1564,6 @@
|
||||
"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 +1573,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 +1588,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 +2232,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 +2432,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 +2464,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 +2583,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 +2682,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 +2708,12 @@
|
||||
"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==",
|
||||
"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 +2733,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 +2857,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 +2893,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 +3004,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 +3013,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 +3050,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 +3062,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 +3676,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 +3712,26 @@
|
||||
"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/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 +3792,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 +3816,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 +3903,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 +3981,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 +3993,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 +4008,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 +4033,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 +4052,12 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"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 +4085,12 @@
|
||||
"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==",
|
||||
"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 +4567,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 +4645,18 @@
|
||||
"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==",
|
||||
"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",
|
||||
@ -4568,6 +4701,15 @@
|
||||
"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==",
|
||||
"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 +4998,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 +5016,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 +5039,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 +5068,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",
|
||||
@ -5091,7 +5261,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 +5447,12 @@
|
||||
"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==",
|
||||
"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 +5561,16 @@
|
||||
"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==",
|
||||
"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 +5578,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 +5624,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,11 +5649,44 @@
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
@ -5597,6 +5829,12 @@
|
||||
"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==",
|
||||
"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 +5870,43 @@
|
||||
"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==",
|
||||
"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 +5966,12 @@
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
@ -5854,6 +6129,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 +6193,15 @@
|
||||
"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==",
|
||||
"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 +6464,15 @@
|
||||
"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==",
|
||||
"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 +6683,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 +6761,12 @@
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@ -6550,6 +6882,49 @@
|
||||
"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==",
|
||||
"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",
|
||||
|
||||
@ -9,11 +9,18 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react-google-recaptcha": "^2.1.9",
|
||||
"axios": "^1.13.2",
|
||||
"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",
|
||||
"selenium-webdriver": "^4.38.0",
|
||||
"sitemap": "^9.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
||||
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/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.webp
Normal file
|
After Width: | Height: | Size: 8.9 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/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 |
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/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/properties/1/</loc></url><url><loc>https://skyandsoil.metatronnest.com/properties/2/</loc></url><url><loc>https://skyandsoil.metatronnest.com/properties/3/</loc></url><url><loc>https://skyandsoil.metatronnest.com/properties/4/</loc></url><url><loc>https://skyandsoil.metatronnest.com/properties/5/</loc></url></urlset>
|
||||
62
script/generate-sitemap.cjs
Normal file
@ -0,0 +1,62 @@
|
||||
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: "/lifestyle/" },
|
||||
{ url: "/contact/" },
|
||||
{ url: "/compare/" },
|
||||
];
|
||||
|
||||
// ✅ Dynamic properties (IDs from src/data/properties.ts)
|
||||
const propertyIds = [1, 2, 3, 4, 5];
|
||||
const propertyLinks = propertyIds.map(id => ({ url: `/properties/${id}/` }));
|
||||
|
||||
// Combine static + dynamic 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}`);
|
||||
})();
|
||||
@ -22,6 +22,7 @@ export default function AboutPage() {
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "About" }
|
||||
]}
|
||||
backgroundImage="/assets/images/about/about-banner.webp"
|
||||
/>
|
||||
<div>
|
||||
<About />
|
||||
|
||||
@ -20,6 +20,7 @@ export default function ContactPage() {
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Contact" }
|
||||
]}
|
||||
backgroundImage="/assets/images/about/contact-banner.webp"
|
||||
/>
|
||||
<div>
|
||||
<ContactCTA />
|
||||
|
||||
@ -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,12 +8,25 @@ 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";
|
||||
@ -28,8 +41,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"
|
||||
|
||||
@ -20,6 +20,7 @@ export default function LifestylePage() {
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Lifestyle" }
|
||||
]}
|
||||
backgroundImage="/assets/images/about/lifestyle-banner.webp"
|
||||
/>
|
||||
<div>
|
||||
<Lifestyle />
|
||||
|
||||
34
src/app/properties/[id]/layout.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Metadata } from "next";
|
||||
import { properties } from "@/data/properties";
|
||||
|
||||
// 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`,
|
||||
description: `Explore ${property.title} in ${property.location}. ${property.overview.bhk} ${property.category} starting at ${property.price}.`,
|
||||
};
|
||||
}
|
||||
|
||||
export default function PropertyLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
26
src/app/properties/[id]/metadata.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Metadata } from "next";
|
||||
import { properties } from "@/data/properties";
|
||||
|
||||
// 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`,
|
||||
description: `Explore ${property.title} in ${property.location}. ${property.overview.bhk} ${property.category} starting at ${property.price}.`,
|
||||
};
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
import { use } from "react";
|
||||
"use client";
|
||||
|
||||
import { use, useState, ChangeEvent, FormEvent, useEffect } from "react";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
import Image from "next/image";
|
||||
@ -7,31 +9,19 @@ import PropertyNav from "@/components/PropertyNav";
|
||||
import InnerBanner from "@/components/InnerBanner";
|
||||
import { properties } from "@/data/properties";
|
||||
import { notFound } from "next/navigation";
|
||||
import axios from "axios";
|
||||
|
||||
import { Metadata } from "next";
|
||||
|
||||
// Required for static site generation with dynamic routes
|
||||
export function generateStaticParams() {
|
||||
return properties.map((property) => ({
|
||||
id: property.id.toString(),
|
||||
}));
|
||||
interface FormData {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
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}.`,
|
||||
};
|
||||
interface FormErrors {
|
||||
name?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
const sections = [
|
||||
@ -43,10 +33,98 @@ const sections = [
|
||||
{ id: "pricing", label: "Pricing" },
|
||||
];
|
||||
|
||||
export default async function PropertyDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const resolvedParams = await params;
|
||||
export default function PropertyDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const resolvedParams = use(params);
|
||||
const property = properties.find(p => p.id === parseInt(resolvedParams.id));
|
||||
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
message: "",
|
||||
});
|
||||
|
||||
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 | HTMLTextAreaElement>) => {
|
||||
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 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.";
|
||||
|
||||
setFormErrors(errors);
|
||||
if (Object.keys(errors).length > 0) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
const emailData = {
|
||||
name: formData.name,
|
||||
phone: formData.phone,
|
||||
email: formData.email,
|
||||
subject: `Property Inquiry: ${property?.title} from ${formData.name}`,
|
||||
message: `<strong>Property Inquiry</strong><br /><br />
|
||||
<strong>Property:</strong> ${property?.title}<br />
|
||||
<strong>Location:</strong> ${property?.location}<br />
|
||||
<strong>Price:</strong> ${property?.price}<br /><br />
|
||||
<strong>Customer Details:</strong><br />
|
||||
Name: ${formData.name}<br />
|
||||
Phone: ${formData.phone}<br />
|
||||
Email: ${formData.email}<br />
|
||||
Message: ${formData.message || 'N/A'}`,
|
||||
to: "hello@skyandsoil.com",
|
||||
senderName: "Sky and Soil Property Inquiry",
|
||||
};
|
||||
|
||||
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 || "Request sent successfully! We will contact you soon.",
|
||||
});
|
||||
|
||||
setFormData({ name: "", email: "", phone: "", message: "" });
|
||||
setFormErrors({});
|
||||
} catch (error) {
|
||||
setAlert({
|
||||
show: true,
|
||||
type: "danger",
|
||||
message: "Failed to send request. 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]);
|
||||
|
||||
if (!property) {
|
||||
notFound();
|
||||
}
|
||||
@ -228,32 +306,61 @@ export default async function PropertyDetailPage({ params }: { params: Promise<{
|
||||
{/* 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"
|
||||
/>
|
||||
|
||||
{alert.show && (
|
||||
<div className={`mb-4 p-3 rounded-lg 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>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder="Your Name"
|
||||
className={`w-full px-4 py-3 border ${formErrors.name ? 'border-red-500' : '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`}
|
||||
/>
|
||||
{formErrors.name && <small className="text-red-500 text-xs ml-1">{formErrors.name}</small>}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
placeholder="Email Address"
|
||||
className={`w-full px-4 py-3 border ${formErrors.email ? 'border-red-500' : '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`}
|
||||
/>
|
||||
{formErrors.email && <small className="text-red-500 text-xs ml-1">{formErrors.email}</small>}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
placeholder="Phone Number"
|
||||
className={`w-full px-4 py-3 border ${formErrors.phone ? 'border-red-500' : '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`}
|
||||
/>
|
||||
{formErrors.phone && <small className="text-red-500 text-xs ml-1">{formErrors.phone}</small>}
|
||||
</div>
|
||||
<textarea
|
||||
rows={4}
|
||||
name="message"
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
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"
|
||||
disabled={isSubmitting}
|
||||
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 ${isSubmitting ? 'opacity-70 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
Request Callback
|
||||
{isSubmitting ? 'Sending...' : 'Request Callback'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useRef, MouseEvent } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import { FloatingHouse, RotatingKey, GrowingBuilding } from "./PropertyAnimations";
|
||||
|
||||
@ -72,12 +73,12 @@ 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>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 3D Cube Container */}
|
||||
@ -95,7 +96,7 @@ export default function About() {
|
||||
{/* 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"
|
||||
src="/assets/images/home/front.webp"
|
||||
alt="Front View"
|
||||
fill
|
||||
className="object-cover"
|
||||
@ -106,7 +107,7 @@ export default function About() {
|
||||
{/* 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"
|
||||
src="/assets/images/home/back.webp"
|
||||
alt="Back View"
|
||||
fill
|
||||
className="object-cover"
|
||||
@ -117,7 +118,7 @@ export default function About() {
|
||||
{/* 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"
|
||||
src="/assets/images/home/right.webp"
|
||||
alt="Right View"
|
||||
fill
|
||||
className="object-cover"
|
||||
@ -128,7 +129,7 @@ export default function About() {
|
||||
{/* 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"
|
||||
src="/assets/images/home/left.webp"
|
||||
alt="Left View"
|
||||
fill
|
||||
className="object-cover"
|
||||
@ -139,7 +140,7 @@ export default function About() {
|
||||
{/* 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"
|
||||
src="/assets/images/home/top.webp"
|
||||
alt="Top View"
|
||||
fill
|
||||
className="object-cover"
|
||||
@ -150,7 +151,7 @@ export default function About() {
|
||||
{/* 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"
|
||||
src="/assets/images/home/bottom.webp"
|
||||
alt="Bottom View"
|
||||
fill
|
||||
className="object-cover"
|
||||
|
||||
@ -59,177 +59,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>
|
||||
|
||||
@ -1,59 +1,247 @@
|
||||
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";
|
||||
|
||||
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 py-24 bg-[#f3f1e6] dark:bg-gray-900">
|
||||
|
||||
<div className="relative z-10 max-w-6xl mx-auto px-6">
|
||||
<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">
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -27,9 +27,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() {
|
||||
|
||||
@ -11,7 +11,7 @@ export default function Footer() {
|
||||
<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,10 +24,10 @@ 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>
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ 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. */}
|
||||
|
||||
@ -21,7 +21,7 @@ export default function InnerBanner({
|
||||
<div className="absolute inset-0">
|
||||
<Image
|
||||
src={backgroundImage}
|
||||
alt={title}
|
||||
alt={`${title} - Sky and Soil Real Estate`}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
|
||||
@ -44,8 +44,8 @@ export default function Lifestyle() {
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<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={{
|
||||
|
||||
@ -72,12 +72,15 @@ export default function PropertiesClient() {
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Properties" }
|
||||
]}
|
||||
backgroundImage="/assets/images/projects/projects-banner.webp"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<PropertyFilters onFilterChange={handleFilterChange} />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 py-12">
|
||||
<h2 className="text-3xl font-bold text-foreground mb-8">Browse Our Properties</h2>
|
||||
|
||||
{/* Property Grid */}
|
||||
{filteredProperties.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
@ -42,7 +42,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"
|
||||
/>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { FloatingHouse, RotatingKey, GrowingBuilding } from "./PropertyAnimations";
|
||||
|
||||
export default function WhyChooseUs() {
|
||||
@ -11,39 +12,22 @@ export default function WhyChooseUs() {
|
||||
{
|
||||
title: "Prime Locations",
|
||||
description: "Strategically located in the heart of North Bengaluru's growth corridor.",
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
|
||||
</svg>
|
||||
),
|
||||
image: "/assets/images/home/why/location.webp",
|
||||
},
|
||||
{
|
||||
title: "Smart Homes",
|
||||
description: "Future-ready infrastructure with integrated smart home automation.",
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 002.25-2.25V6.75a2.25 2.25 0 00-2.25-2.25H6.75A2.25 2.25 0 004.5 6.75v10.5a2.25 2.25 0 002.25 2.25z" />
|
||||
</svg>
|
||||
),
|
||||
image: "/assets/images/home/why/smart-home.webp",
|
||||
},
|
||||
{
|
||||
title: "Transparent Deals",
|
||||
description: "100% clear documentation and legal compliance for peace of mind.",
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
),
|
||||
image: "/assets/images/home/why/transparent-deals.webp",
|
||||
},
|
||||
{
|
||||
title: "Customer First",
|
||||
description: "Dedicated support team to guide you through every step of the journey.",
|
||||
icon: (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-8 h-8">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.182 15.182a4.5 4.5 0 01-6.364 0M21 12a9 9 0 11-18 0 9 9 0 0118 0zM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75zm-.375 0h.008v.015h-.008V9.75zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75zm-.375 0h.008v.015h-.008V9.75z" />
|
||||
</svg>
|
||||
),
|
||||
image: "/assets/images/home/why/customer-first.webp",
|
||||
},
|
||||
];
|
||||
|
||||
@ -115,8 +99,15 @@ export default function WhyChooseUs() {
|
||||
transitionDelay: `${index * 100}ms`
|
||||
}}
|
||||
>
|
||||
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900/30 text-primary rounded-full group-hover:scale-110 group-hover:rotate-6 transition-all duration-300">
|
||||
{feature.icon}
|
||||
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900/30 rounded-full group-hover:scale-110 group-hover:rotate-6 transition-all duration-300 relative w-20 h-20 flex items-center justify-center">
|
||||
<div className="relative w-[80%] h-[80%]">
|
||||
<Image
|
||||
src={feature.image}
|
||||
alt={feature.title}
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-foreground mb-3">
|
||||
{feature.title}
|
||||
|
||||
@ -26,8 +26,8 @@ export const properties: Property[] = [
|
||||
price: "₹ 1.9 CR*",
|
||||
description: "Experience the pinnacle of luxury living at BARCA, Godrej MSR City. These 3 BHK apartments are designed for those who appreciate the finer things in life, offering a perfect blend of modern architecture and natural serenity.",
|
||||
status: "New Launch",
|
||||
image: "/hero-image.jpg", // Using hero image as placeholder
|
||||
images: ["/hero-image.jpg", "/hero-image.jpg", "/hero-image.jpg"],
|
||||
image: "/assets/images/projects/barca.webp", // Using hero image as placeholder
|
||||
images: ["/assets/images/projects/details/barca-1.webp", "/assets/images/projects/details/barca-2.webp", "/assets/images/projects/details/barca-3.webp"],
|
||||
overview: {
|
||||
bhk: "3 BHK",
|
||||
size: "1500 - 2200 Sq.Ft",
|
||||
@ -44,8 +44,8 @@ export const properties: Property[] = [
|
||||
price: "₹ 1.62 CR*",
|
||||
description: "Godrej Woods offers premium 2 & 3 BHK apartments nestled in the lush greenery of Thanisandra. Enjoy a forest-themed lifestyle with world-class amenities and excellent connectivity to the airport and tech parks.",
|
||||
status: "Best Seller",
|
||||
image: "/hero-image.jpg",
|
||||
images: ["/hero-image.jpg", "/hero-image.jpg", "/hero-image.jpg"],
|
||||
image: "/assets/images/projects/godrej-woods.webp",
|
||||
images: ["/assets/images/projects/details/woods-1.webp", "/assets/images/projects/details/woods-2.webp", "/assets/images/projects/details/woods-3.webp"],
|
||||
overview: {
|
||||
bhk: "2 & 3 BHK",
|
||||
size: "1100 - 1600 Sq.Ft",
|
||||
@ -62,8 +62,8 @@ export const properties: Property[] = [
|
||||
price: "₹ 1.17 Cr*",
|
||||
description: "Discover the joy of community living at Godrej Hoskote. These premium 2 & 3 BHK homes are designed to provide a comfortable and convenient lifestyle, with easy access to industrial hubs and educational institutions.",
|
||||
status: "New Launch",
|
||||
image: "/hero-image.jpg",
|
||||
images: ["/hero-image.jpg", "/hero-image.jpg", "/hero-image.jpg"],
|
||||
image: "/assets/images/projects/godrej-hoskote.webp",
|
||||
images: ["/assets/images/projects/details/hoskote-1.webp", "/assets/images/projects/details/hoskote-2.webp", "/assets/images/projects/details/hoskote-3.webp"],
|
||||
overview: {
|
||||
bhk: "2 & 3 BHK",
|
||||
size: "1050 - 1750 Sq.Ft",
|
||||
@ -80,8 +80,8 @@ export const properties: Property[] = [
|
||||
price: "₹ 1.89 Cr*",
|
||||
description: "Wake up to the serene views of a lake and orchard at Godrej Lakeside Orchard. These luxury 2, 3, 3.5 & 4.5 BHK apartments offer a resort-like living experience on Sarjapur Road.",
|
||||
status: "Trending",
|
||||
image: "/hero-image.jpg",
|
||||
images: ["/hero-image.jpg", "/hero-image.jpg", "/hero-image.jpg"],
|
||||
image: "/assets/images/projects/godrej-lakeside.webp",
|
||||
images: ["/assets/images/projects/details/lakeside-1.webp", "/assets/images/projects/details/lakeside-2.webp", "/assets/images/projects/details/lakeside-3.webp"],
|
||||
overview: {
|
||||
bhk: "2, 3, 3.5 & 4.5 BHK",
|
||||
size: "1200 - 2800 Sq.Ft",
|
||||
@ -98,8 +98,8 @@ export const properties: Property[] = [
|
||||
price: "Price on Request",
|
||||
description: "Godrej Tiara presents exclusive 3 & 4 BHK apartments in the heart of Yeshwanthpur. Experience unmatched luxury, sophisticated design, and panoramic views of the city skyline.",
|
||||
status: "Premium",
|
||||
image: "/hero-image.jpg",
|
||||
images: ["/hero-image.jpg", "/hero-image.jpg", "/hero-image.jpg"],
|
||||
image: "/assets/images/projects/godrej-tiara.webp",
|
||||
images: ["/assets/images/projects/details/tiara-1.webp", "/assets/images/projects/details/tiara-2.webp", "/assets/images/projects/details/tiara-3.webp"],
|
||||
overview: {
|
||||
bhk: "3 & 4 BHK",
|
||||
size: "2000 - 3500 Sq.Ft",
|
||||
|
||||