Compare commits
21 commits
2e0f57a3c0
...
f051581171
| Author | SHA1 | Date | |
|---|---|---|---|
| f051581171 | |||
| 8927422283 | |||
| bf82eeded0 | |||
| 3dceaa35e8 | |||
| f5f41934a1 | |||
| 82dcdf1097 | |||
| 4dfdb2da99 | |||
| c312d4b270 | |||
| d88874b550 | |||
| 5de8228938 | |||
| 9532bb5470 | |||
| a800d51902 | |||
| a89df0aaa3 | |||
| 97a9f163c3 | |||
| 5f35f52f36 | |||
| 2e89c5794b | |||
| ca22a9df76 | |||
| 274134b90e | |||
| 6a24ff4127 | |||
| 26b5ac1d4d | |||
| 10c3e9fac9 |
12 changed files with 1678 additions and 466 deletions
22
Jenkinsfile
vendored
Executable file
22
Jenkinsfile
vendored
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
pipeline
|
||||||
|
{
|
||||||
|
agent {dockerfile true}
|
||||||
|
stages
|
||||||
|
{
|
||||||
|
stage ('Build')
|
||||||
|
{
|
||||||
|
steps
|
||||||
|
{
|
||||||
|
echo 'running build'
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stage ('Test')
|
||||||
|
{
|
||||||
|
steps
|
||||||
|
{
|
||||||
|
echo 'Running Test...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
291
README.md
291
README.md
|
|
@ -1,54 +1,259 @@
|
||||||
# React + TypeScript + Vite
|
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
A lightweight React + GeoJSON based mapping interface designed to visualize, explore, and interact with geospatial datasets using modern web technologies.
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
## Problem
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
Geospatial datasets are often difficult to visualize without specialized GIS software. Analysts, developers, and researchers need a simpler, browser-based tool that can:
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
* Load and display GeoJSON layers dynamically
|
||||||
|
* Visualize markers, regions, and polygons interactively
|
||||||
|
* Switch between different map layers and base tiles
|
||||||
|
* Integrate custom APIs for external data (e.g., legislation, statistics)
|
||||||
|
* Provide an intuitive UI that works seamlessly without GIS expertise
|
||||||
|
|
||||||
```js
|
Traditional tools are either too heavy, too costly, or lack customization options for developers looking to embed maps in their applications.
|
||||||
export default tseslint.config({
|
|
||||||
extends: [
|
---
|
||||||
// Remove ...tseslint.configs.recommended and replace with this
|
|
||||||
...tseslint.configs.recommendedTypeChecked,
|
## Solution
|
||||||
// Alternatively, use this for stricter rules
|
|
||||||
...tseslint.configs.strictTypeChecked,
|
MapLayer provides an extensible React-based mapping application built using OpenLayers, capable of:
|
||||||
// Optionally, add this for stylistic rules
|
|
||||||
...tseslint.configs.stylisticTypeChecked,
|
* Rendering GeoJSON datasets directly inside the browser
|
||||||
],
|
* Displaying multiple layers with controls for toggling visibility
|
||||||
languageOptions: {
|
* Adding marker overlays with custom interactivity
|
||||||
// other options...
|
* Integrating external APIs (e.g., LegiScan API)
|
||||||
parserOptions: {
|
* Offering a clean UI for switching maps, searching, and navigating
|
||||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
* Deploying at scale using Docker, Jenkins, and containerized workflows
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
It enables quick visualization and experimentation with spatial datasets while staying lightweight and developer-friendly.
|
||||||
},
|
|
||||||
})
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Core Features
|
||||||
|
|
||||||
|
* Load and visualize GeoJSON datasets with OpenLayers
|
||||||
|
* Multiple layer rendering (base maps, vector layers, custom overlays)
|
||||||
|
* Marker placement with custom interaction logic
|
||||||
|
* Search bar for querying map elements
|
||||||
|
* Modular component structure for easy extension
|
||||||
|
* Integration-ready API layer
|
||||||
|
|
||||||
|
### UI/UX Features
|
||||||
|
|
||||||
|
* Landing page and project-specific information pages
|
||||||
|
* Clean interactive mapping interface
|
||||||
|
* Dock-style UI elements for layer controls
|
||||||
|
* Responsive layouts using modern React patterns
|
||||||
|
|
||||||
|
### DevOps Features
|
||||||
|
|
||||||
|
* Dockerized build for consistent deployments
|
||||||
|
* Docker Compose support
|
||||||
|
* CI/CD automation using Jenkins pipelines
|
||||||
|
* Production-ready Vite bundling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
maplayer/
|
||||||
|
└── frontend
|
||||||
|
└── public
|
||||||
|
├── vite.svg
|
||||||
|
└── src
|
||||||
|
└── assets
|
||||||
|
├── Address_Points.geojson
|
||||||
|
├── react.svg
|
||||||
|
└── components
|
||||||
|
└── Methods
|
||||||
|
├── GeoJson_Load.tsx
|
||||||
|
├── Layers.tsx
|
||||||
|
├── Marker.tsx
|
||||||
|
└── UI
|
||||||
|
├── another.tsx
|
||||||
|
├── Dock.css
|
||||||
|
├── Dock.tsx
|
||||||
|
├── Map_page.css
|
||||||
|
├── Map_page.tsx
|
||||||
|
├── LandingPage.tsx
|
||||||
|
├── LegiScanAPI.tsx
|
||||||
|
├── OpenLayerEsri.tsx
|
||||||
|
├── OpenLayerMap.tsx
|
||||||
|
├── Projectpage.tsx
|
||||||
|
├── SearchBar.tsx
|
||||||
|
├── App.css
|
||||||
|
├── App.tsx
|
||||||
|
├── index.css
|
||||||
|
├── main.tsx
|
||||||
|
├── vite-env.d.ts
|
||||||
|
├── .gitignore
|
||||||
|
├── Dockerfile
|
||||||
|
├── eslint.config.js
|
||||||
|
├── index.html
|
||||||
|
├── Jenkinsfile
|
||||||
|
├── package-lock.json
|
||||||
|
├── package.json
|
||||||
|
├── README.md
|
||||||
|
├── tsconfig.app.json
|
||||||
|
├── tsconfig.json
|
||||||
|
├── tsconfig.node.json
|
||||||
|
├── vite.config.ts
|
||||||
|
├── .gitattributes
|
||||||
|
├── docker-compose.yaml
|
||||||
|
├── package-lock.json
|
||||||
|
└── package.json
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
---
|
||||||
|
|
||||||
```js
|
## Tech Stack
|
||||||
// eslint.config.js
|
|
||||||
import reactX from 'eslint-plugin-react-x'
|
|
||||||
import reactDom from 'eslint-plugin-react-dom'
|
|
||||||
|
|
||||||
export default tseslint.config({
|
### Frontend
|
||||||
plugins: {
|
|
||||||
// Add the react-x and react-dom plugins
|
* React (TypeScript)
|
||||||
'react-x': reactX,
|
* Vite bundler
|
||||||
'react-dom': reactDom,
|
* OpenLayers for map rendering
|
||||||
},
|
* GeoJSON for dataset representation
|
||||||
rules: {
|
* Custom CSS for UI components
|
||||||
// other rules...
|
|
||||||
// Enable its recommended typescript rules
|
### DevOps & Deployment
|
||||||
...reactX.configs['recommended-typescript'].rules,
|
|
||||||
...reactDom.configs.recommended.rules,
|
* Docker for containerization
|
||||||
},
|
* Docker Compose for local orchestration
|
||||||
})
|
* Jenkins CI/CD pipeline
|
||||||
|
* Nginx (optional, for serving static builds)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontend (React + GeoJSON)
|
||||||
|
|
||||||
|
The frontend is built with Vite + React and organized into functional modules:
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
* **GeoJson_Load.tsx**
|
||||||
|
Handles loading and parsing of GeoJSON datasets, converting them into OpenLayers vector sources.
|
||||||
|
|
||||||
|
* **Layers.tsx**
|
||||||
|
Manages creation, toggling, and rendering of OpenLayers map layers.
|
||||||
|
|
||||||
|
* **Marker.tsx**
|
||||||
|
Adds interactive marker overlays with custom styles.
|
||||||
|
|
||||||
|
### Mapping Components
|
||||||
|
|
||||||
|
* **OpenLayerMap.tsx**
|
||||||
|
Main OpenLayers implementation with map initialization, view configuration, and layer attachment.
|
||||||
|
|
||||||
|
* **OpenLayerEsri.tsx**
|
||||||
|
Optional integration for ESRI tiles/basemaps.
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
|
||||||
|
* **Dock.tsx**
|
||||||
|
UI menu for layer switching and actions.
|
||||||
|
|
||||||
|
* **Map_page.tsx**
|
||||||
|
Primary mapping interface.
|
||||||
|
|
||||||
|
* **SearchBar.tsx**
|
||||||
|
Search functionality (e.g., filter markers, search dataset, API queries).
|
||||||
|
|
||||||
|
### Supporting Pages
|
||||||
|
|
||||||
|
* Landing page
|
||||||
|
* Project information page
|
||||||
|
* LegiScan API integration page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DevOps & Deployment
|
||||||
|
|
||||||
|
MapLayer is designed to be production-ready with minimal configuration.
|
||||||
|
|
||||||
|
### Dockerfile
|
||||||
|
|
||||||
|
Defines a lightweight Node + Nginx pipeline for building and serving the application.
|
||||||
|
|
||||||
|
### docker-compose.yaml
|
||||||
|
|
||||||
|
Orchestrates services required to run the project locally, including:
|
||||||
|
|
||||||
|
* Frontend container
|
||||||
|
* Optional reverse proxy container
|
||||||
|
|
||||||
|
|
||||||
|
## Frontend Architecture
|
||||||
|
|
||||||
|
MapLayer follows a modular, scalable architecture:
|
||||||
|
|
||||||
|
1. **UI Layer**
|
||||||
|
Presentation components (Dock, Map Page, Search Bar).
|
||||||
|
|
||||||
|
2. **Map Logic Layer**
|
||||||
|
OpenLayers configuration (map object, layers, markers).
|
||||||
|
|
||||||
|
3. **Data Access Layer**
|
||||||
|
GeoJSON loader, external API services (LegiScan API).
|
||||||
|
|
||||||
|
4. **Asset Layer**
|
||||||
|
GeoJSON files, icons, static resources.
|
||||||
|
|
||||||
|
5. **Configuration Layer**
|
||||||
|
Vite and TypeScript configs.
|
||||||
|
|
||||||
|
This separation makes the project easy to scale and adapt to new features.
|
||||||
|
|
||||||
|
|
||||||
|
## High-Level Architecture Diagram
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
+-------------------------+
|
||||||
|
| Client (UI) |
|
||||||
|
| React + OpenLayers |
|
||||||
|
+-----------+-------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-------------------------+
|
||||||
|
| Mapping Engine |
|
||||||
|
| OpenLayers Logic |
|
||||||
|
+-----------+-------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-------------------------+
|
||||||
|
| GeoJSON Data Layer |
|
||||||
|
| Local assets or APIs |
|
||||||
|
+-----------+-------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-------------------------+
|
||||||
|
| External Services |
|
||||||
|
| Example: LegiScan API |
|
||||||
|
+-------------------------+
|
||||||
|
|
||||||
|
+-------------------------+
|
||||||
|
| Deployment Layer |
|
||||||
|
| Docker + Jenkins + Nginx|
|
||||||
|
+-------------------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Advantages
|
||||||
|
|
||||||
|
* Lightweight and fast due to React + Vite
|
||||||
|
* Highly customizable OpenLayers engine
|
||||||
|
* Easy to integrate external APIs
|
||||||
|
* Full support for GeoJSON datasets
|
||||||
|
* Completely containerized for reliable deployments
|
||||||
|
* Modular structure suitable for scaling into a full GIS dashboard
|
||||||
|
* Developer-friendly with TypeScript, linting, and formatted code
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/png" href="./src/assets/map.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>MapLayer</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
220
package-lock.json
generated
220
package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
||||||
"-": "^0.0.1",
|
"-": "^0.0.1",
|
||||||
"@types/leaflet": "^1.9.18",
|
"@types/leaflet": "^1.9.18",
|
||||||
"framer-motion": "^12.17.3",
|
"framer-motion": "^12.17.3",
|
||||||
|
"gsap": "^3.13.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"ol": "^10.5.0",
|
"ol": "^10.5.0",
|
||||||
"ol-ext": "^4.0.31",
|
"ol-ext": "^4.0.31",
|
||||||
|
|
@ -84,6 +85,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
|
||||||
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
|
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.26.2",
|
||||||
|
|
@ -694,10 +696,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.6.0",
|
"version": "4.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
||||||
"integrity": "sha512-WhCn7Z7TauhBtmzhvKpoQs0Wwb/kBcy4CwpuI0/eEIr2Lx2auxmulAzLr91wVZJaz47iUZdkXOK7WlAfxGKCnA==",
|
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint-visitor-keys": "^3.4.3"
|
"eslint-visitor-keys": "^3.4.3"
|
||||||
},
|
},
|
||||||
|
|
@ -733,12 +736,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-array": {
|
"node_modules/@eslint/config-array": {
|
||||||
"version": "0.20.0",
|
"version": "0.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||||
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
|
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/object-schema": "^2.1.6",
|
"@eslint/object-schema": "^2.1.7",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"minimatch": "^3.1.2"
|
"minimatch": "^3.1.2"
|
||||||
},
|
},
|
||||||
|
|
@ -747,19 +751,24 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-helpers": {
|
"node_modules/@eslint/config-helpers": {
|
||||||
"version": "0.2.1",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
||||||
"integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==",
|
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@eslint/core": "^0.17.0"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/core": {
|
"node_modules/@eslint/core": {
|
||||||
"version": "0.12.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||||
"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
|
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json-schema": "^7.0.15"
|
"@types/json-schema": "^7.0.15"
|
||||||
},
|
},
|
||||||
|
|
@ -803,48 +812,42 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.24.0",
|
"version": "9.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
|
||||||
"integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==",
|
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://eslint.org/donate"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/object-schema": {
|
"node_modules/@eslint/object-schema": {
|
||||||
"version": "2.1.6",
|
"version": "2.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
||||||
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
|
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit": {
|
"node_modules/@eslint/plugin-kit": {
|
||||||
"version": "0.2.8",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
||||||
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
|
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.13.0",
|
"@eslint/core": "^0.17.0",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"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": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
|
|
@ -1308,7 +1311,8 @@
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"devOptional": true
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/leaflet": {
|
"node_modules/@types/leaflet": {
|
||||||
"version": "1.9.18",
|
"version": "1.9.18",
|
||||||
|
|
@ -1356,6 +1360,7 @@
|
||||||
"version": "19.1.1",
|
"version": "19.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz",
|
||||||
"integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==",
|
"integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
|
|
@ -1413,6 +1418,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz",
|
||||||
"integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==",
|
"integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.29.1",
|
"@typescript-eslint/scope-manager": "8.29.1",
|
||||||
"@typescript-eslint/types": "8.29.1",
|
"@typescript-eslint/types": "8.29.1",
|
||||||
|
|
@ -1512,10 +1518,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -1607,10 +1614,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.1",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -1688,10 +1697,11 @@
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
|
@ -1728,6 +1738,7 @@
|
||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001688",
|
"caniuse-lite": "^1.0.30001688",
|
||||||
"electron-to-chromium": "^1.5.73",
|
"electron-to-chromium": "^1.5.73",
|
||||||
|
|
@ -1831,12 +1842,16 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "1.0.2",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
|
|
@ -1959,32 +1974,33 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.24.0",
|
"version": "9.39.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
||||||
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==",
|
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
"@eslint/config-array": "^0.20.0",
|
"@eslint/config-array": "^0.21.1",
|
||||||
"@eslint/config-helpers": "^0.2.0",
|
"@eslint/config-helpers": "^0.4.2",
|
||||||
"@eslint/core": "^0.12.0",
|
"@eslint/core": "^0.17.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.24.0",
|
"@eslint/js": "9.39.1",
|
||||||
"@eslint/plugin-kit": "^0.2.7",
|
"@eslint/plugin-kit": "^0.4.1",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@humanwhocodes/retry": "^0.4.2",
|
"@humanwhocodes/retry": "^0.4.2",
|
||||||
"@types/estree": "^1.0.6",
|
"@types/estree": "^1.0.6",
|
||||||
"@types/json-schema": "^7.0.15",
|
|
||||||
"ajv": "^6.12.4",
|
"ajv": "^6.12.4",
|
||||||
"chalk": "^4.0.0",
|
"chalk": "^4.0.0",
|
||||||
"cross-spawn": "^7.0.6",
|
"cross-spawn": "^7.0.6",
|
||||||
"debug": "^4.3.2",
|
"debug": "^4.3.2",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
"eslint-scope": "^8.3.0",
|
"eslint-scope": "^8.4.0",
|
||||||
"eslint-visitor-keys": "^4.2.0",
|
"eslint-visitor-keys": "^4.2.1",
|
||||||
"espree": "^10.3.0",
|
"espree": "^10.4.0",
|
||||||
"esquery": "^1.5.0",
|
"esquery": "^1.5.0",
|
||||||
"esutils": "^2.0.2",
|
"esutils": "^2.0.2",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
|
@ -2040,10 +2056,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
"version": "8.3.0",
|
"version": "8.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
||||||
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
|
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esrecurse": "^4.3.0",
|
"esrecurse": "^4.3.0",
|
||||||
"estraverse": "^5.2.0"
|
"estraverse": "^5.2.0"
|
||||||
|
|
@ -2056,10 +2073,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-visitor-keys": {
|
"node_modules/eslint-visitor-keys": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||||
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
|
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
},
|
},
|
||||||
|
|
@ -2068,14 +2086,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
"version": "10.3.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
|
||||||
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
|
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "^8.14.0",
|
"acorn": "^8.15.0",
|
||||||
"acorn-jsx": "^5.3.2",
|
"acorn-jsx": "^5.3.2",
|
||||||
"eslint-visitor-keys": "^4.2.0"
|
"eslint-visitor-keys": "^4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -2101,6 +2120,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||||
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"estraverse": "^5.2.0"
|
"estraverse": "^5.2.0"
|
||||||
},
|
},
|
||||||
|
|
@ -2358,6 +2378,12 @@
|
||||||
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
|
@ -2443,10 +2469,11 @@
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
},
|
},
|
||||||
|
|
@ -2509,7 +2536,8 @@
|
||||||
"version": "1.9.4",
|
"version": "1.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lerc": {
|
"node_modules/lerc": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
|
|
@ -2545,9 +2573,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash-es": {
|
"node_modules/lodash-es": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.23",
|
||||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
|
||||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.assign": {
|
"node_modules/lodash.assign": {
|
||||||
|
|
@ -2698,6 +2726,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/ol/-/ol-10.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/ol/-/ol-10.5.0.tgz",
|
||||||
"integrity": "sha512-nHFx8gkGmvYImsa7iKkwUnZidd5gn1XbMZd9GNOorvm9orjW9gQvT3Naw/MjIasVJ3cB9EJUdCGR2EFAulMHsQ==",
|
"integrity": "sha512-nHFx8gkGmvYImsa7iKkwUnZidd5gn1XbMZd9GNOorvm9orjW9gQvT3Naw/MjIasVJ3cB9EJUdCGR2EFAulMHsQ==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/rbush": "4.0.0",
|
"@types/rbush": "4.0.0",
|
||||||
"earcut": "^3.0.0",
|
"earcut": "^3.0.0",
|
||||||
|
|
@ -2724,6 +2753,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-is": "^0.1.3",
|
"deep-is": "^0.1.3",
|
||||||
"fast-levenshtein": "^2.0.6",
|
"fast-levenshtein": "^2.0.6",
|
||||||
|
|
@ -2945,6 +2975,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
|
@ -2967,6 +2998,7 @@
|
||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
|
|
@ -3007,9 +3039,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.6.2",
|
"version": "7.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz",
|
||||||
"integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==",
|
"integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
|
|
@ -3029,12 +3061,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "7.6.2",
|
"version": "7.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz",
|
||||||
"integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==",
|
"integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-router": "7.6.2"
|
"react-router": "7.13.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
|
@ -3171,9 +3203,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
"node_modules/set-cookie-parser": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
|
|
@ -3311,6 +3343,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -3365,6 +3398,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||||
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -3446,10 +3480,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.5",
|
"version": "6.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
|
|
@ -3609,6 +3644,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"-": "^0.0.1",
|
"-": "^0.0.1",
|
||||||
"@types/leaflet": "^1.9.18",
|
"@types/leaflet": "^1.9.18",
|
||||||
"framer-motion": "^12.17.3",
|
"framer-motion": "^12.17.3",
|
||||||
|
"gsap": "^3.13.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"ol": "^10.5.0",
|
"ol": "^10.5.0",
|
||||||
"ol-ext": "^4.0.31",
|
"ol-ext": "^4.0.31",
|
||||||
|
|
|
||||||
60
src/App.css
60
src/App.css
|
|
@ -1,42 +1,30 @@
|
||||||
#root {
|
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap');
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
:root {
|
||||||
padding: 2rem;
|
--bg-color: #050505;
|
||||||
text-align: center;
|
--text-main: #ffffff;
|
||||||
|
--text-muted: #888888;
|
||||||
|
--card-bg: rgba(255, 255, 255, 0.03);
|
||||||
|
--border-color: rgba(255, 255, 255, 0.08);
|
||||||
|
--accent: #00F0FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map {
|
[data-theme='light'] {
|
||||||
position: absolute;
|
--bg-color: #F4F4F5;
|
||||||
top: 0;
|
--text-main: #18181B;
|
||||||
bottom: 25px;
|
--text-muted: #52525B;
|
||||||
left: 0;
|
--card-bg: rgba(255, 255, 255, 0.6);
|
||||||
width: 100%;
|
--border-color: rgba(0, 0, 0, 0.08);
|
||||||
|
--accent: #0070F3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coordinates-container {
|
body {
|
||||||
position: absolute;
|
background-color: var(--bg-color);
|
||||||
bottom: -2px;
|
color: var(--text-main);
|
||||||
|
transition: background-color 0.5s ease, color 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ol-popup{
|
/* Hide Scrollbar for cleaner look */
|
||||||
position: absolute;
|
::-webkit-scrollbar { width: 6px; }
|
||||||
background-color: white;
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
padding: 5px;
|
::-webkit-scrollbar-thumb { background: rgba(128, 128, 128, 0.4); border-radius: 4px; }
|
||||||
border-radius: 5px;
|
|
||||||
border: 1px solid black;
|
|
||||||
transform: translate(-50%, -100%);
|
|
||||||
pointer-events: none;
|
|
||||||
width: 220px;
|
|
||||||
color: black;
|
|
||||||
};
|
|
||||||
|
|
||||||
.input {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
padding: 5px 15px;
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 5px 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
25
src/App.tsx
25
src/App.tsx
|
|
@ -1,23 +1,30 @@
|
||||||
|
import './App.css';
|
||||||
import './App.css'
|
import { useState } from 'react';
|
||||||
import OpenLayerMap from './components/OpenLayerMap';
|
import OpenLayerMap from './components/OpenLayerMap';
|
||||||
import OpenLayerEsri from './components/OpenLayerEsri';
|
import OpenLayerEsri from './components/OpenLayerEsri';
|
||||||
import LegiScanAPI from './components/LegiScanAPI';
|
import LegiScanAPI from './components/LegiScanAPI';
|
||||||
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||||
|
import LandingPage from './components/LandingPage';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [addressData, setAddressData] = useState<any>(null);
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||||
|
|
||||||
|
const handleSearchSuccess = (data: any, term: string) => {
|
||||||
return (
|
setAddressData(data);
|
||||||
|
setSearchTerm(term);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<OpenLayerMap />} />
|
<Route path="/" element={<LandingPage onSearchSuccess={handleSearchSuccess} />} />
|
||||||
<Route path="/open" element={<OpenLayerMap />} />
|
<Route path="/open" element={<OpenLayerMap initialAddressData={addressData} initialTerm={searchTerm} />} />
|
||||||
<Route path="/esri" element={<OpenLayerEsri />} />
|
<Route path="/esri" element={<OpenLayerEsri />} />
|
||||||
<Route path="/legi" element={<LegiScanAPI />} />
|
<Route path="/legi" element={<LegiScanAPI />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
BIN
src/assets/map.png
Normal file
BIN
src/assets/map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 718 B |
292
src/components/LandingPage.tsx
Normal file
292
src/components/LandingPage.tsx
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
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<LandingPageProps> = ({ onSearchSuccess }) => {
|
||||||
|
const [term, setTerm] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// --- REFS ---
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const glowRef = useRef<HTMLDivElement>(null);
|
||||||
|
const titleRef = useRef<HTMLHeadingElement>(null);
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(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',
|
||||||
|
},
|
||||||
|
texting: {
|
||||||
|
fontSize: 'clamp(1rem, 6vw, 1rem)',
|
||||||
|
fontWeight: 500,
|
||||||
|
margin: '80px 240px 20px 240px',
|
||||||
|
textAlign: 'center' as const,
|
||||||
|
letterSpacing: '-0.04em',
|
||||||
|
// Gradient Text Effect
|
||||||
|
background: 'linear-gradient(to bottom, #ffffff 0%, #888888 100%)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} style={styles.wrapper}>
|
||||||
|
|
||||||
|
{/* CSS Animation for loader */}
|
||||||
|
<style>{`
|
||||||
|
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
||||||
|
::selection { background: rgba(255,255,255,0.2); color: white; }
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* Ambient Background Aura */}
|
||||||
|
<div ref={glowRef} style={styles.glow} />
|
||||||
|
|
||||||
|
<div style={styles.contentZ}>
|
||||||
|
{/* Title */}
|
||||||
|
<h1 ref={titleRef} style={styles.title}>
|
||||||
|
Explore the San Diego.
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Search Component */}
|
||||||
|
<form ref={formRef} onSubmit={handleSearch} style={styles.form}>
|
||||||
|
<div style={styles.glassContainer}>
|
||||||
|
|
||||||
|
{/* Search Icon */}
|
||||||
|
<div style={{...styles.searchIcon, color: isFocused ? '#fff' : '#555'}}>
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Field */}
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={term}
|
||||||
|
onChange={(e) => setTerm(e.target.value)}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
onBlur={() => setIsFocused(false)}
|
||||||
|
placeholder="Search address, city, or place..."
|
||||||
|
style={styles.input}
|
||||||
|
disabled={isLoading}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Animated Button */}
|
||||||
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
|
type="submit"
|
||||||
|
style={styles.button}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div style={styles.loader} />
|
||||||
|
) : (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
<polyline points="12 5 19 12 12 19"></polyline>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<h3 ref={titleRef} style={styles.texting}>
|
||||||
|
MapLayer is a lightweight and powerful geospatial visualization platform designed to make mapping simple and intuitive. It allows users to load, explore, and interact with GeoJSON datasets directly in the browser using a modern React interface. With customizable layers, markers, and seamless OpenLayers integration, MapLayer transforms raw geographic data into meaningful insights. Built for developers, researchers, and analysts, it delivers speed, flexibility, and a clean mapping experience.
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LandingPage;
|
||||||
|
|
@ -1,272 +1,499 @@
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState, useLayoutEffect } from 'react';
|
||||||
import { Map, View } from 'ol';
|
import { Map, View } from 'ol';
|
||||||
import 'ol/ol.css';
|
import 'ol/ol.css';
|
||||||
import TileLayer from 'ol/layer/Tile';
|
import TileLayer from 'ol/layer/Tile';
|
||||||
import { OSM } from 'ol/source';
|
import XYZ from 'ol/source/XYZ';
|
||||||
import { fromLonLat } from 'ol/proj';
|
import { fromLonLat, toLonLat } from 'ol/proj';
|
||||||
import { Coordinate } from 'ol/coordinate';
|
import { Coordinate } from 'ol/coordinate';
|
||||||
import VectorLayer from 'ol/layer/Vector';
|
import VectorLayer from 'ol/layer/Vector';
|
||||||
import { layerConfigs } from './Methods/Layers';
|
import { layerConfigs } from './Methods/Layers';
|
||||||
import { loadAndRenderGeoJsonLayer } from './Methods/GeoJson_Load'
|
import { loadAndRenderGeoJsonLayer } from './Methods/GeoJson_Load';
|
||||||
import { addMarker } from './Methods/Marker';
|
import { addMarker } from './Methods/Marker';
|
||||||
|
import gsap from 'gsap';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { easeOut } from 'ol/easing';
|
||||||
|
|
||||||
const OpenLayersMap = () => {
|
// --- THEME CONFIGURATION ---
|
||||||
const [term, setTerm] = useState<string>('');
|
const themes = {
|
||||||
const [address, setAddress] = useState();
|
dark: {
|
||||||
const [layerVisibility, setLayerVisibility] = useState<Record<string, boolean>>({});
|
bg: '#050505',
|
||||||
const [searchCompleted, setSearchCompleted] = useState(false);
|
glass: 'rgba(20, 20, 20, 0.75)', // Slightly darker for better contrast
|
||||||
|
border: 'rgba(255, 255, 255, 0.15)', // Stronger border
|
||||||
// Submit Button Logic
|
textMain: '#ffffff',
|
||||||
const submitForm = async (event: React.FormEvent<HTMLFormElement>) => {
|
textMuted: '#AAAAAA',
|
||||||
// Preventing the page from reloading
|
iconColor: '#ffffff',
|
||||||
event.preventDefault();
|
shadow: '0 4px 20px rgba(0,0,0,0.4)', // Strong shadow
|
||||||
const response = await fetch(`https://nominatim.openstreetmap.org/search?q=${term}&format=json&polygon=1&addressdetails=1`);
|
switchTrackActive: 'rgba(255,255,255,0.2)',
|
||||||
const json = await response.json();
|
switchTrackInactive: 'rgba(255,255,255,0.05)',
|
||||||
console.log(json);
|
switchKnobActive: '#fff',
|
||||||
setAddress(json);
|
switchKnobInactive: '#666',
|
||||||
console.log(address);
|
mapUrl: 'https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
||||||
setSearchCompleted(true);
|
},
|
||||||
|
light: {
|
||||||
// Initialize all layers as visible
|
bg: '#F4F4F5',
|
||||||
const initialVisibility = Object.fromEntries(
|
glass: 'rgba(255, 255, 255, 0.90)', // More opaque for visibility
|
||||||
layerConfigs.map(layer => [layer.topic, true])
|
border: 'rgba(0, 0, 0, 0.1)',
|
||||||
);
|
textMain: '#18181B',
|
||||||
setLayerVisibility(initialVisibility);
|
textMuted: '#52525B',
|
||||||
|
iconColor: '#18181B',
|
||||||
alert(term);
|
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<HTMLDivElement>(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<string, string> = {
|
||||||
|
'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<MapViewProps> = ({ initialAddressData, initialTerm }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// -- STATE --
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState(true);
|
||||||
|
const currentTheme = isDarkMode ? themes.dark : themes.light;
|
||||||
|
|
||||||
|
const [layerVisibility, setLayerVisibility] = useState<Record<string, boolean>>(() =>
|
||||||
|
Object.fromEntries(layerConfigs.map(layer => [layer.topic, true]))
|
||||||
|
);
|
||||||
const [clickedCoordinate, setClickedCoordinate] = useState<Coordinate>();
|
const [clickedCoordinate, setClickedCoordinate] = useState<Coordinate>();
|
||||||
|
const [mapStats, setMapStats] = useState({
|
||||||
|
zoom: 2,
|
||||||
|
center: [0, 0] as Coordinate,
|
||||||
|
rotation: 0,
|
||||||
|
size: { width: 0, height: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// -- REFS --
|
||||||
|
const mapDivRef = useRef<HTMLDivElement>(null);
|
||||||
const mapRef = useRef<Map | null>(null);
|
const mapRef = useRef<Map | null>(null);
|
||||||
|
const tileLayerRef = useRef<TileLayer<XYZ> | null>(null);
|
||||||
const markerLayerRef = useRef<VectorLayer | null>(null);
|
const markerLayerRef = useRef<VectorLayer | null>(null);
|
||||||
const layerRefs = useRef<Record<string, React.MutableRefObject<VectorLayer | null>>>(
|
const layerRefs = useRef<Record<string, React.MutableRefObject<VectorLayer | null>>>(
|
||||||
Object.fromEntries(
|
Object.fromEntries(layerConfigs.map((layer) => [layer.topic, { current: null }]))
|
||||||
layerConfigs.map((layer) => [layer.topic, { current: null }])
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
const uiContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Toggle layer visibility
|
// -- GSAP ENTRANCE --
|
||||||
const toggleLayer = (layerTopic: string) => {
|
useLayoutEffect(() => {
|
||||||
const layerRef = layerRefs.current[layerTopic];
|
const ctx = gsap.context(() => {
|
||||||
const isCurrentlyVisible = layerVisibility[layerTopic];
|
// We use clearProps: 'all' to ensure GSAP doesn't leave the elements with weird opacity states
|
||||||
|
gsap.from('.floating-ui', {
|
||||||
if (layerRef.current) {
|
y: 20,
|
||||||
layerRef.current.setVisible(!isCurrentlyVisible);
|
opacity: 0,
|
||||||
}
|
duration: 0.8,
|
||||||
|
stagger: 0.1,
|
||||||
setLayerVisibility(prev => ({
|
ease: 'power3.out',
|
||||||
...prev,
|
delay: 0.5,
|
||||||
[layerTopic]: !isCurrentlyVisible
|
clearProps: 'opacity,transform' // Crucial: Ensures visibility after animation ends
|
||||||
}));
|
});
|
||||||
};
|
}, uiContainerRef);
|
||||||
|
return () => ctx.revert();
|
||||||
// Format layer name for display
|
}, []);
|
||||||
const formatLayerName = (topic: string) => {
|
|
||||||
return topic.replace(/_/g, ' ').replace(/SD$/, '').trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// -- MAP INIT --
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapRef.current) {
|
if (!mapRef.current) {
|
||||||
|
const baseLayer = new TileLayer({
|
||||||
|
source: new XYZ({
|
||||||
|
url: currentTheme.mapUrl,
|
||||||
|
attributions: '© OpenStreetMap © CARTO'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
tileLayerRef.current = baseLayer;
|
||||||
|
|
||||||
const map = new Map({
|
const map = new Map({
|
||||||
target: mapDivRef.current as HTMLDivElement,
|
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;
|
mapRef.current = map;
|
||||||
map.on('click', (e) => {
|
map.on('click', (e) => setClickedCoordinate(e.coordinate));
|
||||||
setClickedCoordinate(e.coordinate);
|
map.on('moveend', () => {
|
||||||
|
const view = map.getView();
|
||||||
|
const center = view.getCenter() ?? [0, 0];
|
||||||
|
const zoom = view.getZoom() ?? 0;
|
||||||
|
const rotation = view.getRotation() ?? 0;
|
||||||
|
const size = map.getSize();
|
||||||
|
|
||||||
|
setMapStats({
|
||||||
|
center,
|
||||||
|
zoom,
|
||||||
|
rotation,
|
||||||
|
size: { width: size?.[0] ?? 0, height: size?.[1] ?? 0 }
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (address && address.length > 0) {
|
// -- THEME SWITCHING --
|
||||||
const lat = parseFloat(address[0].lat);
|
useEffect(() => {
|
||||||
const lon = parseFloat(address[0].lon);
|
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]);
|
const coords = fromLonLat([lon, lat]);
|
||||||
mapRef.current!.setView(new View({
|
|
||||||
center: coords,
|
|
||||||
zoom: 15,
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
mapRef.current!.getView().animate({ center: coords, zoom: 14, duration: 2500, easing: easeOut });
|
||||||
markerLayerRef.current = addMarker(coords, mapRef.current!, markerLayerRef.current);
|
markerLayerRef.current = addMarker(coords, mapRef.current!, markerLayerRef.current);
|
||||||
|
|
||||||
layerConfigs.forEach((layerConfig) => {
|
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 url = layerConfig.getUrl(lon, lat, layerConfig.radius);
|
||||||
const ref = layerRefs.current[layerConfig.topic];
|
const ref = layerRefs.current[layerConfig.topic];
|
||||||
loadAndRenderGeoJsonLayer(
|
loadAndRenderGeoJsonLayer(url, mapRef.current!, ref, layerConfig.style);
|
||||||
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();
|
||||||
|
const formatNumber = (value: number, digits = 2) => Number.isFinite(value) ? value.toFixed(digits) : '—';
|
||||||
|
const centerLonLat = toLonLat(mapStats.center);
|
||||||
|
const clickedLonLat = clickedCoordinate ? toLonLat(clickedCoordinate) : null;
|
||||||
|
const activeLayerCount = Object.values(layerVisibility).filter(Boolean).length;
|
||||||
|
|
||||||
|
// -- 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'
|
||||||
|
},
|
||||||
|
rightSidebar: {
|
||||||
|
position: 'absolute' as const,
|
||||||
|
top: '92px',
|
||||||
|
right: '24px',
|
||||||
|
width: '320px',
|
||||||
|
maxHeight: 'calc(100% - 140px)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column' as const,
|
||||||
|
gap: '12px',
|
||||||
|
pointerEvents: 'auto' as const,
|
||||||
|
zIndex: 1100
|
||||||
|
},
|
||||||
|
sidebarCard: {
|
||||||
|
background: currentTheme.glass,
|
||||||
|
backdropFilter: 'blur(20px)',
|
||||||
|
WebkitBackdropFilter: 'blur(20px)',
|
||||||
|
border: `1px solid ${currentTheme.border}`,
|
||||||
|
borderRadius: '16px',
|
||||||
|
padding: '16px',
|
||||||
|
boxShadow: currentTheme.shadow,
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
},
|
||||||
|
sidebarTitle: {
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: '1px',
|
||||||
|
color: currentTheme.textMuted,
|
||||||
|
textTransform: 'uppercase' as const,
|
||||||
|
marginBottom: '12px'
|
||||||
|
},
|
||||||
|
statRow: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '6px 0',
|
||||||
|
borderBottom: `1px dashed ${currentTheme.border}`
|
||||||
|
},
|
||||||
|
statLabel: {
|
||||||
|
fontSize: '12px',
|
||||||
|
color: currentTheme.textMuted
|
||||||
|
},
|
||||||
|
statValue: {
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: currentTheme.textMain
|
||||||
|
},
|
||||||
|
layerMetaRow: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '12px',
|
||||||
|
padding: '8px 0',
|
||||||
|
borderBottom: `1px dashed ${currentTheme.border}`
|
||||||
|
},
|
||||||
|
layerMetaName: {
|
||||||
|
fontSize: '12px',
|
||||||
|
color: currentTheme.textMain,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px'
|
||||||
|
},
|
||||||
|
layerMetaBadge: {
|
||||||
|
fontSize: '10px',
|
||||||
|
fontWeight: 600,
|
||||||
|
padding: '2px 8px',
|
||||||
|
borderRadius: '999px',
|
||||||
|
border: `1px solid ${currentTheme.border}`,
|
||||||
|
color: currentTheme.textMuted
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// UI
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', height: '100vh', width: '100vw' }}>
|
<div style={styles.wrapper}>
|
||||||
{/* Sidebar */}
|
<style>{scrollbarStyles}</style>
|
||||||
<div style={{
|
|
||||||
width: '320px',
|
|
||||||
backgroundColor: '#f8f9fa',
|
|
||||||
borderRight: '1px solid #dee2e6',
|
|
||||||
padding: '20px',
|
|
||||||
overflowY: 'auto',
|
|
||||||
boxShadow: '2px 0 5px rgba(0,0,0,0.1)'
|
|
||||||
}}>
|
|
||||||
<h3 style={{
|
|
||||||
margin: '0 0 20px 0',
|
|
||||||
color: '#333',
|
|
||||||
fontSize: '18px',
|
|
||||||
fontWeight: '600'
|
|
||||||
}}>
|
|
||||||
Map Layers
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* Search Form */}
|
|
||||||
<form onSubmit={submitForm} style={{ marginBottom: '20px' }}>
|
|
||||||
<input
|
|
||||||
value={term}
|
|
||||||
onChange={(e) => 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'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
padding: '10px',
|
|
||||||
backgroundColor: '#007bff',
|
|
||||||
color: 'white',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: '14px',
|
|
||||||
fontWeight: '500'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Search
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Layer Toggle List */}
|
{/* 1. MAP LAYER */}
|
||||||
<div>
|
<div
|
||||||
<h4 style={{
|
ref={mapDivRef}
|
||||||
margin: '0 0 15px 0',
|
style={{ width: '100%', height: '100%', transition: 'filter 0.5s' }}
|
||||||
color: '#666',
|
/>
|
||||||
fontSize: '14px',
|
|
||||||
fontWeight: '500',
|
{/* 2. UI LAYER */}
|
||||||
textTransform: 'uppercase',
|
<div ref={uiContainerRef} style={styles.uiLayer}>
|
||||||
letterSpacing: '0.5px'
|
|
||||||
}}>
|
{/* TOP BAR */}
|
||||||
Available Layers ({layerConfigs.length})
|
<div style={styles.topBar}>
|
||||||
</h4>
|
|
||||||
|
{/* Back Button */}
|
||||||
{layerConfigs.map((layer) => (
|
<button
|
||||||
<div key={layer.topic} style={{
|
className="floating-ui"
|
||||||
display: 'flex',
|
style={styles.actionButton}
|
||||||
alignItems: 'center',
|
onClick={() => navigate('/')}
|
||||||
justifyContent: 'space-between',
|
title="Go Back"
|
||||||
padding: '10px 0',
|
onMouseEnter={(e) => e.currentTarget.style.borderColor = isDarkMode ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)'}
|
||||||
borderBottom: '1px solid #eee'
|
onMouseLeave={(e) => e.currentTarget.style.borderColor = currentTheme.border}
|
||||||
}}>
|
>
|
||||||
<span style={{
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||||
fontSize: '13px',
|
</button>
|
||||||
color: '#333',
|
|
||||||
flex: 1,
|
{/* Location Badge */}
|
||||||
marginRight: '10px'
|
<div className="floating-ui" style={styles.locationBadge}>
|
||||||
}}>
|
<div style={{ width: '6px', height: '6px', background: '#00FF94', borderRadius: '50%', marginRight: '12px', boxShadow: '0 0 8px #00FF94' }} />
|
||||||
{formatLayerName(layer.topic)}
|
<span style={{ fontSize: '14px', fontWeight: 600, letterSpacing: '0.5px', color: currentTheme.textMain, textTransform: 'uppercase' }}>
|
||||||
</span>
|
{initialTerm || "Exploring View"}
|
||||||
|
|
||||||
{searchCompleted && (
|
|
||||||
<label style={{
|
|
||||||
position: 'relative',
|
|
||||||
display: 'inline-block',
|
|
||||||
width: '44px',
|
|
||||||
height: '24px'
|
|
||||||
}}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={layerVisibility[layer.topic] || false}
|
|
||||||
onChange={() => toggleLayer(layer.topic)}
|
|
||||||
style={{ opacity: 0, width: 0, height: 0 }}
|
|
||||||
/>
|
|
||||||
<span style={{
|
|
||||||
position: 'absolute',
|
|
||||||
cursor: 'pointer',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: layerVisibility[layer.topic] ? '#007bff' : '#ccc',
|
|
||||||
borderRadius: '24px',
|
|
||||||
transition: '0.3s',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center'
|
|
||||||
}}>
|
|
||||||
<span style={{
|
|
||||||
position: 'absolute',
|
|
||||||
content: '',
|
|
||||||
height: '18px',
|
|
||||||
width: '18px',
|
|
||||||
left: layerVisibility[layer.topic] ? '23px' : '3px',
|
|
||||||
bottom: '3px',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
borderRadius: '50%',
|
|
||||||
transition: '0.3s',
|
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
|
|
||||||
}} />
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!searchCompleted && (
|
|
||||||
<span style={{
|
|
||||||
fontSize: '11px',
|
|
||||||
color: '#999',
|
|
||||||
fontStyle: 'italic'
|
|
||||||
}}>
|
|
||||||
Search first
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
|
{/* Theme Toggle Button */}
|
||||||
|
<button
|
||||||
|
className="floating-ui"
|
||||||
|
style={styles.actionButton}
|
||||||
|
onClick={() => setIsDarkMode(!isDarkMode)}
|
||||||
|
title="Toggle Theme"
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.borderColor = isDarkMode ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.borderColor = currentTheme.border}
|
||||||
|
>
|
||||||
|
{isDarkMode ? (
|
||||||
|
<svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
||||||
|
) : (
|
||||||
|
<svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
{/* CONTROLS */}
|
||||||
|
<div className="floating-ui" style={styles.controlsContainer}>
|
||||||
|
<div style={styles.controlsHeader}>Overlay Controls</div>
|
||||||
|
|
||||||
|
<div className="custom-scrollbar" style={styles.scrollableList}>
|
||||||
|
{layerConfigs.map((layer) => {
|
||||||
|
const isActive = layerVisibility[layer.topic];
|
||||||
|
const layerColor = extractColorFromStyle(layer.style);
|
||||||
|
|
||||||
{/* Map Container */}
|
return (
|
||||||
<div style={{ flex: 1, position: 'relative' }}>
|
<div key={layer.topic} style={styles.layerRow} onClick={() => toggleLayer(layer.topic)}>
|
||||||
<div ref={mapDivRef} style={{ width: '100%', height: '100%' }} />
|
<div style={styles.layerLabel}>
|
||||||
|
<div style={{
|
||||||
|
width: '8px', height: '8px', borderRadius: '50%',
|
||||||
|
backgroundColor: layerColor,
|
||||||
|
boxShadow: `0 0 8px ${layerColor}`,
|
||||||
|
opacity: isActive ? 1 : 0.3, transition: 'all 0.3s'
|
||||||
|
}} />
|
||||||
|
<span style={{ fontSize: '13px', color: isActive ? currentTheme.textMain : currentTheme.textMuted, transition: 'color 0.3s' }}>
|
||||||
|
{formatLayerName(layer.topic)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ ...styles.switchTrack, background: isActive ? currentTheme.switchTrackActive : currentTheme.switchTrackInactive }}>
|
||||||
|
<div style={{ ...styles.switchKnob, left: isActive ? '16px' : '2px', background: isActive ? currentTheme.switchKnobActive : currentTheme.switchKnobInactive }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* HUD */}
|
||||||
{clickedCoordinate && (
|
{clickedCoordinate && (
|
||||||
<div style={{
|
<div className="floating-ui" style={styles.hud}>
|
||||||
position: 'absolute',
|
LOCATION_LOCK: {clickedCoordinate[1].toFixed(4)} N, {clickedCoordinate[0].toFixed(4)} E
|
||||||
bottom: '20px',
|
</div>
|
||||||
left: '20px',
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
||||||
color: 'white',
|
|
||||||
padding: '10px 15px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '12px',
|
|
||||||
fontFamily: 'monospace'
|
|
||||||
}}>
|
|
||||||
Coordinates: {clickedCoordinate[0].toFixed(2)} / {clickedCoordinate[1].toFixed(2)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* RIGHT SIDEBAR */}
|
||||||
|
<div className="floating-ui" style={styles.rightSidebar}>
|
||||||
|
<div style={styles.sidebarCard}>
|
||||||
|
<div style={styles.sidebarTitle}>General Values</div>
|
||||||
|
<div style={styles.statRow}>
|
||||||
|
<span style={styles.statLabel}>Height</span>
|
||||||
|
<span style={styles.statValue}>{mapStats.size.height ? `${mapStats.size.height}px` : '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.statRow}>
|
||||||
|
<span style={styles.statLabel}>Depth</span>
|
||||||
|
<span style={styles.statValue}>{mapStats.size.width ? `${mapStats.size.width}px` : '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.statRow}>
|
||||||
|
<span style={styles.statLabel}>Zoom</span>
|
||||||
|
<span style={styles.statValue}>{formatNumber(mapStats.zoom, 2)}</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.statRow}>
|
||||||
|
<span style={styles.statLabel}>Center</span>
|
||||||
|
<span style={styles.statValue}>{formatNumber(centerLonLat[1], 4)} N / {formatNumber(centerLonLat[0], 4)} E</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.statRow}>
|
||||||
|
<span style={styles.statLabel}>Last Click</span>
|
||||||
|
<span style={styles.statValue}>{clickedLonLat ? `${formatNumber(clickedLonLat[1], 4)} N / ${formatNumber(clickedLonLat[0], 4)} E` : '—'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ ...styles.statRow, borderBottom: 'none' }}>
|
||||||
|
<span style={styles.statLabel}>Layers On</span>
|
||||||
|
<span style={styles.statValue}>{activeLayerCount}/{layerConfigs.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.sidebarCard}>
|
||||||
|
<div style={styles.sidebarTitle}>Layer Values</div>
|
||||||
|
<div className="custom-scrollbar" style={{ maxHeight: '36vh', overflowY: 'auto', paddingRight: '6px' }}>
|
||||||
|
{layerConfigs.map((layer) => {
|
||||||
|
const isActive = layerVisibility[layer.topic];
|
||||||
|
const layerColor = extractColorFromStyle(layer.style);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={layer.topic} style={{ ...styles.layerMetaRow, borderBottom: `1px dashed ${currentTheme.border}` }}>
|
||||||
|
<div style={styles.layerMetaName}>
|
||||||
|
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: layerColor, boxShadow: `0 0 8px ${layerColor}`, opacity: isActive ? 1 : 0.3 }} />
|
||||||
|
{formatLayerName(layer.topic)}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<span style={styles.layerMetaBadge}>{layer.radius}m</span>
|
||||||
|
<span style={{ ...styles.layerMetaBadge, color: isActive ? currentTheme.textMain : currentTheme.textMuted }}>
|
||||||
|
{isActive ? 'On' : 'Off'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default OpenLayersMap;
|
export default MapView;
|
||||||
478
src/components/Projectpage.tsx
Normal file
478
src/components/Projectpage.tsx
Normal file
|
|
@ -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<HTMLDivElement>(null);
|
||||||
|
// const cursorRef = useRef<HTMLDivElement>(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 (
|
||||||
|
<div ref={containerRef} style={styles.wrapper}>
|
||||||
|
|
||||||
|
{/* NAVBAR */}
|
||||||
|
<nav style={styles.nav}>
|
||||||
|
<div style={styles.logo} className="nav-item" onClick={() => window.scrollTo(0,0)}>
|
||||||
|
<div style={styles.logoDot}/> MapLayer.
|
||||||
|
</div>
|
||||||
|
<div style={styles.navLinks} className="nav-item">
|
||||||
|
{[].map((item) => (
|
||||||
|
<span key={item} style={styles.link}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.color = 'var(--text-main)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.color = 'var(--text-muted)'}>
|
||||||
|
{item}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
<button style={styles.themeBtn} onClick={toggleTheme}>
|
||||||
|
{themeMode === 'dark' ?
|
||||||
|
<svg width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
|
||||||
|
:
|
||||||
|
<svg width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* HERO SECTION */}
|
||||||
|
<header style={styles.hero}>
|
||||||
|
<div style={styles.heroText}>
|
||||||
|
|
||||||
|
|
||||||
|
<h1 style={styles.h1}>
|
||||||
|
<div style={{overflow:'hidden'}}><div className="hero-word">The Operating System</div></div>
|
||||||
|
<div style={{overflow:'hidden'}}><div className="hero-word" style={{color: 'var(--text-muted)'}}>for Modern Cities.</div></div>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="hero-desc" style={styles.desc}>
|
||||||
|
MapLayer unifies fragmented urban data into a single, high-performance geospatial decision engine. Visualize zoning, transit, and environmental risks in real-time.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="hero-desc">
|
||||||
|
<button
|
||||||
|
style={styles.btnPrimary}
|
||||||
|
onClick={handleLaunch}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
gsap.to(e.currentTarget, { scale: 1.05, duration: 0.3 });
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
gsap.to(e.currentTarget, { scale: 1, duration: 0.3 });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Launch Platform
|
||||||
|
</button>
|
||||||
|
{/* <button style={styles.btnSecondary}>Documentation</button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 3D VISUALIZATION */}
|
||||||
|
<div style={styles.visualContainer}>
|
||||||
|
<div style={styles.layerStack}>
|
||||||
|
<div className="visual-card" style={styles.layerCard(0, 'var(--border-color)')}>
|
||||||
|
BASEMAP
|
||||||
|
</div>
|
||||||
|
<div className="visual-card" style={styles.layerCard(80, 'var(--accent)')}>
|
||||||
|
INFRASTRUCTURE
|
||||||
|
</div>
|
||||||
|
<div className="visual-card" style={styles.layerCard(160, '#7000FF')}>
|
||||||
|
ZONING DATA
|
||||||
|
</div>
|
||||||
|
<div className="visual-card" style={styles.layerCard(240, 'var(--text-main)')}>
|
||||||
|
ANALYTICS
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* CHALLENGE SECTION */}
|
||||||
|
<section className="reveal-section" style={styles.section}>
|
||||||
|
<div style={{maxWidth: '800px'}}>
|
||||||
|
<span style={styles.sectionLabel}>01 — The Challenge</span>
|
||||||
|
<h2 style={styles.h2}>Data fragmentation <br/>is paralyzing progress.</h2>
|
||||||
|
<p style={styles.desc}>
|
||||||
|
Cities generate terabytes of spatial data daily. Yet, Planners and Emergency Responders operate in silos, using outdated PDF maps and disjointed portals.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.grid}>
|
||||||
|
{/* Card 1 */}
|
||||||
|
<div style={styles.card}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.borderColor = 'var(--accent)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.borderColor = 'var(--border-color)'}>
|
||||||
|
<div style={styles.icon}>
|
||||||
|
<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 style={styles.cardTitle}>Emergency Latency</h3>
|
||||||
|
<p style={styles.cardText}>First responders struggle to access integrated utility and hazard data during critical incidents, costing seconds that matter.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card 2 */}
|
||||||
|
<div style={styles.card}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.borderColor = 'var(--accent)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.borderColor = 'var(--border-color)'}>
|
||||||
|
<div style={styles.icon}>
|
||||||
|
<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"><path d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 style={styles.cardTitle}>Planning Blindspots</h3>
|
||||||
|
<p style={styles.cardText}>Zoning, environmental, and transit layers are rarely visualized together, making holistic impact analysis impossible.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card 3 */}
|
||||||
|
<div style={styles.card}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.borderColor = 'var(--accent)'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.borderColor = 'var(--border-color)'}>
|
||||||
|
<div style={styles.icon}>
|
||||||
|
<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"><path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 style={styles.cardTitle}>Transparency Gaps</h3>
|
||||||
|
<p style={styles.cardText}>Citizens lack a clear, real-time window into construction projects, road closures, and city development plans.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* SOLUTION SECTION */}
|
||||||
|
<section className="reveal-section" style={styles.section}>
|
||||||
|
<div style={{maxWidth: '800px'}}>
|
||||||
|
<span style={styles.sectionLabel}>02 — The Solution</span>
|
||||||
|
<h2 style={styles.h2}>A unified geospatial <br/>intelligence core.</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.grid}>
|
||||||
|
{/* Feature 1 */}
|
||||||
|
<div style={{paddingTop: '20px'}}>
|
||||||
|
<h3 style={{...styles.cardTitle, fontSize: '2rem', color: 'var(--accent)'}}>Modular Layering</h3>
|
||||||
|
<p style={styles.cardText}>
|
||||||
|
MapLayer's engine allows users to stack infinite datasets—parcels, sewer lines, fire zones—and toggle them instantly with zero latency.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Feature 2 */}
|
||||||
|
<div style={{paddingTop: '20px', borderLeft: '1px solid var(--border-color)', paddingLeft: '40px'}}>
|
||||||
|
<h3 style={{...styles.cardTitle, fontSize: '2rem', color: '#7000FF'}}>Real-Time Sync</h3>
|
||||||
|
<p style={styles.cardText}>
|
||||||
|
Direct integration with San Diego's Open Data portal ensures that the map you see today reflects the reality of the city right now.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* Feature 3 */}
|
||||||
|
<div style={{paddingTop: '20px', borderLeft: '1px solid var(--border-color)', paddingLeft: '40px'}}>
|
||||||
|
<h3 style={{...styles.cardTitle, fontSize: '2rem'}}>Interactive HUD</h3>
|
||||||
|
<p style={styles.cardText}>
|
||||||
|
A designer-grade UI that floats above the map, providing coordinate locking, layer analysis, and distinct color coding without clutter.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* FOOTER */}
|
||||||
|
<footer className="reveal-section" style={styles.footer}>
|
||||||
|
<h2 style={{...styles.h2, fontSize: 'clamp(3rem, 6vw, 5rem)', marginBottom: '40px'}}>
|
||||||
|
See the data. <br/> Shape the future.
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleLaunch}
|
||||||
|
style={{
|
||||||
|
padding: '24px 64px',
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
background: 'var(--text-main)',
|
||||||
|
color: 'var(--bg-color)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '2px',
|
||||||
|
fontWeight: 700,
|
||||||
|
cursor: 'pointer',
|
||||||
|
letterSpacing: '0.05em'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => gsap.to(e.currentTarget, {scale: 1.05})}
|
||||||
|
onMouseLeave={(e) => gsap.to(e.currentTarget, {scale: 1})}
|
||||||
|
>
|
||||||
|
LAUNCH PLATFORM
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div style={{marginTop: '120px', display: 'flex', justifyContent: 'space-between', borderTop: '1px solid var(--border-color)', paddingTop: '40px'}}>
|
||||||
|
<span style={{color: 'var(--text-muted)', fontSize: '0.9rem'}}>© 2024 MapLayer.</span>
|
||||||
|
<span style={{color: 'var(--text-muted)', fontSize: '0.9rem'}}>San Diego Smart City Initiative</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectShowcase;
|
||||||
|
|
@ -1,68 +1,24 @@
|
||||||
:root {
|
/* IMPORTANT: Global Reset */
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
* {
|
||||||
line-height: 1.5;
|
margin: 0;
|
||||||
font-weight: 400;
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
color-scheme: light dark;
|
html, body {
|
||||||
color: rgba(255, 255, 255, 0.87);
|
width: 100%;
|
||||||
background-color: #242424;
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden; /* Prevents horizontal scrollbars */
|
||||||
font-synthesis: none;
|
background-color: #030303; /* Match your theme.bg color so any loading gaps are black */
|
||||||
text-rendering: optimizeLegibility;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
/* Ensure the React root div also takes full width */
|
||||||
font-weight: 500;
|
#root {
|
||||||
color: #646cff;
|
width: 100%;
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue