From d88874b550c4d4d1243d91eebd0416387c5306f8 Mon Sep 17 00:00:00 2001 From: lokeshramchand-ctrl Date: Mon, 8 Dec 2025 15:16:11 +0530 Subject: [PATCH] feat: Add LandingPage and ProjectShowcase components with GSAP animations and responsive design --- package-lock.json | 184 ++++++----- package.json | 1 + src/components/LandingPage.tsx | 278 +++++++++++++++++ src/components/OpenLayerMap.tsx | 521 ++++++++++++++++++-------------- src/components/Projectpage.tsx | 478 +++++++++++++++++++++++++++++ 5 files changed, 1164 insertions(+), 298 deletions(-) create mode 100644 src/components/LandingPage.tsx create mode 100644 src/components/Projectpage.tsx diff --git a/package-lock.json b/package-lock.json index 3865fbf..f04dae3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "-": "^0.0.1", "@types/leaflet": "^1.9.18", "framer-motion": "^12.17.3", + "gsap": "^3.13.0", "leaflet": "^1.9.4", "ol": "^10.5.0", "ol-ext": "^4.0.31", @@ -84,6 +85,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -694,10 +696,11 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.0.tgz", - "integrity": "sha512-WhCn7Z7TauhBtmzhvKpoQs0Wwb/kBcy4CwpuI0/eEIr2Lx2auxmulAzLr91wVZJaz47iUZdkXOK7WlAfxGKCnA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "devOptional": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -733,12 +736,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "devOptional": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -747,19 +751,24 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "devOptional": true, + "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, @@ -803,48 +812,42 @@ } }, "node_modules/@eslint/js": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", - "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "devOptional": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "devOptional": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "devOptional": true, + "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "devOptional": true, - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1308,7 +1311,8 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "devOptional": true + "devOptional": true, + "license": "MIT" }, "node_modules/@types/leaflet": { "version": "1.9.18", @@ -1356,6 +1360,7 @@ "version": "19.1.1", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz", "integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1413,6 +1418,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz", "integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.29.1", "@typescript-eslint/types": "8.29.1", @@ -1512,10 +1518,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1607,10 +1614,12 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, + "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1688,10 +1697,11 @@ "devOptional": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "devOptional": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1728,6 +1738,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -1959,32 +1970,33 @@ } }, "node_modules/eslint": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", - "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "devOptional": true, + "license": "MIT", + "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.24.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2040,10 +2052,11 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "devOptional": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2056,10 +2069,11 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "devOptional": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2068,14 +2082,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "devOptional": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2101,6 +2116,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "devOptional": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -2358,6 +2374,12 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gsap": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.13.0.tgz", + "integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2443,10 +2465,11 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "devOptional": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2509,7 +2532,8 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/lerc": { "version": "3.0.0", @@ -2698,6 +2722,7 @@ "resolved": "https://registry.npmjs.org/ol/-/ol-10.5.0.tgz", "integrity": "sha512-nHFx8gkGmvYImsa7iKkwUnZidd5gn1XbMZd9GNOorvm9orjW9gQvT3Naw/MjIasVJ3cB9EJUdCGR2EFAulMHsQ==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@types/rbush": "4.0.0", "earcut": "^3.0.0", @@ -2724,6 +2749,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "devOptional": true, + "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -2945,6 +2971,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2967,6 +2994,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -3311,6 +3339,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3365,6 +3394,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "devOptional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3446,10 +3476,11 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -3609,6 +3640,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 6072c19..cd6156f 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "-": "^0.0.1", "@types/leaflet": "^1.9.18", "framer-motion": "^12.17.3", + "gsap": "^3.13.0", "leaflet": "^1.9.4", "ol": "^10.5.0", "ol-ext": "^4.0.31", diff --git a/src/components/LandingPage.tsx b/src/components/LandingPage.tsx new file mode 100644 index 0000000..ae26401 --- /dev/null +++ b/src/components/LandingPage.tsx @@ -0,0 +1,278 @@ +import React, { useState, useRef, useLayoutEffect } from 'react'; +import gsap from 'gsap'; +import { useNavigate } from 'react-router-dom'; + +// --- TYPES --- +interface LandingPageProps { + onSearchSuccess: (addressData: any, searchTerm: string) => void; +} + +const LandingPage: React.FC = ({ onSearchSuccess }) => { + const [term, setTerm] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isFocused, setIsFocused] = useState(false); + const navigate = useNavigate(); + + // --- REFS --- + const containerRef = useRef(null); + const glowRef = useRef(null); + const titleRef = useRef(null); + const formRef = useRef(null); + const inputRef = useRef(null); + const buttonRef = useRef(null); + + // --- GSAP INTRO ANIMATION --- + useLayoutEffect(() => { + const ctx = gsap.context(() => { + const tl = gsap.timeline({ defaults: { ease: 'expo.out' } }); + + // 1. Background Glow expands + tl.fromTo(glowRef.current, + { scale: 0.5, opacity: 0 }, + { scale: 1, opacity: 0.6, duration: 2, ease: 'power3.out' } + ) + // 2. Title floats up + .from(titleRef.current, { + y: 40, + opacity: 0, + duration: 1.5, + filter: 'blur(10px)', // Cinematic blur in + }, '-=1.5') + // 3. Search Bar expands width-wise + .from(formRef.current, { + scaleX: 0.8, + y: 20, + opacity: 0, + duration: 1.2, + }, '-=1.2'); + + }, containerRef); + + return () => ctx.revert(); + }, []); + + // --- INTERACTION ANIMATIONS --- + // Animate button in/out based on text content + useLayoutEffect(() => { + if (term.length > 0) { + gsap.to(buttonRef.current, { width: 'auto', opacity: 1, paddingLeft: 20, paddingRight: 20, duration: 0.4, ease: 'power2.out' }); + } else { + gsap.to(buttonRef.current, { width: 0, opacity: 0, paddingLeft: 0, paddingRight: 0, duration: 0.3, ease: 'power2.in' }); + } + }, [term]); + + // Animate Glow on Focus + useLayoutEffect(() => { + gsap.to(glowRef.current, { + opacity: isFocused ? 0.8 : 0.4, + scale: isFocused ? 1.1 : 1, + duration: 0.5 + }); + }, [isFocused]); + + + // --- LOGIC --- + const handleSearch = async (e: React.FormEvent) => { + e.preventDefault(); + if (!term) return; + + setIsLoading(true); + + // Cinematic Exit Animation + const tl = gsap.timeline(); + tl.to(titleRef.current, { opacity: 0, y: -20, duration: 0.5 }) + .to(formRef.current, { scale: 0.9, opacity: 0, blur: 10, duration: 0.4 }, '-=0.3') + .to(glowRef.current, { scale: 3, opacity: 0, duration: 0.8 }, '-=0.4'); + + try { + const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${term}&format=json&polygon=1&addressdetails=1`); + const json = await response.json(); + + // Artificial delay for smooth transition feel + setTimeout(() => { + onSearchSuccess(json, term); + navigate('/open'); + }, 800); + + } catch (error) { + console.error("Search failed", error); + setIsLoading(false); + // Reset Animation if failed + gsap.to([containerRef.current, titleRef.current, formRef.current], { opacity: 1, y: 0, scale: 1, blur: 0, duration: 0.5 }); + } + }; + + // --- STYLES --- + const styles = { + wrapper: { + height: '100vh', + width: '100vw', + backgroundColor: '#030303', // Deepest black + color: '#ffffff', + display: 'flex', + flexDirection: 'column' as const, + justifyContent: 'center', + alignItems: 'center', + fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + position: 'relative' as const, + overflow: 'hidden', + }, + // The Ambient Glow Behind + glow: { + position: 'absolute' as const, + width: '600px', + height: '600px', + background: 'radial-gradient(circle, rgba(255,255,255,0.08) 0%, rgba(0,0,0,0) 70%)', + borderRadius: '50%', + pointerEvents: 'none' as const, + zIndex: 0, + transform: 'translate(-50%, -50%)', + }, + contentZ: { + zIndex: 10, + display: 'flex', + flexDirection: 'column' as const, + alignItems: 'center', + width: '100%', + }, + title: { + fontSize: 'clamp(3rem, 6vw, 5rem)', + fontWeight: 800, + margin: '0 0 40px 0', + textAlign: 'center' as const, + letterSpacing: '-0.04em', + // Gradient Text Effect + background: 'linear-gradient(to bottom, #ffffff 0%, #888888 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + }, + form: { + position: 'relative' as const, + width: '100%', + maxWidth: '600px', + padding: '0 24px', + }, + glassContainer: { + display: 'flex', + alignItems: 'center', + background: 'rgba(255, 255, 255, 0.03)', // Extremely subtle fill + backdropFilter: 'blur(20px)', // Heavy blur + WebkitBackdropFilter: 'blur(20px)', + border: '1px solid rgba(255, 255, 255, 0.08)', + borderRadius: '24px', // Smooth pill shape + padding: '8px', + transition: 'border-color 0.3s ease, box-shadow 0.3s ease', + boxShadow: isFocused + ? '0 0 40px rgba(255, 255, 255, 0.05), inset 0 0 0 1px rgba(255,255,255,0.1)' + : '0 10px 40px rgba(0,0,0,0.5)', + }, + searchIcon: { + marginLeft: '16px', + color: '#555', + transition: 'color 0.3s', + }, + input: { + flex: 1, + background: 'transparent', + border: 'none', + outline: 'none', + color: '#fff', + fontSize: '1.1rem', + fontWeight: 400, + padding: '16px', + letterSpacing: '-0.02em', + caretColor: '#fff', // White cursor + }, + button: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + background: '#fff', + color: '#000', + border: 'none', + borderRadius: '16px', + height: '48px', + fontSize: '0.9rem', + fontWeight: 600, + cursor: 'pointer', + overflow: 'hidden', // Hide content when width is 0 + whiteSpace: 'nowrap' as const, + }, + loader: { + width: '20px', + height: '20px', + border: '2px solid rgba(0,0,0,0.1)', + borderTop: '2px solid #000', + borderRadius: '50%', + animation: 'spin 1s linear infinite', + } + }; + + return ( +
+ + {/* CSS Animation for loader */} + + + {/* Ambient Background Aura */} +
+ +
+ {/* Title */} +

