Новый сервис pcp-tgu-ui-service: ТГУ-фронт на headless-auth nstart + платформенный CI/деплой

This commit is contained in:
Дмитрий Соловьев
2026-06-09 13:35:13 +03:00
parent 0b617594fd
commit 505bf1f31c
129 changed files with 21640 additions and 0 deletions
@@ -0,0 +1,127 @@
import { useRef } from "react";
import type { OrbitPassInfo } from "../tgu-map-2d/geometry/passes";
type MapPassSelectorProps = {
passes: OrbitPassInfo[];
fromIndex: number;
toIndex: number;
onFromChange: (idx: number) => void;
onToChange: (idx: number) => void;
};
function localHM(ms: number): string {
const d = new Date(ms);
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
}
function localDM(ms: number): string {
const d = new Date(ms);
return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}`;
}
export function MapPassSelector({ passes, fromIndex, toIndex, onFromChange, onToChange }: MapPassSelectorProps) {
const trackRef = useRef<HTMLDivElement>(null);
const n = passes.length;
if (n === 0) return null;
const fromPass = passes[Math.max(0, Math.min(fromIndex - 1, n - 1))];
const toPass = passes[Math.max(0, Math.min(toIndex - 1, n - 1))];
const multiDay = localDM(passes[0].startMs) !== localDM(passes[n - 1].startMs);
const isFullRange = fromIndex === 1 && toIndex === n;
const fromRev = fromPass.revolution ?? fromIndex;
const toRev = toPass.revolution ?? toIndex;
const pct = (idx: number): number => (n <= 1 ? 0 : ((idx - 1) / (n - 1)) * 100);
const fromPct = pct(fromIndex);
const toPct = pct(toIndex);
const indexAt = (clientX: number): number => {
const rect = trackRef.current?.getBoundingClientRect();
if (!rect || n <= 1) return 1;
return Math.round(Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)) * (n - 1)) + 1;
};
const makeHandlers = (isFrom: boolean) => ({
onPointerDown(e: React.PointerEvent<HTMLDivElement>) {
e.stopPropagation();
(e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
},
onPointerMove(e: React.PointerEvent<HTMLDivElement>) {
if (!(e.currentTarget as HTMLDivElement).hasPointerCapture(e.pointerId)) return;
const idx = indexAt(e.clientX);
if (isFrom) {
onFromChange(Math.max(1, Math.min(idx, toIndex)));
} else {
onToChange(Math.min(n, Math.max(idx, fromIndex)));
}
},
onPointerUp(e: React.PointerEvent<HTMLDivElement>) {
(e.currentTarget as HTMLDivElement).releasePointerCapture(e.pointerId);
},
});
const onTrackPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
const idx = indexAt(e.clientX);
if (Math.abs(idx - fromIndex) <= Math.abs(idx - toIndex)) {
onFromChange(Math.max(1, Math.min(idx, toIndex)));
} else {
onToChange(Math.min(n, Math.max(idx, fromIndex)));
}
};
return (
<div className="map-pass-selector">
<div className="map-pass-selector__header">
<span className="map-pass-selector__title">Витки</span>
<span className="map-pass-selector__nums">{fromRev}{toRev}</span>
<span className="map-pass-selector__times">
{multiDay ? `${localDM(fromPass.startMs)} ` : ""}{localHM(fromPass.startMs)}
{" "}
{multiDay ? `${localDM(toPass.startMs)} ` : ""}{localHM(toPass.startMs)}
</span>
{!isFullRange && (
<button
type="button"
className="map-pass-selector__reset"
title="Все витки"
onClick={() => { onFromChange(1); onToChange(n); }}
>
Все
</button>
)}
</div>
<div className="map-pass-selector__body" onPointerDown={onTrackPointerDown}>
{/* Track line */}
<div className="map-pass-selector__track" ref={trackRef}>
<div
className="map-pass-selector__fill"
style={{ left: `${fromPct}%`, width: `${toPct - fromPct}%` }}
/>
{/* Tick marks */}
{passes.map((p) => (
<div
key={p.index}
className={`map-pass-selector__tick${p.index >= fromIndex && p.index <= toIndex ? " is-active" : ""}`}
style={{ left: `${pct(p.index)}%` }}
/>
))}
{/* From handle */}
<div
className="map-pass-selector__thumb"
style={{ left: `${fromPct}%` }}
{...makeHandlers(true)}
/>
{/* To handle */}
<div
className="map-pass-selector__thumb"
style={{ left: `${toPct}%` }}
{...makeHandlers(false)}
/>
</div>
</div>
</div>
);
}