Files
microdao-daarion/src/features/space-dashboard/components/StarSystemView.tsx
Apple 6bd769ef40 feat(city-map): Add 2D City Map with coordinates and agent presence
- Add migration 013_city_map_coordinates.sql with map coordinates, zones, and agents table
- Add /city/map API endpoint in city-service
- Add /city/agents and /city/agents/online endpoints
- Extend presence aggregator to include agents[] in snapshot
- Add AgentsSource for fetching agent data from DB
- Create CityMap component with interactive room tiles
- Add useCityMap hook for fetching map data
- Update useGlobalPresence to include agents
- Add map/list view toggle on /city page
- Add agent badges to room cards and map tiles
2025-11-27 07:00:47 -08:00

221 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState } from 'react';
import type { SpaceScene, StarObject, PlanetObject, SpaceObjectBase } from '../types/space';
import { CosmosView } from './CosmosView';
interface StarSystemViewProps {
scene: SpaceScene;
onSelect?: (entity: SpaceObjectBase) => void;
}
export function StarSystemView({ scene, onSelect }: StarSystemViewProps) {
const [selectedStarId, setSelectedStarId] = useState<string | null>(null);
const [selectedPlanetId, setSelectedPlanetId] = useState<string | null>(null);
const stars = scene.stars ?? [];
useEffect(() => {
if (!selectedStarId && stars.length > 0) {
setSelectedStarId(stars[0].id);
}
}, [stars, selectedStarId]);
const selectedStar: StarObject | null = useMemo(
() => stars.find((star) => star.id === selectedStarId) ?? stars[0] ?? null,
[stars, selectedStarId],
);
const systemPlanets: PlanetObject[] = useMemo(() => {
if (!selectedStar) return [];
return (scene.planets ?? []).filter((planet) => planet.starId === selectedStar.id);
}, [scene.planets, selectedStar]);
useEffect(() => {
if (systemPlanets.length === 0) {
setSelectedPlanetId(null);
return;
}
if (!selectedPlanetId || !systemPlanets.some((p) => p.id === selectedPlanetId)) {
setSelectedPlanetId(systemPlanets[0].id);
}
}, [systemPlanets, selectedPlanetId]);
const selectedPlanet = useMemo(
() => systemPlanets.find((planet) => planet.id === selectedPlanetId) ?? null,
[systemPlanets, selectedPlanetId],
);
const planetMoons = useMemo(
() =>
(scene.moons ?? []).filter((moon) =>
selectedPlanet ? moon.planetId === selectedPlanet.id : systemPlanets.some((p) => p.id === moon.planetId),
),
[scene.moons, selectedPlanet, systemPlanets],
);
const filteredScene = useMemo<SpaceScene>(
() => ({
clusters: [],
stars: selectedStar ? [selectedStar] : [],
planets: systemPlanets,
moons: planetMoons,
gateways: scene.gateways ?? [],
anomalies: scene.anomalies ?? [],
}),
[selectedStar, systemPlanets, planetMoons, scene.gateways, scene.anomalies],
);
const systemMetrics = useMemo(() => {
if (!selectedStar) {
return {
health: 0,
microDaos: 0,
agents: 0,
alerts: 0,
};
}
const alerts =
scene.anomalies?.filter((anomaly) => {
// поки немає прив'язки до конкретної ноди, показуємо всі
return anomaly.status !== 'stable';
}).length ?? 0;
return {
health: selectedStar.health,
microDaos: systemPlanets.length,
agents: systemPlanets.reduce((acc, planet) => acc + planet.agents, 0),
alerts,
};
}, [selectedStar, systemPlanets, scene.anomalies]);
const starButtonClass = (star: StarObject) =>
`rounded-full border px-4 py-2 text-sm font-semibold transition-colors ${
selectedStar && selectedStar.id === star.id
? 'border-indigo-400 bg-indigo-500/20 text-white'
: 'border-white/20 text-white/70 hover:border-white/40'
}`;
const planetButtonClass = (planet: PlanetObject) =>
`flex items-center justify-between rounded-2xl border px-4 py-3 text-left transition ${
selectedPlanet && selectedPlanet.id === planet.id
? 'border-cyan-400/60 bg-cyan-500/10 text-white'
: 'border-white/10 bg-white/5 text-white/80 hover:border-white/30'
}`;
return (
<div className="rounded-3xl border border-white/10 bg-slate-950/60 p-6 shadow-[0_15px_90px_rgba(15,23,42,0.8)]">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<p className="text-sm uppercase text-indigo-400/70">Режим</p>
<h3 className="text-2xl font-semibold text-white">Star System View</h3>
{selectedStar && (
<p className="mt-1 text-sm text-indigo-100/70">
Центральна нода: <span className="font-semibold">{selectedStar.name}</span>
</p>
)}
</div>
<div className="flex gap-3">
{[
{ label: 'Health', value: `${Math.round(systemMetrics.health)}%` },
{ label: 'microDAO', value: systemMetrics.microDaos },
{ label: 'Агенти', value: systemMetrics.agents },
{ label: 'Alerts', value: systemMetrics.alerts },
].map((metric) => (
<div
key={metric.label}
className="rounded-2xl border border-white/10 bg-white/5 px-4 py-2 text-center text-sm text-white backdrop-blur"
>
<p className="text-xs uppercase tracking-widest text-white/50">{metric.label}</p>
<p className="text-lg font-semibold text-white">{metric.value}</p>
</div>
))}
</div>
</div>
<div className="mt-4 grid gap-6 lg:grid-cols-[3fr_2fr]">
<div>
<div className="flex flex-wrap gap-2">
{stars.map((star) => (
<button
key={star.id}
className={starButtonClass(star)}
onClick={() => {
setSelectedStarId(star.id);
onSelect?.(star);
}}
>
{star.name}
</button>
))}
</div>
<div className="mt-4">
<CosmosView
scene={filteredScene}
onSelect={(entity) => {
setSelectedPlanetId(entity.type === 'planet' ? entity.id : selectedPlanetId);
onSelect?.(entity);
}}
/>
</div>
</div>
<div className="rounded-2xl border border-white/10 bg-slate-900/60 p-4 shadow-inner shadow-black/30">
<h4 className="text-sm uppercase tracking-[0.3em] text-white/60">Орбіти microDAO</h4>
<div className="mt-3 space-y-3">
{systemPlanets.length === 0 && (
<p className="text-sm text-white/50">Для цієї ноди ще не підєднано microDAO.</p>
)}
{systemPlanets.map((planet) => (
<button
key={planet.id}
className={planetButtonClass(planet)}
onClick={() => {
setSelectedPlanetId(planet.id);
onSelect?.(planet);
}}
>
<div>
<p className="text-base font-semibold">{planet.name}</p>
<p className="text-xs text-white/60">
Населення {planet.population} · Агенти {planet.agents}
</p>
</div>
<span className="text-sm text-white/70">
Орбіта {planet.orbitRadius}
</span>
</button>
))}
</div>
{selectedPlanet && (
<div className="mt-5 rounded-2xl border border-white/5 bg-slate-950/60 p-4">
<p className="text-sm uppercase tracking-[0.3em] text-white/50">Супутники / агенти</p>
{planetMoons.length === 0 ? (
<p className="mt-2 text-sm text-white/60">Немає активних агентів на цій орбіті.</p>
) : (
<div className="mt-3 space-y-2">
{planetMoons.map((moon) => (
<button
key={moon.id}
className="w-full rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-left text-sm text-white/80 hover:border-white/30"
onClick={() => onSelect?.(moon)}
>
<div className="font-semibold text-white">{moon.name}</div>
<div className="text-xs text-white/60">{moon.focus}</div>
</button>
))}
</div>
)}
</div>
)}
</div>
</div>
</div>
);
}