+ Explore the San Diego. +

+ + {/* Search Component */} +
+
+ + {/* Search Icon */} +
+ + + + +
+ + {/* Input Field */} + setTerm(e.target.value)} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + placeholder="Search address, city, or place..." + style={styles.input} + disabled={isLoading} + autoFocus + /> + + {/* Animated Button */} + +
+
+
+
+ ); +}; + +export default LandingPage; \ No newline at end of file diff --git a/src/components/OpenLayerMap.tsx b/src/components/OpenLayerMap.tsx index e406fe9..34529a2 100644 --- a/src/components/OpenLayerMap.tsx +++ b/src/components/OpenLayerMap.tsx @@ -1,272 +1,349 @@ -import { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useLayoutEffect } from 'react'; import { Map, View } from 'ol'; import 'ol/ol.css'; import TileLayer from 'ol/layer/Tile'; -import { OSM } from 'ol/source'; +import XYZ from 'ol/source/XYZ'; import { fromLonLat } from 'ol/proj'; import { Coordinate } from 'ol/coordinate'; import VectorLayer from 'ol/layer/Vector'; import { layerConfigs } from './Methods/Layers'; -import { loadAndRenderGeoJsonLayer } from './Methods/GeoJson_Load' +import { loadAndRenderGeoJsonLayer } from './Methods/GeoJson_Load'; import { addMarker } from './Methods/Marker'; +import gsap from 'gsap'; +import { useNavigate } from 'react-router-dom'; -const OpenLayersMap = () => { - const [term, setTerm] = useState(''); - const [address, setAddress] = useState(); - const [layerVisibility, setLayerVisibility] = useState>({}); - const [searchCompleted, setSearchCompleted] = useState(false); - - // Submit Button Logic - const submitForm = async (event: React.FormEvent) => { - // Preventing the page from reloading - event.preventDefault(); - const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${term}&format=json&polygon=1&addressdetails=1`); - const json = await response.json(); - console.log(json); - setAddress(json); - console.log(address); - setSearchCompleted(true); - - // Initialize all layers as visible - const initialVisibility = Object.fromEntries( - layerConfigs.map(layer => [layer.topic, true]) - ); - setLayerVisibility(initialVisibility); - - // alert(term); +// --- THEME CONFIGURATION --- +const themes = { + dark: { + bg: '#050505', + glass: 'rgba(20, 20, 20, 0.75)', // Slightly darker for better contrast + border: 'rgba(255, 255, 255, 0.15)', // Stronger border + textMain: '#ffffff', + textMuted: '#AAAAAA', + iconColor: '#ffffff', + shadow: '0 4px 20px rgba(0,0,0,0.4)', // Strong shadow + switchTrackActive: 'rgba(255,255,255,0.2)', + switchTrackInactive: 'rgba(255,255,255,0.05)', + switchKnobActive: '#fff', + switchKnobInactive: '#666', + mapUrl: 'https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' + }, + light: { + bg: '#F4F4F5', + glass: 'rgba(255, 255, 255, 0.90)', // More opaque for visibility + border: 'rgba(0, 0, 0, 0.1)', + textMain: '#18181B', + textMuted: '#52525B', + iconColor: '#18181B', + shadow: '0 4px 20px rgba(0,0,0,0.1)', + switchTrackActive: 'rgba(0,0,0,0.2)', + switchTrackInactive: 'rgba(0,0,0,0.05)', + switchKnobActive: '#000', + switchKnobInactive: '#999', + mapUrl: 'https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png' } +}; - const mapDivRef = useRef(null); +// --- SCROLLBAR CSS --- +const scrollbarStyles = ` + .custom-scrollbar::-webkit-scrollbar { width: 4px; } + .custom-scrollbar::-webkit-scrollbar-track { background: rgba(128, 128, 128, 0.1); border-radius: 4px; } + .custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(128, 128, 128, 0.3); border-radius: 4px; } + .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(128, 128, 128, 0.5); } +`; + +// --- HELPER: Extract Color --- +const extractColorFromStyle = (style: any): string => { + let color: any = '#888888'; + if (style.getStroke() && style.getStroke().getColor()) color = style.getStroke().getColor(); + else if (style.getFill() && style.getFill().getColor()) color = style.getFill().getColor(); + + const colorMap: Record = { + 'skin': '#FFCBA4', 'darkyellow': '#FCC200', 'darkblue': '#00008B', + 'darkgreen': '#006400', 'purple': '#800080', 'orange': '#FFA500', + 'green': '#00FF00', 'blue': '#0000FF', 'red': '#FF0000', 'black': '#888888', + }; + return colorMap[color] || color; +}; + +interface MapViewProps { + initialAddressData: any; + initialTerm: string; +} + +const MapView: React.FC = ({ initialAddressData, initialTerm }) => { + const navigate = useNavigate(); + + // -- STATE -- + const [isDarkMode, setIsDarkMode] = useState(true); + const currentTheme = isDarkMode ? themes.dark : themes.light; + + const [layerVisibility, setLayerVisibility] = useState>(() => + Object.fromEntries(layerConfigs.map(layer => [layer.topic, true])) + ); const [clickedCoordinate, setClickedCoordinate] = useState(); + + // -- REFS -- + const mapDivRef = useRef(null); const mapRef = useRef(null); + const tileLayerRef = useRef | null>(null); const markerLayerRef = useRef(null); const layerRefs = useRef>>( - Object.fromEntries( - layerConfigs.map((layer) => [layer.topic, { current: null }]) - ) + Object.fromEntries(layerConfigs.map((layer) => [layer.topic, { current: null }])) ); + const uiContainerRef = useRef(null); - // Toggle layer visibility - const toggleLayer = (layerTopic: string) => { - const layerRef = layerRefs.current[layerTopic]; - const isCurrentlyVisible = layerVisibility[layerTopic]; - - if (layerRef.current) { - layerRef.current.setVisible(!isCurrentlyVisible); - } - - setLayerVisibility(prev => ({ - ...prev, - [layerTopic]: !isCurrentlyVisible - })); - }; - - // Format layer name for display - const formatLayerName = (topic: string) => { - return topic.replace(/_/g, ' ').replace(/SD$/, '').trim(); - }; + // -- GSAP ENTRANCE -- + useLayoutEffect(() => { + const ctx = gsap.context(() => { + // We use clearProps: 'all' to ensure GSAP doesn't leave the elements with weird opacity states + gsap.from('.floating-ui', { + y: 20, + opacity: 0, + duration: 0.8, + stagger: 0.1, + ease: 'power3.out', + delay: 0.5, + clearProps: 'opacity,transform' // Crucial: Ensures visibility after animation ends + }); + }, uiContainerRef); + return () => ctx.revert(); + }, []); + // -- MAP INIT -- useEffect(() => { if (!mapRef.current) { + const baseLayer = new TileLayer({ + source: new XYZ({ + url: currentTheme.mapUrl, + attributions: '© OpenStreetMap © CARTO' + }) + }); + tileLayerRef.current = baseLayer; + const map = new Map({ target: mapDivRef.current as HTMLDivElement, - layers: [new TileLayer({ source: new OSM() })], + layers: [ baseLayer ], + view: new View({ center: fromLonLat([0, 0]), zoom: 2 }), + controls: [] }); + mapRef.current = map; - map.on('click', (e) => { - setClickedCoordinate(e.coordinate); - }); + map.on('click', (e) => setClickedCoordinate(e.coordinate)); } + }, []); - if (address && address.length > 0) { - const lat = parseFloat(address[0].lat); - const lon = parseFloat(address[0].lon); + // -- THEME SWITCHING -- + useEffect(() => { + if (mapRef.current && tileLayerRef.current) { + tileLayerRef.current.setSource(new XYZ({ + url: currentTheme.mapUrl, + attributions: '© OpenStreetMap © CARTO' + })); + } + }, [isDarkMode]); + + // -- SEARCH DATA -- + useEffect(() => { + if (initialAddressData && initialAddressData.length > 0) { + const lat = parseFloat(initialAddressData[0].lat); + const lon = parseFloat(initialAddressData[0].lon); const coords = fromLonLat([lon, lat]); - mapRef.current!.setView(new View({ - center: coords, - zoom: 15, - })); + mapRef.current!.getView().animate({ center: coords, zoom: 14, duration: 2500, ease: (t) => Math.pow(t, 2) }); markerLayerRef.current = addMarker(coords, mapRef.current!, markerLayerRef.current); layerConfigs.forEach((layerConfig) => { + if(layerRefs.current[layerConfig.topic].current) mapRef.current!.removeLayer(layerRefs.current[layerConfig.topic].current!); const url = layerConfig.getUrl(lon, lat, layerConfig.radius); const ref = layerRefs.current[layerConfig.topic]; - loadAndRenderGeoJsonLayer( - url, - mapRef.current!, - ref, - layerConfig.style - ); + loadAndRenderGeoJsonLayer(url, mapRef.current!, ref, layerConfig.style); }); } - }, [address]); + }, [initialAddressData]); + + const toggleLayer = (layerTopic: string) => { + const layerRef = layerRefs.current[layerTopic]; + const isVisible = !layerVisibility[layerTopic]; + if (layerRef.current) layerRef.current.setVisible(isVisible); + setLayerVisibility(prev => ({ ...prev, [layerTopic]: isVisible })); + }; + + const formatLayerName = (topic: string) => topic.replace(/_/g, ' ').replace(/SD$/, '').trim(); + + // -- STYLES -- + const styles = { + wrapper: { + position: 'relative' as const, width: '100vw', height: '100vh', + background: currentTheme.bg, overflow: 'hidden', + fontFamily: '"Inter", -apple-system, sans-serif', + transition: 'background 0.5s ease', + }, + uiLayer: { + position: 'absolute' as const, top: 0, left: 0, width: '100%', height: '100%', + pointerEvents: 'none' as const, + zIndex: 1000, // FORCE TOP LAYER + padding: '24px', + display: 'flex', flexDirection: 'column' as const, justifyContent: 'space-between', + }, + topBar: { + display: 'flex', + alignItems: 'center', + pointerEvents: 'auto' as const, + gap: '12px', + width: '100%' // Ensure it spans width + }, + glassPanel: { + background: currentTheme.glass, + backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)', + border: `1px solid ${currentTheme.border}`, + borderRadius: '20px', padding: '40px', + boxShadow: currentTheme.shadow, + transition: 'all 0.3s ease' + }, + locationBadge: { + display: 'flex', alignItems: 'center', padding: '12px 24px', + borderRadius: '30px', background: currentTheme.glass, + backdropFilter: 'blur(20px)', border: `1px solid ${currentTheme.border}`, + boxShadow: currentTheme.shadow, + transition: 'all 0.3s ease', + flexShrink: 0 // Prevent collapsing + }, + actionButton: { + width: '48px', height: '48px', borderRadius: '50%', + display: 'flex', alignItems: 'center', justifyContent: 'center', + background: currentTheme.glass, + backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)', + border: `1px solid ${currentTheme.border}`, + color: currentTheme.iconColor, // Explicit high contrast color + boxShadow: currentTheme.shadow, // Force visibility + cursor: 'pointer', transition: 'all 0.2s', + flexShrink: 0 // Prevent collapsing + }, + controlsContainer: { + marginTop: 'auto', maxWidth: '340px', maxHeight: '60vh', + display: 'flex', flexDirection: 'column' as const, pointerEvents: 'auto' as const, + background: currentTheme.glass, backdropFilter: 'blur(20px)', + border: `1px solid ${currentTheme.border}`, borderRadius: '10px', padding: '20px', + boxShadow: currentTheme.shadow, transition: 'all 0.3s ease' + }, + controlsHeader: { + flexShrink: 0, fontSize: '10px', fontWeight: 700, letterSpacing: '1px', + color: currentTheme.textMuted, marginBottom: '16px', textTransform: 'uppercase' as const, + borderBottom: `1px solid ${currentTheme.border}`, paddingBottom: '12px' + }, + scrollableList: { overflowY: 'auto' as const, paddingRight: '8px' }, + layerRow: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px', cursor: 'pointer', padding: '4px 8px' }, + layerLabel: { display: 'flex', alignItems: 'center', gap: '12px' }, + switchTrack: { width: '32px', height: '18px', borderRadius: '10px', position: 'relative' as const, transition: 'background 0.3s ease' }, + switchKnob: { width: '14px', height: '14px', borderRadius: '50%', position: 'absolute' as const, top: '2px', transition: 'left 0.3s cubic-bezier(0.4, 0.0, 0.2, 1)', boxShadow: '0 2px 4px rgba(0,0,0,0.2)' }, + hud: { + position: 'absolute' as const, bottom: '24px', right: '24px', + fontFamily: '"JetBrains Mono", monospace', fontSize: '10px', + color: currentTheme.textMuted, pointerEvents: 'auto' as const, + padding: '8px 12px', background: currentTheme.glass, + borderRadius: '8px', border: `1px solid ${currentTheme.border}`, + boxShadow: currentTheme.shadow, + transition: 'background 0.3s, color 0.3s' + } + }; - // UI return ( -
- {/* Sidebar */} -
-

- Map Layers -

- - {/* Search Form */} -
- setTerm(e.target.value)} - type="text" - placeholder="Enter address to search" - style={{ - width: '100%', - padding: '10px', - border: '1px solid #ddd', - borderRadius: '4px', - marginBottom: '10px', - fontSize: '14px' - }} - /> - -
+
+ - {/* Layer Toggle List */} -
-

- Available Layers ({layerConfigs.length}) -

- - {layerConfigs.map((layer) => ( -
- - {formatLayerName(layer.topic)} - - - {searchCompleted && ( - - )} - - {!searchCompleted && ( - - Search first + {/* 1. MAP LAYER */} +
+ + {/* 2. UI LAYER */} +
+ + {/* TOP BAR */} +
+ + {/* Back Button */} + + + {/* Location Badge */} +
+
+ + {initialTerm || "Exploring View"} - )}
- ))} + + {/* Theme Toggle Button */} + +
- -
+ {/* CONTROLS */} +
+
Overlay Controls
+ +
+ {layerConfigs.map((layer) => { + const isActive = layerVisibility[layer.topic]; + const layerColor = extractColorFromStyle(layer.style); - {/* Map Container */} -
-
- + return ( +
toggleLayer(layer.topic)}> +
+
+ + {formatLayerName(layer.topic)} + +
+ +
+
+
+
+ ); + })} +
+
+ + {/* HUD */} {clickedCoordinate && ( -
- Coordinates: {clickedCoordinate[0].toFixed(2)} / {clickedCoordinate[1].toFixed(2)} -
+
+ LOCATION_LOCK: {clickedCoordinate[1].toFixed(4)} N, {clickedCoordinate[0].toFixed(4)} E +
)} +
); }; -export default OpenLayersMap; +export default MapView; \ No newline at end of file diff --git a/src/components/Projectpage.tsx b/src/components/Projectpage.tsx new file mode 100644 index 0000000..9a30d91 --- /dev/null +++ b/src/components/Projectpage.tsx @@ -0,0 +1,478 @@ +import React, { useLayoutEffect, useRef, useState, useEffect } from 'react'; +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import { useNavigate } from 'react-router-dom'; + +// Register GSAP plugins +gsap.registerPlugin(ScrollTrigger); + +const ProjectShowcase: React.FC = () => { + const navigate = useNavigate(); + const [themeMode, setThemeMode] = useState<'dark' | 'light'>('dark'); + const [isScrolled, setIsScrolled] = useState(false); + + // Refs + const containerRef = useRef(null); +// const cursorRef = useRef(null); + + // Toggle Theme + const toggleTheme = () => { + const newTheme = themeMode === 'dark' ? 'light' : 'dark'; + setThemeMode(newTheme); + document.documentElement.setAttribute('data-theme', newTheme); + }; + + // Scroll Listener for Navbar + useEffect(() => { + const handleScroll = () => setIsScrolled(window.scrollY > 50); + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + // --- GSAP ANIMATIONS --- + useLayoutEffect(() => { + const ctx = gsap.context(() => { + const tl = gsap.timeline(); + + // 1. Initial Load Sequence + tl.from('.hero-word', { + y: 100, + opacity: 0, + duration: 1.2, + stagger: 0.05, + ease: 'power4.out' + }) + .from('.hero-desc', { + y: 20, + opacity: 0, + duration: 1, + ease: 'power2.out' + }, '-=0.8') + .from('.nav-item', { + y: -20, + opacity: 0, + stagger: 0.1, + duration: 0.8, + ease: 'power2.out' + }, '-=1'); + + // 2. Floating 3D Elements + gsap.to('.visual-card', { + y: '15px', + rotateX: 4, + rotateY: 4, + duration: 4, + yoyo: true, + repeat: -1, + ease: 'sine.inOut', + stagger: { each: 0.5, from: "end" } + }); + + // 3. Scroll Triggered Reveals + const sections = gsap.utils.toArray('.reveal-section'); + sections.forEach((section: any) => { + gsap.from(section.children, { + scrollTrigger: { + trigger: section, + start: 'top 80%', + toggleActions: 'play none none reverse' + }, + y: 60, + opacity: 0, + duration: 1, + stagger: 0.15, + ease: 'expo.out' + }); + }); + + }, containerRef); + return () => ctx.revert(); + }, []); + + const handleLaunch = () => { + const tl = gsap.timeline(); + tl.to(containerRef.current, { scale: 0.98, opacity: 0, filter: 'blur(20px)', duration: 0.8, ease: 'power2.in' }); + setTimeout(() => navigate('/site'), 800); + }; + + // --- STYLES (Using CSS Variables for Theme Support) --- + const styles = { + wrapper: { + fontFamily: '"Space Grotesk", sans-serif', // Modern Tech Font + minHeight: '100vh', + overflowX: 'hidden' as const, + position: 'relative' as const, + }, + // NAVBAR + nav: { + position: 'fixed' as const, + top: 0, left: 0, right: 0, + height: '90px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '0 5%', + zIndex: 100, + transition: 'all 0.4s ease', + backgroundColor: isScrolled ? 'var(--bg-color)' : 'transparent', + backdropFilter: isScrolled ? 'blur(20px)' : 'none', + borderBottom: isScrolled ? '1px solid var(--border-color)' : '1px solid transparent', + }, + logo: { + fontSize: '1.25rem', + fontWeight: 700, + letterSpacing: '-0.02em', + color: 'var(--text-main)', + display: 'flex', + alignItems: 'center', + gap: '8px', + cursor: 'pointer', + }, + logoDot: { + width: '8px', height: '8px', background: 'var(--accent)', borderRadius: '50%', + boxShadow: '0 0 10px var(--accent)' + }, + navLinks: { + display: 'flex', gap: '40px', alignItems: 'center', + '@media (max-width: 768px)': { display: 'none' } + }, + link: { + fontSize: '0.9rem', + fontWeight: 500, + color: 'var(--text-muted)', + cursor: 'pointer', + transition: 'color 0.3s', + letterSpacing: '0.02em', + }, + // HERO + hero: { + minHeight: '100vh', + padding: '140px 5% 80px', + display: 'flex', + flexWrap: 'wrap' as const, + alignItems: 'center', + justifyContent: 'center', + gap: '60px', + }, + heroText: { flex: '1 1 500px' }, + pill: { + display: 'inline-flex', alignItems: 'center', gap: '8px', + padding: '8px 16px', borderRadius: '100px', + background: 'rgba(0, 240, 255, 0.05)', + border: '1px solid rgba(0, 240, 255, 0.2)', + color: 'var(--accent)', fontSize: '0.75rem', fontWeight: 600, + marginBottom: '32px', letterSpacing: '0.05em' + }, + h1: { + fontSize: 'clamp(3.5rem, 6vw, 6rem)', + fontWeight: 700, + lineHeight: 1.05, + marginBottom: '32px', + color: 'var(--text-main)', + letterSpacing: '-0.03em', + }, + desc: { + fontSize: 'clamp(1.1rem, 1.5vw, 1.25rem)', + color: 'var(--text-muted)', + lineHeight: 1.6, + maxWidth: '580px', + marginBottom: '48px', + fontFamily: '"Inter", sans-serif', + fontWeight: 300, + }, + btnPrimary: { + padding: '18px 48px', + background: 'var(--text-main)', + color: 'var(--bg-color)', + border: 'none', + borderRadius: '2px', // Sharper corners for modern look + fontWeight: 600, + fontSize: '0.95rem', + cursor: 'pointer', + transition: 'transform 0.2s, box-shadow 0.2s', + letterSpacing: '0.02em', + }, + btnSecondary: { + padding: '18px 48px', + background: 'transparent', + color: 'var(--text-main)', + border: '1px solid var(--border-color)', + borderRadius: '2px', + fontWeight: 600, + fontSize: '0.95rem', + cursor: 'pointer', + marginLeft: '20px', + transition: 'background 0.2s', + }, + // VISUAL + visualContainer: { + flex: '1 1 400px', + height: '600px', + position: 'relative' as const, + perspective: '2000px', + display: 'flex', alignItems: 'center', justifyContent: 'center' + }, + layerStack: { + position: 'relative' as const, + width: '340px', height: '440px', + transformStyle: 'preserve-3d' as const, + transform: 'rotateX(55deg) rotateZ(-35deg) rotateY(5deg)', + }, + layerCard: (z: number, color: string) => ({ + position: 'absolute' as const, + inset: 0, + background: 'var(--card-bg)', // Glass var + backdropFilter: 'blur(12px)', + border: `1px solid ${color}`, + transform: `translateZ(${z}px)`, + display: 'flex', alignItems: 'center', justifyContent: 'center', + boxShadow: `0 0 40px -10px ${color}40`, + borderRadius: '16px', + color: 'var(--text-main)', + fontSize: '0.9rem', fontWeight: 600, letterSpacing: '0.1em' + }), + + // SECTION GENERAL + section: { padding: '160px 5%', borderTop: '1px solid var(--border-color)' }, + sectionLabel: { + color: 'var(--accent)', + fontSize: '0.8rem', fontWeight: 600, + textTransform: 'uppercase' as const, letterSpacing: '0.2em', + marginBottom: '24px', display: 'block' + }, + h2: { + fontSize: 'clamp(2.5rem, 5vw, 4rem)', + fontWeight: 700, color: 'var(--text-main)', + marginBottom: '24px', lineHeight: 1.1, letterSpacing: '-0.02em' + }, + + // GRID + grid: { + display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', + gap: '40px', marginTop: '80px' + }, + card: { + padding: '48px', + background: 'var(--card-bg)', + border: '1px solid var(--border-color)', + borderRadius: '4px', + transition: 'border-color 0.3s', + cursor: 'default' + }, + icon: { + width: '48px', height: '48px', marginBottom: '32px', + color: 'var(--text-main)', display: 'flex', alignItems: 'center', justifyContent: 'center', + background: 'rgba(255,255,255,0.05)', borderRadius: '8px' + }, + cardTitle: { + fontSize: '1.5rem', fontWeight: 600, marginBottom: '16px', + color: 'var(--text-main)', letterSpacing: '-0.02em' + }, + cardText: { + color: 'var(--text-muted)', lineHeight: 1.6, fontSize: '1rem', + fontFamily: '"Inter", sans-serif' + }, + + // FOOTER + footer: { + padding: '120px 5%', + background: 'var(--bg-color)', + borderTop: '1px solid var(--border-color)', + textAlign: 'center' as const + }, + themeBtn: { + background: 'transparent', border: '1px solid var(--border-color)', + color: 'var(--text-main)', width: '40px', height: '40px', + borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', + cursor: 'pointer', transition: 'all 0.3s' + } + }; + + return ( +
+ + {/* NAVBAR */} + + + {/* HERO SECTION */} +
+
+ + +

+
The Operating System
+
for Modern Cities.
+

+ +

+ MapLayer unifies fragmented urban data into a single, high-performance geospatial decision engine. Visualize zoning, transit, and environmental risks in real-time. +

+ +
+ + {/* */} +
+
+ + {/* 3D VISUALIZATION */} +
+
+
+ BASEMAP +
+
+ INFRASTRUCTURE +
+
+ ZONING DATA +
+
+ ANALYTICS +
+
+
+
+ + {/* CHALLENGE SECTION */} +
+
+ 01 — The Challenge +

Data fragmentation
is paralyzing progress.

+

+ Cities generate terabytes of spatial data daily. Yet, Planners and Emergency Responders operate in silos, using outdated PDF maps and disjointed portals. +

+
+ +
+ {/* Card 1 */} +
e.currentTarget.style.borderColor = 'var(--accent)'} + onMouseLeave={(e) => e.currentTarget.style.borderColor = 'var(--border-color)'}> +
+ +
+

Emergency Latency

+

First responders struggle to access integrated utility and hazard data during critical incidents, costing seconds that matter.

+
+ + {/* Card 2 */} +
e.currentTarget.style.borderColor = 'var(--accent)'} + onMouseLeave={(e) => e.currentTarget.style.borderColor = 'var(--border-color)'}> +
+ +
+

