diff --git a/backend/schemas.py b/backend/schemas.py
index e471f7a..a1ab563 100644
--- a/backend/schemas.py
+++ b/backend/schemas.py
@@ -32,6 +32,8 @@ class EmergencyRequest(BaseModel):
class HospitalCard(BaseModel):
hospital_id: str
name: str
+ lat: float
+ lng: float
eta_minutes: int
department_match: bool
distance_km: float
@@ -52,12 +54,16 @@ class EmergencyResponse(BaseModel):
class HospitalStatusCard(BaseModel):
hospital_id: str
name: str
+ lat: float
+ lng: float
eta_minutes: int
status: HospitalStatus
class EmergencyStatusResponse(BaseModel):
request_id: str
+ lat: float
+ lng: float
hospitals: List[HospitalStatusCard]
donors_alerted: int
donors_responded: int
diff --git a/backend/services/emergency_service.py b/backend/services/emergency_service.py
index a33272f..b9674a6 100644
--- a/backend/services/emergency_service.py
+++ b/backend/services/emergency_service.py
@@ -35,6 +35,8 @@ async def trigger_emergency(store, lat, lng, emergency_type, blood_group) -> Dic
{
"hospital_id": c["hospital_id"],
"name": c["name"],
+ "lat": c["lat"],
+ "lng": c["lng"],
"eta_minutes": c["eta_minutes"],
"department_match": c["department_match"],
"distance_km": c["distance_km"],
@@ -115,6 +117,8 @@ def get_status(store, request_id: str) -> Dict:
{
"hospital_id": card["hospital_id"],
"name": card["name"],
+ "lat": card["lat"],
+ "lng": card["lng"],
"eta_minutes": card["eta_minutes"],
"status": status,
}
@@ -129,6 +133,8 @@ def get_status(store, request_id: str) -> Dict:
return {
"request_id": request_id,
+ "lat": emergency["lat"],
+ "lng": emergency["lng"],
"hospitals": hospital_cards,
"donors_alerted": emergency["donors_alerted"],
"donors_responded": emergency["donors_responded"],
diff --git a/backend/services/hospital_service.py b/backend/services/hospital_service.py
index 5a22ebb..118371a 100644
--- a/backend/services/hospital_service.py
+++ b/backend/services/hospital_service.py
@@ -346,27 +346,26 @@ async def rank_hospitals(
candidates = candidates[:candidate_pool]
- scored: List[Dict] = []
- for h in candidates:
+ async def get_scored_candidate(h):
eta = await eta_minutes(lat, lng, h["lat"], h["lng"])
dept_match = needed_dept in h.get("departments", [])
prox = _proximity_score(eta)
dept = 1.0 if dept_match else 0.0
score = _score(store, h["id"], prox, dept)
+ return {
+ "hospital_id": h["id"],
+ "name": h["name"],
+ "lat": h["lat"],
+ "lng": h["lng"],
+ "eta_minutes": eta,
+ "department_match": dept_match,
+ "distance_km": h["distance_km"],
+ "status": "pending",
+ "phone": h["phone"],
+ "_score": score,
+ "_record": h,
+ }
- scored.append(
- {
- "hospital_id": h["id"],
- "name": h["name"],
- "eta_minutes": eta,
- "department_match": dept_match,
- "distance_km": h["distance_km"],
- "status": "pending",
- "phone": h["phone"],
- "_score": score,
- "_record": h,
- }
- )
-
+ scored = await asyncio.gather(*(get_scored_candidate(h) for h in candidates))
scored.sort(key=lambda c: c["_score"], reverse=True)
return scored[:top_n]
diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py
index fa319f1..f0e0b43 100644
--- a/backend/tests/test_api.py
+++ b/backend/tests/test_api.py
@@ -41,6 +41,8 @@ def test_emergency_returns_contract_shape(client):
assert set(card) == {
"hospital_id",
"name",
+ "lat",
+ "lng",
"eta_minutes",
"department_match",
"distance_km",
@@ -92,6 +94,8 @@ def test_status_shape(client):
body = client.get(f"/emergency/{request_id}/status").json()
assert set(body) == {
"request_id",
+ "lat",
+ "lng",
"hospitals",
"donors_alerted",
"donors_responded",
@@ -101,7 +105,9 @@ def test_status_shape(client):
# Freshly created — well inside the no-confirmation window.
assert body["unconfirmed_fallback"] is False
card = body["hospitals"][0]
- assert set(card) == {"hospital_id", "name", "eta_minutes", "status"}
+ assert set(card) == {
+ "hospital_id", "name", "lat", "lng", "eta_minutes", "status"
+ }
# --- POST /confirm/{token} — the live flow --------------------------------
diff --git a/frontend/index.html b/frontend/index.html
index 0794bb4..7a2dbbd 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,12 +3,10 @@
-
-
- GoldenHour — Every Second Counts
+ GoldenHour — Emergency Hospital & Blood Response
=6.0.0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
@@ -283,6 +295,29 @@
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
"license": "Apache-2.0"
},
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
@@ -422,6 +457,16 @@
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
+ "node_modules/@gsap/react": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz",
+ "integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==",
+ "license": "SEE LICENSE AT https://gsap.com/standard-license",
+ "peerDependencies": {
+ "gsap": "^3.12.5",
+ "react": ">=17"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -1231,6 +1276,49 @@
"vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
+ "node_modules/@turf/boolean-point-in-polygon": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.3.5.tgz",
+ "integrity": "sha512-ba7+B0wzaS9GtERZOoXUZ6oW8IcIJHNQZf3c+tiD9ESjcsPO1Q/4qIJGTKl92nBLhhracHJxMWBM/U6hAVkaRg==",
+ "license": "MIT",
+ "dependencies": {
+ "@turf/helpers": "7.3.5",
+ "@turf/invariant": "7.3.5",
+ "@types/geojson": "^7946.0.10",
+ "point-in-polygon-hao": "^1.1.0",
+ "tslib": "^2.8.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/turf"
+ }
+ },
+ "node_modules/@turf/helpers": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.5.tgz",
+ "integrity": "sha512-E/NMGV5MwbjjP7AJXBtsanC3yY8N2MQ87IGdIgkB2ji5AtBpwnH4L3gEqpYN4RlCJJWbLbzO91BbKv2waUd0eg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "^7946.0.10",
+ "tslib": "^2.8.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/turf"
+ }
+ },
+ "node_modules/@turf/invariant": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.3.5.tgz",
+ "integrity": "sha512-ZVIvsBvjr8lO7WxC5zYNjRsjSDvyGvWkJMjuWaJjTU8x+1tmfNnw3gDX/TI2Sit83gcRYLYkNo23lB/udqx/Hg==",
+ "license": "MIT",
+ "dependencies": {
+ "@turf/helpers": "7.3.5",
+ "@types/geojson": "^7946.0.10",
+ "tslib": "^2.8.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/turf"
+ }
+ },
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
@@ -1262,6 +1350,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1269,13 +1363,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/leaflet": {
+ "version": "1.9.21",
+ "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
+ "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@types/react": {
"version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1342,13 +1445,21 @@
}
}
},
+ "node_modules/accessor-fn": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz",
+ "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/acorn": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1476,7 +1587,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -1541,6 +1651,180 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo-voronoi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/d3-geo-voronoi/-/d3-geo-voronoi-2.1.0.tgz",
+ "integrity": "sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-delaunay": "6",
+ "d3-geo": "3",
+ "d3-tricontour": "1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-octree": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
+ "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
+ "license": "MIT"
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-tricontour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/d3-tricontour/-/d3-tricontour-1.1.0.tgz",
+ "integrity": "sha512-G7gHKj89n2owmkGb6WX6ixcnQ0Kf/0wpa9VIh9DGdbHu8wdrlaHU4ir3/bFNERl8N8nn4G7e7qbtBG8N9caihQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-delaunay": "6",
+ "d3-scale": "4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-bind-mapper": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/data-bind-mapper/-/data-bind-mapper-1.0.3.tgz",
+ "integrity": "sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw==",
+ "license": "MIT",
+ "dependencies": {
+ "accessor-fn": "1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -1566,6 +1850,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/delaunator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1576,6 +1869,12 @@
"node": ">=8"
}
},
+ "node_modules/earcut": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
+ "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
+ "license": "ISC"
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.375",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz",
@@ -1626,7 +1925,6 @@
"integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"workspaces": [
"packages/*"
],
@@ -1902,6 +2200,20 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/float-tooltip": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz",
+ "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==",
+ "license": "MIT",
+ "dependencies": {
+ "d3-selection": "2 - 3",
+ "kapsule": "^1.16",
+ "preact": "10"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@@ -1916,6 +2228,15 @@
"url": "https://github.com/sponsors/rawify"
}
},
+ "node_modules/frame-ticker": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/frame-ticker/-/frame-ticker-1.0.3.tgz",
+ "integrity": "sha512-E0X2u2JIvbEMrqEg5+4BpTqaD22OwojJI63K7MdKHdncjtAhGRbCR8nJCr2vwEt9NWBPCPcu70X9smPviEBy8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "simplesignal": "^2.1.6"
+ }
+ },
"node_modules/framer-motion": {
"version": "12.40.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz",
@@ -1994,6 +2315,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/globe.gl": {
+ "version": "2.46.1",
+ "resolved": "https://registry.npmjs.org/globe.gl/-/globe.gl-2.46.1.tgz",
+ "integrity": "sha512-h+OvX52EBIPLtM0/2JkM+JZ9gPAhPJ4y3+hxUwD5Ey/O0Zk2ockuTiJ71bZbnNBGmNiIZzA5Vr3TMT0b3d35IQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tweenjs/tween.js": "18 - 25",
+ "accessor-fn": "1",
+ "kapsule": "^1.16",
+ "three": ">=0.179 <1",
+ "three-globe": "^2.45",
+ "three-render-objects": "^1.41"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -2007,6 +2345,17 @@
"integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
},
+ "node_modules/h3-js": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/h3-js/-/h3-js-4.4.0.tgz",
+ "integrity": "sha512-DvJh07MhGgY2KcC4OeZc8SSyA+ZXpdvoh6uCzGpoKvWtZxJB+g6VXXC1+eWYkaMIsLz7J/ErhOalHCpcs1KYog==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=4",
+ "npm": ">=3",
+ "yarn": ">=1.3.0"
+ }
+ },
"node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -2053,6 +2402,24 @@
"node": ">=0.8.19"
}
},
+ "node_modules/index-array-by": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz",
+ "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2083,6 +2450,15 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/jerrypick": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz",
+ "integrity": "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
@@ -2097,7 +2473,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/jsesc": {
@@ -2147,6 +2522,18 @@
"node": ">=6"
}
},
+ "node_modules/kapsule": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz",
+ "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash-es": "4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -2157,6 +2544,12 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/leaflet": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
+ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/lenis": {
"version": "1.3.23",
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.23.tgz",
@@ -2479,6 +2872,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -2588,6 +2999,15 @@
"node": ">=18"
}
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2671,7 +3091,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -2679,6 +3098,27 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/point-in-polygon-hao": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz",
+ "integrity": "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==",
+ "license": "MIT",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
+ "node_modules/polished": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz",
+ "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.17.8"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
@@ -2699,7 +3139,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
@@ -2716,6 +3155,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/preact": {
+ "version": "10.29.2",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz",
+ "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -2726,6 +3175,17 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2741,7 +3201,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2751,7 +3210,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -2759,6 +3217,44 @@
"react": "^19.2.7"
}
},
+ "node_modules/react-globe.gl": {
+ "version": "2.38.0",
+ "resolved": "https://registry.npmjs.org/react-globe.gl/-/react-globe.gl-2.38.0.tgz",
+ "integrity": "sha512-NQkMy5BGD269lkdjm9BJ2PEK81++y9H82+j4/QuWVlfXPJPTVfzaxnkC3MvaiGmpus8P6czq58OfMeYvn/4Xqg==",
+ "license": "MIT",
+ "dependencies": {
+ "globe.gl": "^2.46",
+ "prop-types": "15",
+ "react-kapsule": "^2.5"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-kapsule": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.6.0.tgz",
+ "integrity": "sha512-HzLJoYb1n1kfwjXbqFFcRR0EA6oPsJ64tNdDmCSaL/bz2o9wUZRSb0cMe//grLFeF9EVoL4CD/e6ozLyzEv+PQ==",
+ "license": "MIT",
+ "dependencies": {
+ "jerrypick": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "react": ">=16.13.1"
+ }
+ },
"node_modules/react-router": {
"version": "6.30.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
@@ -2793,6 +3289,12 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/robust-predicates": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+ "license": "Unlicense"
+ },
"node_modules/rolldown": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
@@ -2866,6 +3368,12 @@
"node": ">=8"
}
},
+ "node_modules/simplesignal": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/simplesignal/-/simplesignal-2.1.7.tgz",
+ "integrity": "sha512-PEo2qWpUke7IMhlqiBxrulIFvhJRLkl1ih52Rwa+bPjzhJepcd4GIjn2RiQmFSx3dQvsEAgF0/lXMwMN7vODaA==",
+ "license": "MIT"
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2903,6 +3411,117 @@
"integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
"license": "MIT"
},
+ "node_modules/three-conic-polygon-geometry": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/three-conic-polygon-geometry/-/three-conic-polygon-geometry-2.1.3.tgz",
+ "integrity": "sha512-7KYcgd7rYg91L8AnCEddeyhw4qlv9+8NAI0luHq7QGt26icWUGNCsvLyD+u6FPKjP+un3zGBi4JGzqgGpmBSWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@turf/boolean-point-in-polygon": "^7.2",
+ "d3-array": "1 - 3",
+ "d3-geo": "1 - 3",
+ "d3-geo-voronoi": "2",
+ "d3-scale": "1 - 4",
+ "delaunator": "5",
+ "earcut": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "three": ">=0.72.0"
+ }
+ },
+ "node_modules/three-geojson-geometry": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/three-geojson-geometry/-/three-geojson-geometry-2.1.1.tgz",
+ "integrity": "sha512-dC7bF3ri1goDcihYhzACHOBQqu7YNNazYLa2bSydVIiJUb3jDFojKSy+gNj2pMkqZNSVjssSmdY9zlmnhEpr1w==",
+ "license": "MIT",
+ "dependencies": {
+ "d3-geo": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "earcut": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "three": ">=0.72.0"
+ }
+ },
+ "node_modules/three-globe": {
+ "version": "2.45.2",
+ "resolved": "https://registry.npmjs.org/three-globe/-/three-globe-2.45.2.tgz",
+ "integrity": "sha512-3qJE2LAdyHsUPt02mgMRc+PG3j9kGEA0fUYrwKPGIVtvMR1XjDn9hCXu31AWocdgHOFcXkrRVz7jJZzTIvR0eQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tweenjs/tween.js": "18 - 25",
+ "accessor-fn": "1",
+ "d3-array": "3",
+ "d3-color": "3",
+ "d3-geo": "3",
+ "d3-interpolate": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "data-bind-mapper": "1",
+ "frame-ticker": "1",
+ "h3-js": "4",
+ "index-array-by": "1",
+ "kapsule": "^1.16",
+ "three-conic-polygon-geometry": "2",
+ "three-geojson-geometry": "2",
+ "three-slippy-map-globe": "1",
+ "tinycolor2": "1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "three": ">=0.154"
+ }
+ },
+ "node_modules/three-render-objects": {
+ "version": "1.42.0",
+ "resolved": "https://registry.npmjs.org/three-render-objects/-/three-render-objects-1.42.0.tgz",
+ "integrity": "sha512-KYfkPrYGEbIK8ChFocWqOF1aAN80FBUBWVYB8mB2oBpVuVN+52FvvngVYB5ieFANQu7Rt21rPYZ/xKaAgVWWRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tweenjs/tween.js": "18 - 25",
+ "accessor-fn": "1",
+ "float-tooltip": "^1.7",
+ "kapsule": "^1.16",
+ "polished": "4"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "three": ">=0.179"
+ }
+ },
+ "node_modules/three-slippy-map-globe": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/three-slippy-map-globe/-/three-slippy-map-globe-1.0.6.tgz",
+ "integrity": "sha512-PCUR+X+1kYFYtQBf8+b/ct8xBHtnkeu7FItRYBeFxyIe3ksnGuLi0H9RAxAfVSSUsZVbKIKNz9q1atEjynrrkg==",
+ "license": "MIT",
+ "dependencies": {
+ "d3-geo": "1 - 3",
+ "d3-octree": "^1.1",
+ "d3-scale": "1 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "three": ">=0.154"
+ }
+ },
+ "node_modules/tinycolor2": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
+ "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -2986,7 +3605,6 @@
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
@@ -3111,7 +3729,6 @@
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/frontend/package.json b/frontend/package.json
index 95a1508..55822f7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -22,19 +22,23 @@
"preview": "vite preview"
},
"dependencies": {
+ "@gsap/react": "^2.1.2",
"@supabase/supabase-js": "^2.108.2",
"@types/three": "^0.184.1",
"framer-motion": "^12.40.0",
"gsap": "^3.15.0",
+ "leaflet": "^1.9.4",
"lenis": "^1.3.23",
"lucide-react": "^1.21.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
+ "react-globe.gl": "^2.38.0",
"three": "^0.184.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.3.1",
+ "@types/leaflet": "^1.9.21",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
diff --git a/frontend/public/hero_ambulance.png b/frontend/public/hero_ambulance.png
new file mode 100644
index 0000000..bc22825
Binary files /dev/null and b/frontend/public/hero_ambulance.png differ
diff --git a/frontend/public/hero_ambulance_speed.png b/frontend/public/hero_ambulance_speed.png
new file mode 100644
index 0000000..6c01586
Binary files /dev/null and b/frontend/public/hero_ambulance_speed.png differ
diff --git a/frontend/public/hero_app_mockup.png b/frontend/public/hero_app_mockup.png
new file mode 100644
index 0000000..bf545dd
Binary files /dev/null and b/frontend/public/hero_app_mockup.png differ
diff --git a/frontend/public/hero_blood_donor.png b/frontend/public/hero_blood_donor.png
new file mode 100644
index 0000000..863299d
Binary files /dev/null and b/frontend/public/hero_blood_donor.png differ
diff --git a/frontend/public/hero_dispatch_control.png b/frontend/public/hero_dispatch_control.png
new file mode 100644
index 0000000..8e6636e
Binary files /dev/null and b/frontend/public/hero_dispatch_control.png differ
diff --git a/frontend/public/hero_hospital_er.png b/frontend/public/hero_hospital_er.png
new file mode 100644
index 0000000..6aeb1f4
Binary files /dev/null and b/frontend/public/hero_hospital_er.png differ
diff --git a/frontend/public/hero_paramedics.png b/frontend/public/hero_paramedics.png
new file mode 100644
index 0000000..3845837
Binary files /dev/null and b/frontend/public/hero_paramedics.png differ
diff --git a/frontend/public/hero_sos_dispatch.png b/frontend/public/hero_sos_dispatch.png
new file mode 100644
index 0000000..9f764f7
Binary files /dev/null and b/frontend/public/hero_sos_dispatch.png differ
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 8c8a459..807fd7a 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,12 +1,16 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
+import { ReactLenis, useLenis } from 'lenis/react';
+import 'lenis/dist/lenis.css';
+
import { AppBar } from './components/ui/AppBar';
import { CustomCursor } from './components/ui/CustomCursor';
import PatientIntakeView from './components/PatientIntakeView';
import PatientResultsView from './components/PatientResultsView';
import HospitalConfirmPage from './components/HospitalConfirmPage';
import DonorRegistration from './components/DonorRegistration';
+import { gsap, ScrollTrigger } from './lib/gsap-setup';
// Shared premium page entry animation wrapper
const PageWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
@@ -23,20 +27,44 @@ const PageWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
);
};
-function AppContent() {
+function AppContent({ prefersReduced }: { prefersReduced: boolean }) {
const location = useLocation();
const isHome = location.pathname === '/';
const isRegister = location.pathname === '/register';
+ const isResults = location.pathname.startsWith('/results/');
const isHospitalConfirm = location.pathname.startsWith('/confirm/');
- // Dark theme for home and donor registration
- const theme = isHome || isRegister ? 'dark' : 'light';
+ // Dark theme for home, donor registration, and results page
+ const theme = isHome || isRegister || isResults ? 'dark' : 'light';
+
+ // Sync Lenis with GSAP ScrollTrigger
+ const lenis = useLenis();
+
+ useEffect(() => {
+ if (!lenis || prefersReduced) return;
+
+ const handleScroll = () => {
+ ScrollTrigger.update();
+ };
+ lenis.on('scroll', handleScroll);
+
+ const updateRaf = (time: number) => {
+ lenis.raf(time * 1000);
+ };
+ gsap.ticker.add(updateRaf);
+ gsap.ticker.lagSmoothing(0);
+
+ return () => {
+ lenis.off('scroll', handleScroll);
+ gsap.ticker.remove(updateRaf);
+ };
+ }, [lenis, prefersReduced]);
return (
{
+ const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
+ setPrefersReduced(mediaQuery.matches);
+ const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches);
+ mediaQuery.addEventListener('change', handler);
+ return () => mediaQuery.removeEventListener('change', handler);
+ }, []);
+
return (
-
+
+
+
);
}
diff --git a/frontend/src/components/DonorRegistration.tsx b/frontend/src/components/DonorRegistration.tsx
index ce1ea0b..bebbfec 100644
--- a/frontend/src/components/DonorRegistration.tsx
+++ b/frontend/src/components/DonorRegistration.tsx
@@ -6,6 +6,7 @@ import { Input } from './ui/Input';
import { Select } from './ui/Select';
import { Button } from './ui/Button';
import { api } from '../lib/api';
+import { AuroraBackground } from './ui/Effects';
export default function DonorRegistration() {
const navigate = useNavigate();
@@ -138,276 +139,241 @@ export default function DonorRegistration() {
const isButtonDisabled = !name.trim() || !phone.trim() || !bloodGroup;
return (
-
-
- {/* Top accent bar */}
-
-
-
-
- {!isRegistered ? (
-
- {/* Header / Hero */}
-
-
-
-
Donor Network
+
+
+
+ {/* Visual Identity Highlight (GoldenHour crimson to amber stripe) */}
+
+
+
+ {!isRegistered ? (
+
+ {/* Header / Hero */}
+
+
+ Become a donor. Save a life.
+
+
+ Register to receive alerts when emergency blood replacements are needed within 5km.
+
+
+
+
+
+ {/* Optional Date Picker */}
+ setLastDonated(e.target.value)}
+ />
+
+ {/* Submit Registration Button */}
+
+ SUBMIT REGISTRATION
+
+
+
+ ) : (
+ /* Premium Success State Panel */
+
+ {/* Animated Ring Checkmark */}
+
+
+
+
You're registered 🎉
+
+ Thank you, {name} . We've logged your profile under Donor ID {registeredId} .
+
+
+ We'll alert you immediately when someone nearby needs your blood type ({bloodGroup}).
+
+
+
+ navigate('/')}
+ variant="ghost"
+ fullWidth
+ className="mt-6"
+ >
+ Return to Intake
+
+
+ )}
+
+
+
+
);
}
diff --git a/frontend/src/components/HospitalConfirmPage.tsx b/frontend/src/components/HospitalConfirmPage.tsx
index cb722dd..ce7117a 100644
--- a/frontend/src/components/HospitalConfirmPage.tsx
+++ b/frontend/src/components/HospitalConfirmPage.tsx
@@ -4,6 +4,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import { Card } from './ui/Card';
import { Button } from './ui/Button';
import { api } from '../lib/api';
+import { AuroraBackground, ShimmerSkeleton } from './ui/Effects';
export default function HospitalConfirmPage() {
// Read token from URL
@@ -83,52 +84,53 @@ export default function HospitalConfirmPage() {
};
return (
-
-
- {/* Compact Header: Brand + Status Label */}
-
-
-
-
-
-
-
- GoldenHour
-
-
+
+
-
-
-
-
- Emergency Request
+ {/* Compact Header: Brand + Status Label */}
+
+
+
+
+
+
+
+ GoldenHour
+
+
+
+
+
+
+
+ Emergency Request
+
+ {!isLoading && (
+
{hospitalName}
+ )}
- {!isLoading && (
-
{hospitalName}
- )}
-
-
- {isLoading ? (
-
-
-
-
-
-
+
+ {isLoading ? (
+
+
+
+
+
+
+
+
+
+
+
+
+
) : !isResponded ? (
-
+
+
);
}
diff --git a/frontend/src/components/PatientIntakeView.tsx b/frontend/src/components/PatientIntakeView.tsx
index 6554c6a..b9744d8 100644
--- a/frontend/src/components/PatientIntakeView.tsx
+++ b/frontend/src/components/PatientIntakeView.tsx
@@ -1,15 +1,18 @@
import React, { useState, useEffect, useRef } from 'react';
-import { useNavigate, Link } from 'react-router-dom';
-import { AnimatePresence, motion } from 'framer-motion';
+import { useNavigate } from 'react-router-dom';
+import { AnimatePresence, motion, useScroll, useTransform } from 'framer-motion';
import { gsap, ScrollTrigger } from '../lib/gsap-setup';
-import { HeroScene } from './three/HeroScene';
-import { TextReveal } from './motion/TextReveal';
-import { CardReveal } from './motion/CardReveal';
import { CountUp } from './motion/CountUp';
-import { Card } from './ui/Card';
+import { TextReveal } from './motion/TextReveal';
+import { EkgSpine } from './motion/EkgSpine';
+import { HeroCinematicCarousel } from './motion/HeroCinematicCarousel';
+import { HowItWorksCinematic } from './motion/HowItWorksCinematic';
import { Button } from './ui/Button';
import { Select } from './ui/Select';
import { api } from '../lib/api';
+import { Droplet, Zap, Phone } from 'lucide-react';
+
+// Cinematic slides are now managed inside HeroCinematicCarousel (SOS always first).
export default function PatientIntakeView() {
const navigate = useNavigate();
@@ -26,14 +29,36 @@ export default function PatientIntakeView() {
// Form submission states
const [isSubmitting, setIsSubmitting] = useState
(false);
const [submitError, setSubmitError] = useState(null);
- const [isSceneLoaded, setIsSceneLoaded] = useState(false);
+
+ // Dispatch transition states
+ const [dispatchState, setDispatchState] = useState<'idle' | 'dispatching'>('idle');
+ const [currentStep, setCurrentStep] = useState(0);
+ const [animationFinished, setAnimationFinished] = useState(false);
+ const [prefersReduced, setPrefersReduced] = useState(false);
+ const [isMobile, setIsMobile] = useState(false);
+ const heroRef = useRef(null);
+
+ // Ref to hold resolved request_id so it can be navigated to after animation finishes
+ const resolvedRequestIdRef = useRef(null);
+
+ // Carousel state now lives inside HeroCinematicCarousel component.
// Refs for GSAP animations
const heroTitleRef = useRef(null);
const heroSubRef = useRef(null);
const scrollHintRef = useRef(null);
- const horizontalRef = useRef(null);
- const horizontalInnerRef = useRef(null);
+ const intakeSectionRef = useRef(null);
+ const intakeLeftRef = useRef(null);
+ const intakeRightRef = useRef(null);
+
+ // Parallax scroll tracking
+ const { scrollYProgress } = useScroll({
+ target: heroRef,
+ offset: ["start start", "end start"]
+ });
+
+ const y = useTransform(scrollYProgress, [0, 1], [0, (prefersReduced || isMobile) ? 0 : 120]);
+ const darkenOpacity = useTransform(scrollYProgress, [0, 1], [0, (prefersReduced || isMobile) ? 0 : 0.6]);
// Trigger Geolocation API
const handleAcquireLocation = () => {
@@ -69,23 +94,86 @@ export default function PatientIntakeView() {
if (!isFormValid || !coords) return;
setIsSubmitting(true);
setSubmitError(null);
- try {
- const data = await api.triggerEmergency(coords.lat, coords.lng, emergencyType, bloodGroup);
- if (data && data.request_id) {
- sessionStorage.setItem(`emergency_${data.request_id}`, JSON.stringify({
- bloodGroup,
- emergencyType,
- rareGroup: data.rare_group ?? bloodGroup.endsWith('-')
- }));
- navigate(`/results/${data.request_id}`);
- } else {
- throw new Error('Invalid request ID returned from server.');
+ resolvedRequestIdRef.current = null;
+ setAnimationFinished(false);
+ setCurrentStep(0);
+ setDispatchState('dispatching');
+
+ // Timer list to clear on cleanup/error
+ const activeTimers: NodeJS.Timeout[] = [];
+
+ // Trigger API call in background
+ const apiCallPromise = api.triggerEmergency(coords.lat, coords.lng, emergencyType, bloodGroup)
+ .then((data) => {
+ if (data && data.request_id) {
+ sessionStorage.setItem(`emergency_${data.request_id}`, JSON.stringify({
+ bloodGroup,
+ emergencyType,
+ rareGroup: data.rare_group ?? bloodGroup.endsWith('-')
+ }));
+ resolvedRequestIdRef.current = data.request_id;
+ return data.request_id;
+ } else {
+ throw new Error('Invalid request ID returned from server.');
+ }
+ });
+
+ if (prefersReduced) {
+ // Reduced motion: navigate immediately when API call finishes
+ try {
+ const requestId = await apiCallPromise;
+ setDispatchState('idle');
+ setIsSubmitting(false);
+ navigate(`/results/${requestId}`);
+ } catch (err: any) {
+ setSubmitError(err.message || 'Connection failed. Please verify that the API is online.');
+ setDispatchState('idle');
+ setIsSubmitting(false);
+ }
+ return;
+ }
+
+ // Standard motion: orchestrate step-by-step checkmarks sequence (1.0s total)
+ // Step 1 checkmark: 300ms
+ activeTimers.push(setTimeout(() => {
+ setCurrentStep(1);
+ }, 300));
+
+ // Step 2 checkmark: 600ms
+ activeTimers.push(setTimeout(() => {
+ setCurrentStep(2);
+ }, 600));
+
+ // Step 3 checkmark: 900ms
+ activeTimers.push(setTimeout(() => {
+ setCurrentStep(3);
+ }, 900));
+
+ // Sequence completion check: 1000ms
+ activeTimers.push(setTimeout(async () => {
+ setAnimationFinished(true);
+
+ // Wait for API call to complete if it hasn't already
+ try {
+ const requestId = await apiCallPromise;
+ setDispatchState('idle');
+ setIsSubmitting(false);
+ navigate(`/results/${requestId}`);
+ } catch (err: any) {
+ setSubmitError(err.message || 'Connection failed. Please verify that the API is online.');
+ setDispatchState('idle');
+ setIsSubmitting(false);
}
- } catch (err: any) {
+ }, 1000));
+
+ // Watch API call concurrently to handle early failures
+ apiCallPromise.catch((err: any) => {
+ // Clear all active timers immediately on error to abort sequence
+ activeTimers.forEach(clearTimeout);
setSubmitError(err.message || 'Connection failed. Please verify that the API is online.');
- } finally {
+ setDispatchState('idle');
setIsSubmitting(false);
- }
+ });
};
const typeOptions = [
@@ -104,44 +192,40 @@ export default function PatientIntakeView() {
{ value: 'AB+', label: 'AB+' }, { value: 'AB-', label: 'AB-' }
];
- // === GSAP ANIMATIONS ===
+ const stepDetails = [
+ { label: 'Locking GPS', showAtStep: 0 },
+ { label: 'Matching nearest hospital', showAtStep: 1 },
+ { label: 'Broadcasting to donors in range', showAtStep: 2 },
+ ];
- // Hero staggered title reveal
+ // Hero staggered title reveal — re-runs when shouldRenderGlobe settles so the
+ // animation fires AFTER the Suspense swap (which would otherwise trigger ctx.revert).
useEffect(() => {
- const ctx = gsap.context(() => {
- // Title letters stagger
+ // Skip animation if reduced motion is preferred — chars are visible by default,
+ // just ensure no inline transforms are applied.
+ if (prefersReduced) {
if (heroTitleRef.current) {
- const text = heroTitleRef.current.textContent || '';
- const words = text.trim().split(' ');
- heroTitleRef.current.innerHTML = '';
-
- words.forEach((word, wordIdx) => {
- const wordSpan = document.createElement('span');
- wordSpan.style.display = 'inline-block';
- wordSpan.style.whiteSpace = 'nowrap';
-
- word.split('').forEach((char) => {
- const span = document.createElement('span');
- span.textContent = char;
- span.style.display = 'inline-block';
- span.style.opacity = '0';
- span.style.transform = 'translateY(100%)';
- wordSpan.appendChild(span);
- });
-
- heroTitleRef.current!.appendChild(wordSpan);
- if (wordIdx < words.length - 1) {
- heroTitleRef.current!.appendChild(document.createTextNode(' '));
- }
- });
+ const chars = heroTitleRef.current.querySelectorAll('.char-span');
+ chars.forEach(c => { c.style.transform = 'none'; });
+ }
+ if (heroSubRef.current) heroSubRef.current.style.opacity = '1';
+ if (scrollHintRef.current) scrollHintRef.current.style.opacity = '1';
+ return;
+ }
- gsap.to(heroTitleRef.current.querySelectorAll('span > span'), {
- opacity: 1,
+ const ctx = gsap.context(() => {
+ // Title letters stagger — use y transform reveal (word-spans have overflow:hidden)
+ // so chars slide up into view without needing opacity at all,
+ // preserving the parent text-gradient clip.
+ if (heroTitleRef.current) {
+ const chars = heroTitleRef.current.querySelectorAll('.char-span');
+ gsap.set(chars, { y: '110%' });
+ gsap.to(chars, {
y: 0,
- duration: 0.8,
+ duration: 0.7,
stagger: 0.03,
ease: 'power3.out',
- delay: 0.3,
+ delay: 0.2,
});
}
@@ -163,93 +247,146 @@ export default function PatientIntakeView() {
});
return () => ctx.revert();
- }, []);
+ // Re-runs when prefersReduced changes so animation respects motion preference.
+ }, [prefersReduced]);
- // Horizontal scroll section
+ // Intake section scroll-driven entrance animation (3D tilt reveal)
useEffect(() => {
- const section = horizontalRef.current;
- const inner = horizontalInnerRef.current;
- if (!section || !inner) return;
+ if (prefersReduced) return;
+
+ const section = intakeSectionRef.current;
+ const leftEl = intakeLeftRef.current;
+ const rightEl = intakeRightRef.current;
+
+ if (!section || !leftEl || !rightEl) return;
+
+ // Set perspective on the parent container to enable 3D transforms
+ gsap.set(section, { perspective: 1000 });
const ctx = gsap.context(() => {
- const cards = inner.children;
- const totalWidth = inner.scrollWidth - section.offsetWidth;
-
- gsap.to(inner, {
- x: -totalWidth,
- ease: 'none',
- scrollTrigger: {
- trigger: section,
- start: 'top top',
- end: () => `+=${totalWidth}`,
- scrub: 1,
- pin: true,
- anticipatePin: 1,
+ // Left description column: slide in from left and fade in
+ gsap.fromTo(leftEl,
+ { opacity: 0, x: -50 },
+ {
+ opacity: 1,
+ x: 0,
+ scrollTrigger: {
+ trigger: section,
+ start: 'top 85%',
+ end: 'top 45%',
+ scrub: 1,
+ }
+ }
+ );
+
+ // Right dispatch console: 3D rotate entry, scale up, and slide up
+ gsap.fromTo(rightEl,
+ {
+ opacity: 0,
+ y: 100,
+ scale: 0.93,
+ transformOrigin: '50% 50%',
+ rotationX: 12,
+ rotationY: -8,
},
- });
- });
+ {
+ opacity: 1,
+ y: 0,
+ scale: 1,
+ rotationX: 0,
+ rotationY: 0,
+ scrollTrigger: {
+ trigger: section,
+ start: 'top 85%',
+ end: 'top 40%',
+ scrub: 1,
+ }
+ }
+ );
+ }, section);
return () => ctx.revert();
- }, []);
+ }, [prefersReduced]);
+
+ const containerVariants = {
+ hidden: {},
+ visible: {
+ transition: {
+ staggerChildren: 0.15,
+ }
+ }
+ };
+
+ const cardVariants = {
+ hidden: prefersReduced ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: {
+ duration: 0.5,
+ ease: 'easeOut'
+ }
+ }
+ };
return (
-
- {/* Real-time WebGL shader preloader overlay */}
-
- {!isSceneLoaded && (
-
-
- {/* Pulse heartbeat SVG */}
-
-
-
-
- Compiling Volumetric Shaders
-
-
-
-
-
- Preloading GPU Textures...
-
-
-
- )}
-
+
+
{/* =============================================
SECTION 1: HERO — Full screen dark canvas
============================================= */}
-
- {/* Three.js particle canvas with shader compile feedback callback */}
- setIsSceneLoaded(true)} />
+
+ {/* Background Canvas: Cinematic carousel (always) + optional 3D Globe overlay on desktop */}
+ {/* Carousel always visible — globe renders on top for desktop/WebGL capable devices */}
+
+
+
+
+
+
+ {/* Dark Ambient Overlays (Layer 1-5 + scroll fade) - Always rendered to keep copy legible */}
+
+
+
+
+
+
-
+
{/* Giant display title */}
- Every Second Counts
+ {(() => {
+ const text = "Every Second Counts";
+ const words = text.split(' ');
+ return words.map((word, wordIdx) => (
+
+ {word.split('').map((char, charIdx) => (
+
+ {char}
+
+ ))}
+ {wordIdx < words.length - 1 && (
+
+ )}
+
+ ));
+ })()}
{/* Subtitle */}
Smart emergency dispatch. Nearest hospital. Matched blood donors.
All in real time.
@@ -262,26 +399,25 @@ export default function PatientIntakeView() {
transition={{ delay: 1.8, duration: 0.8 }}
className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4"
>
-
-
-
-
+
Get Help Now
-
-
+
- 🩸 Register as Donor
-
+
+ Register as Donor
+
{/* Scroll hint */}
-
+
Scroll
@@ -292,8 +428,8 @@ export default function PatientIntakeView() {
{/* =============================================
SECTION 2: PROBLEM STATEMENT — Text reveals
============================================= */}
-
-
+
+
+
{/* Left: Descriptive text */}
-
+
- Emergency Console
+ Live Dispatch
- Lock. Dispatch. Save.
+ Lock. Dispatch. Save.
Pin your GPS coordinates, select the emergency type and blood group needed, then hit dispatch. We route the request to the closest matching hospital and alert nearby donors instantly.
@@ -348,248 +487,179 @@ export default function PatientIntakeView() {
Donor Alert
-
+
{/* Right: Intake Form Card */}
-
-
- {/* Top accent bar */}
-
-
- {/* Inner content */}
-
- {/* Header */}
-
-
-
- Live Dispatch
-
-
Emergency Dispatch
-
Secure your location and select emergency details.
-
-
- {/* Divider */}
-
-
- {/* Location Button */}
-
-
Patient Location
-
- {locating ? (
-
-
-
-
-
- Acquiring...
-
- ) : coords ? (
-
-
- Location Locked
-
- ) : (
-
-
-
-
-
- Pin Current Location
-
- )}
-
-
- {!coords && !locating && (
-
- { setCoords({ lat: 26.9124, lng: 75.7873 }); setLocationError(null); }}
- className="text-[11px] text-white/30 hover:text-goldenhour transition-colors font-medium underline cursor-pointer"
- >
- Or use demo location (Jaipur)
-
-
- )}
-
-
- {coords && (
-
- Coordinates Secured
- Lat: {coords.lat} · Lng: {coords.lng}
- setCoords(null)} className="text-[10px] text-emergency/70 hover:text-emergency underline font-bold mt-1.5 cursor-pointer">Clear
-
- )}
- {locationError && (
-
- {locationError}
-
- Retry
- |
- { setCoords({ lat: 26.9124, lng: 75.7873 }); setLocationError(null); }} className="text-xs text-success font-extrabold underline cursor-pointer">Demo Location
-
-
- )}
-
-
-
- {/* Selects */}
-
setEmergencyType(e.target.value)} options={typeOptions} />
- setBloodGroup(e.target.value)} options={bloodOptions} />
-
- {submitError && (
- {submitError}
- )}
+
+
+ {/* Top accent */}
+
+
+ {/* Header */}
+
+
Emergency Dispatch
+
Secure your location and select emergency details.
+
- {/* Dispatch Button */}
-
+ Patient Location
+
- {isSubmitting ? (
+ {coords ? (
-
-
-
-
- Processing...
+
+ Location Locked
) : (
-
-
+
+
+
- GET HELP NOW
+ Pin Current Location
)}
-
+
+
+ {!coords && !locating && (
+
+ { setCoords({ lat: 26.9124, lng: 75.7873 }); setLocationError(null); }}
+ className="text-[11px] text-dark-ink-muted hover:text-goldenhour transition-colors font-medium underline cursor-pointer"
+ >
+ Or use demo location (Jaipur)
+
+
+ )}
+
+
+ {coords && (
+
+ Coordinates Secured
+ Lat: {coords.lat} · Lng: {coords.lng}
+ setCoords(null)} className="text-[10px] text-emergency hover:text-emergency-pressed underline font-bold mt-1.5 cursor-pointer">Clear
+
+ )}
+ {locationError && (
+
+ {locationError}
+
+ Retry
+ |
+ { setCoords({ lat: 26.9124, lng: 75.7873 }); setLocationError(null); }} className="text-xs text-success font-extrabold underline cursor-pointer">Demo Location
+
+
+ )}
+
+
+ {/* Selects */}
+
setEmergencyType(e.target.value)} options={typeOptions} />
+ setBloodGroup(e.target.value)} options={bloodOptions} />
+
+ {submitError && (
+ {submitError}
+ )}
+
+ {/* Dispatch Button */}
+
+
+
+ GET HELP NOW
+
+
-
+
{/* =============================================
- SECTION 4: HOW IT WORKS — Horizontal scroll
+ SECTION 4: HOW IT WORKS — Cinematic scroll
============================================= */}
-
-
-
-
How It Works
-
Three steps. Zero delay.
-
-
-
-
- {/* Step 1 */}
-
-
-
📍
-
Step 01
-
Lock Location
-
- Your browser GPS pins your exact coordinates. High-accuracy mode ensures precision even in dense urban areas.
-
-
-
-
-
- {/* Step 2 */}
-
-
-
🏥
-
Step 02
-
Smart Dispatch
-
- Our algorithm matches your emergency type to the nearest hospital with the right department and available bed capacity.
-
-
-
-
-
- {/* Step 3 */}
-
-
-
🩸
-
Step 03
-
Donor Alert
-
- Every registered blood donor matching your blood type within range receives an instant alert with your location coordinates.
-
-
-
-
-
-
+
{/* =============================================
SECTION 5: STATS — Animated counters
============================================= */}
-
+
-
+
By The Numbers
- Built for speed. Designed for trust.
-
+
Built for speed. Designed for trust.
+
-
-
-
+
+
+
Average Response
From dispatch to hospital confirmation
-
+
-
-
- 24/7
+
+
+
Always On
Real-time websocket sync, polling fallback
-
+
-
-
+
+
Open Source
Transparent, auditable, community-driven
-
-
+
+
{/* =============================================
SECTION 6: CTA FOOTER
============================================= */}
-