gc-wing/apps/web/src/widgets/vesselList/VesselList.tsx

60 lines
2.2 KiB
TypeScript
Raw Normal View 히스토리

2026-02-15 11:22:38 +09:00
import { VESSEL_TYPES } from "../../entities/vessel/model/meta";
import type { DerivedLegacyVessel } from "../../features/legacyDashboard/model/types";
type Props = {
vessels: DerivedLegacyVessel[];
selectedMmsi: number | null;
onSelectMmsi: (mmsi: number) => void;
};
function isFiniteNumber(x: unknown): x is number {
return typeof x === "number" && Number.isFinite(x);
}
export function VesselList({ vessels, selectedMmsi, onSelectMmsi }: Props) {
const sorted = vessels
.slice()
.sort((a, b) => (isFiniteNumber(b.sog) ? b.sog : -1) - (isFiniteNumber(a.sog) ? a.sog : -1))
.slice(0, 80);
return (
<div className="vlist">
{sorted.map((v) => {
const meta = VESSEL_TYPES[v.shipCode];
const primarySegs = meta.speedProfile.filter((s) => s.primary);
const inRange =
v.sog !== null && primarySegs.length ? primarySegs.some((s) => v.sog! >= s.range[0] && v.sog! <= s.range[1]) : false;
const sc = v.state.isFishing ? "#22C55E" : (v.sog ?? 0) > 3 ? "#3B82F6" : "#64748B";
const speedColor = inRange ? "#22C55E" : (v.sog ?? 0) > 5 ? "#3B82F6" : "var(--muted)";
const hasPair = v.pairPermitNo ? "⛓" : "";
const sel = selectedMmsi === v.mmsi;
return (
<div
key={v.mmsi}
className="vi"
onClick={() => onSelectMmsi(v.mmsi)}
style={sel ? { background: "rgba(59,130,246,.12)", border: "1px solid rgba(59,130,246,.45)" } : undefined}
title={v.name}
>
<div className="dot" style={{ background: meta.color, boxShadow: v.state.isFishing ? `0 0 3px ${meta.color}` : undefined }} />
<div className="nm">
{hasPair}
{v.permitNo}
</div>
<div className="sp" style={{ color: speedColor }}>
{v.sog !== null ? v.sog.toFixed(1) : "?"}kt
</div>
<div className="st" style={{ background: `${sc}22`, color: sc }}>
{v.state.label}
</div>
</div>
);
})}
{sorted.length === 0 ? <div style={{ fontSize: 11, color: "var(--muted)" }}>( )</div> : null}
</div>
);
}