{t('explorer.engine')}
+{t('explorer.title')}
+{place.city}
+{place.name}
++ {t('explorer.coordinates')}: 37.5665° N, 126.9780° E +
+diff --git a/.bkit/state/memory.json b/.bkit/state/memory.json
index 7bd0942..ab6bf6f 100644
--- a/.bkit/state/memory.json
+++ b/.bkit/state/memory.json
@@ -1,12 +1,12 @@
{
"version": "1.0",
- "lastUpdated": "2026-04-04T06:57:03.197Z",
+ "lastUpdated": "2026-04-05T23:22:41.783Z",
"platform": "gemini",
"data": {
"session": {
- "sessionCount": 2,
- "lastSessionStarted": "2026-04-04T06:57:03.197Z",
- "lastSessionEnded": "2026-04-04T06:57:02.768Z"
+ "sessionCount": 3,
+ "lastSessionStarted": "2026-04-05T23:18:25.147Z",
+ "lastSessionEnded": "2026-04-05T23:22:41.783Z"
},
"pdca": {
"lastFeature": null,
diff --git a/.bkit/state/pdca-status.json b/.bkit/state/pdca-status.json
index f619962..c63a14d 100644
--- a/.bkit/state/pdca-status.json
+++ b/.bkit/state/pdca-status.json
@@ -1,6 +1,6 @@
{
"version": "3.0",
- "lastUpdated": "2026-04-04T06:57:02.805Z",
+ "lastUpdated": "2026-04-05T23:22:41.783Z",
"activeFeatures": {},
"primaryFeature": null,
"archivedFeatures": {},
@@ -12,7 +12,7 @@
"session": {
"startedAt": "2026-04-04T06:56:16.748Z",
"onboardingCompleted": false,
- "lastActivity": "2026-04-04T06:57:02.767Z"
+ "lastActivity": "2026-04-05T23:22:41.782Z"
},
"history": [
{
@@ -20,6 +20,18 @@
"action": "session_end",
"feature": null,
"details": "Session ended"
+ },
+ {
+ "timestamp": "2026-04-05T23:22:41.751Z",
+ "action": "session_end",
+ "feature": null,
+ "details": "Session ended"
+ },
+ {
+ "timestamp": "2026-04-05T23:22:41.783Z",
+ "action": "session_end",
+ "feature": null,
+ "details": "Session ended"
}
]
}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 64ce93f..6850046 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+.bkit
\ No newline at end of file
diff --git a/bun.lock b/bun.lock
index 700ee7e..e1d5e7a 100644
--- a/bun.lock
+++ b/bun.lock
@@ -13,23 +13,25 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.38.0",
+ "lucide-react": "^1.7.0",
"next": "16.2.2",
"react": "19.2.4",
"react-dom": "19.2.4",
+ "tailwind-merge": "^3.5.0",
"three": "^0.183.2",
"zod": "^4.3.6",
"zustand": "^5.0.12",
},
"devDependencies": {
- "@tailwindcss/postcss": "^4",
- "@types/node": "^20",
- "@types/react": "^19",
- "@types/react-dom": "^19",
+ "@tailwindcss/postcss": "^4.2.2",
+ "@types/node": "^20.19.39",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
"cpx2": "^8.0.1",
- "eslint": "^9",
+ "eslint": "^9.39.4",
"eslint-config-next": "16.2.2",
- "tailwindcss": "^4",
- "typescript": "^5",
+ "tailwindcss": "^4.2.2",
+ "typescript": "^5.9.3",
},
},
},
@@ -794,6 +796,8 @@
"lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
+ "lucide-react": ["lucide-react@1.7.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg=="],
+
"maath": ["maath@0.10.8", "", { "peerDependencies": { "@types/three": ">=0.134.0", "three": ">=0.134.0" } }, "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
@@ -992,6 +996,8 @@
"suspend-react": ["suspend-react@0.1.3", "", { "peerDependencies": { "react": ">=17.0" } }, "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ=="],
+ "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
+
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
diff --git a/package.json b/package.json
index f9b8888..efaef85 100644
--- a/package.json
+++ b/package.json
@@ -18,9 +18,11 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.38.0",
+ "lucide-react": "^1.7.0",
"next": "16.2.2",
"react": "19.2.4",
"react-dom": "19.2.4",
+ "tailwind-merge": "^3.5.0",
"three": "^0.183.2",
"zod": "^4.3.6",
"zustand": "^5.0.12"
diff --git a/src/app/api/live/[slug]/places/route.ts b/src/app/api/live/[slug]/places/route.ts
index 17a26a4..95c2d2e 100644
--- a/src/app/api/live/[slug]/places/route.ts
+++ b/src/app/api/live/[slug]/places/route.ts
@@ -3,6 +3,7 @@ import { createStaticSceneBootstrap } from "../../../../../shared/scene";
import { MVP_PLACES } from "../../../../../data/places";
import { toLiveStateCacheKey } from "../../../../../shared/cache";
import { validateLivePlaceSnapshot } from "../../../../../shared/contracts";
+import { normalizeLiveLevel } from "../../../../../shared/selectors";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
@@ -26,8 +27,7 @@ export async function GET(
}
const bootstrap = createStaticSceneBootstrap(place);
- const raw = request.nextUrl.searchParams.get("density") ?? "medium";
- const density = raw === "low" || raw === "high" ? raw : "medium";
+ const density = normalizeLiveLevel(request.nextUrl.searchParams.get("density"));
const snapshot = validateLivePlaceSnapshot({
geometryId: bootstrap.geometryId,
diff --git a/src/app/api/live/[slug]/traffic/route.ts b/src/app/api/live/[slug]/traffic/route.ts
index 7c63ebf..ab3a848 100644
--- a/src/app/api/live/[slug]/traffic/route.ts
+++ b/src/app/api/live/[slug]/traffic/route.ts
@@ -3,6 +3,7 @@ import { createStaticSceneBootstrap } from "../../../../../shared/scene";
import { MVP_PLACES } from "../../../../../data/places";
import { toLiveStateCacheKey } from "../../../../../shared/cache";
import { validateLiveTrafficSnapshot } from "../../../../../shared/contracts";
+import { normalizeHour, toTrafficPolicy } from "../../../../../shared/domains";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
@@ -27,14 +28,14 @@ export async function GET(
const bootstrap = createStaticSceneBootstrap(place);
const hour = Number(request.nextUrl.searchParams.get("hour") ?? "12");
- const normalizedHour = Number.isFinite(hour) ? ((hour % 24) + 24) % 24 : 12;
- const rushHour = (normalizedHour >= 8 && normalizedHour <= 10) || (normalizedHour >= 17 && normalizedHour <= 20);
+ const normalizedHour = normalizeHour(hour);
+ const trafficPolicy = toTrafficPolicy(normalizedHour);
const payload = validateLiveTrafficSnapshot({
geometryId: bootstrap.geometryId,
key: toLiveStateCacheKey(bootstrap.geometryId, "traffic"),
- density: rushHour ? "high" : "medium",
- speedKph: rushHour ? 18 : 32,
+ density: trafficPolicy.density,
+ speedKph: trafficPolicy.speedKph,
capturedAtIso: new Date().toISOString(),
});
diff --git a/src/app/api/live/[slug]/weather/route.ts b/src/app/api/live/[slug]/weather/route.ts
index 54b1cc3..58d6e57 100644
--- a/src/app/api/live/[slug]/weather/route.ts
+++ b/src/app/api/live/[slug]/weather/route.ts
@@ -3,6 +3,7 @@ import { createStaticSceneBootstrap } from "../../../../../shared/scene";
import { MVP_PLACES } from "../../../../../data/places";
import { toLiveStateCacheKey } from "../../../../../shared/cache";
import { validateLiveWeatherSnapshot } from "../../../../../shared/contracts";
+import { buildWeatherPolicy, normalizeHour } from "../../../../../shared/domains";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
@@ -27,20 +28,14 @@ export async function GET(
const bootstrap = createStaticSceneBootstrap(place);
const hour = Number(request.nextUrl.searchParams.get("hour") ?? "12");
- const normalizedHour = Number.isFinite(hour) ? ((hour % 24) + 24) % 24 : 12;
-
- const condition =
- normalizedHour >= 6 && normalizedHour < 17
- ? "clear"
- : normalizedHour >= 17 && normalizedHour < 21
- ? "cloudy"
- : "rain";
+ const normalizedHour = normalizeHour(hour);
+ const weatherPolicy = buildWeatherPolicy(normalizedHour);
const snapshot = validateLiveWeatherSnapshot({
geometryId: bootstrap.geometryId,
key: toLiveStateCacheKey(bootstrap.geometryId, "weather"),
- condition,
- temperatureCelsius: condition === "rain" ? 10 : 18,
+ condition: weatherPolicy.condition,
+ temperatureCelsius: weatherPolicy.temperatureCelsius,
capturedAtIso: new Date().toISOString(),
});
diff --git a/src/app/explorer/page.tsx b/src/app/explorer/page.tsx
new file mode 100644
index 0000000..76d5729
--- /dev/null
+++ b/src/app/explorer/page.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import GlobeScene from "@/src/globe/GlobeScene";
+import { MVP_PLACES } from "@/src/data/places";
+import type { Place } from "@/src/types/place";
+import { Globe, MapPin, Search, ArrowLeft, Map } from "lucide-react";
+import Link from "next/link";
+import { useTranslation } from "@/src/stores/useI18nStore";
+
+export default function ExplorerHome() {
+ const { t } = useTranslation();
+
+ return (
+ {t('explorer.engine')} {place.city}
+ {t('explorer.coordinates')}: 37.5665° N, 126.9780° E
+ {t('explorer.title')}
+ {place.name}
+