Planning Blindspots

+

Zoning, environmental, and transit layers are rarely visualized together, making holistic impact analysis impossible.

+
+ + {/* Card 3 */} +
e.currentTarget.style.borderColor = 'var(--accent)'} + onMouseLeave={(e) => e.currentTarget.style.borderColor = 'var(--border-color)'}> +
+ +
+

Transparency Gaps

+

Citizens lack a clear, real-time window into construction projects, road closures, and city development plans.

+
+
+
+ + {/* SOLUTION SECTION */} +
+
+ 02 — The Solution +

A unified geospatial
intelligence core.

+
+ +
+ {/* Feature 1 */} +
+

Modular Layering

+

+ MapLayer's engine allows users to stack infinite datasets—parcels, sewer lines, fire zones—and toggle them instantly with zero latency. +

+
+ {/* Feature 2 */} +
+

Real-Time Sync

+

+ Direct integration with San Diego's Open Data portal ensures that the map you see today reflects the reality of the city right now. +

+
+ {/* Feature 3 */} +
+

Interactive HUD

+

+ A designer-grade UI that floats above the map, providing coordinate locking, layer analysis, and distinct color coding without clutter. +

+
+
+
+ + {/* FOOTER */} + +
+ ); +}; + +export default ProjectShowcase; \ No newline at end of file