270 lines
9.1 KiB
TypeScript
270 lines
9.1 KiB
TypeScript
"use client";
|
|
|
|
type Props = {
|
|
minYear: number;
|
|
maxYear: number;
|
|
windowStartYear: number;
|
|
windowEndYear: number;
|
|
onWindowStartYearChange: (year: number) => void;
|
|
onWindowEndYearChange: (year: number) => void;
|
|
year: number;
|
|
onYearChange: (year: number) => void;
|
|
isLoading: boolean;
|
|
disabled: boolean;
|
|
statusText?: string | null;
|
|
};
|
|
|
|
export default function TimelineBar({
|
|
minYear,
|
|
maxYear,
|
|
windowStartYear,
|
|
windowEndYear,
|
|
onWindowStartYearChange,
|
|
onWindowEndYearChange,
|
|
year,
|
|
onYearChange,
|
|
isLoading,
|
|
disabled,
|
|
statusText,
|
|
}: Props) {
|
|
const lower = Math.min(minYear, maxYear);
|
|
const upper = Math.max(minYear, maxYear);
|
|
const globalLocked = lower === upper;
|
|
const effectiveDisabled = disabled || globalLocked;
|
|
const safeWindowStart = clampYear(windowStartYear, lower, upper);
|
|
const safeWindowEnd = clampYear(windowEndYear, safeWindowStart, upper);
|
|
const windowLocked = safeWindowStart === safeWindowEnd;
|
|
const safeYear = clampYear(year, safeWindowStart, safeWindowEnd);
|
|
const pointDisabled = effectiveDisabled || windowLocked;
|
|
const windowStartPercent = toPercent(safeWindowStart, lower, upper);
|
|
const windowEndPercent = toPercent(safeWindowEnd, lower, upper);
|
|
|
|
const helperText = isLoading
|
|
? "Đang tải geometry theo mốc thời gian..."
|
|
: statusText || (windowLocked ? "Khoảng lớn đang thu về một mốc duy nhất." : "Kéo mốc nhỏ để query trong khoảng lớn.");
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
left: "18px",
|
|
right: "18px",
|
|
bottom: "16px",
|
|
zIndex: 10,
|
|
background: "rgba(15, 23, 42, 0.9)",
|
|
border: "1px solid rgba(148, 163, 184, 0.3)",
|
|
borderRadius: "10px",
|
|
padding: "12px 14px",
|
|
color: "#e2e8f0",
|
|
backdropFilter: "blur(2px)",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
marginBottom: "8px",
|
|
gap: "8px",
|
|
}}
|
|
>
|
|
<span style={{ fontSize: "13px", fontWeight: 600, letterSpacing: "0.02em" }}>
|
|
Timeline
|
|
</span>
|
|
<span style={{ fontSize: "16px", fontWeight: 700, color: "#f8fafc" }}>
|
|
{formatYear(safeYear)}
|
|
</span>
|
|
</div>
|
|
|
|
<div style={{ fontSize: "12px", color: "#cbd5e1", marginBottom: "6px" }}>
|
|
Khoảng thời gian lớn
|
|
</div>
|
|
<div
|
|
className="dual-range"
|
|
style={{
|
|
opacity: effectiveDisabled ? 0.6 : 1,
|
|
}}
|
|
>
|
|
<div className="dual-range-track" />
|
|
<div
|
|
className="dual-range-selected"
|
|
style={{
|
|
left: `${windowStartPercent}%`,
|
|
width: `${Math.max(windowEndPercent - windowStartPercent, 0)}%`,
|
|
}}
|
|
/>
|
|
<input
|
|
className="dual-range-input"
|
|
type="range"
|
|
min={lower}
|
|
max={upper}
|
|
step={1}
|
|
value={safeWindowStart}
|
|
onChange={(event) =>
|
|
onWindowStartYearChange(
|
|
Math.min(Number(event.target.value), safeWindowEnd)
|
|
)
|
|
}
|
|
disabled={effectiveDisabled}
|
|
aria-label="Timeline window start"
|
|
/>
|
|
<input
|
|
className="dual-range-input"
|
|
type="range"
|
|
min={lower}
|
|
max={upper}
|
|
step={1}
|
|
value={safeWindowEnd}
|
|
onChange={(event) =>
|
|
onWindowEndYearChange(
|
|
Math.max(Number(event.target.value), safeWindowStart)
|
|
)
|
|
}
|
|
disabled={effectiveDisabled}
|
|
aria-label="Timeline window end"
|
|
/>
|
|
</div>
|
|
<div
|
|
style={{
|
|
marginTop: "6px",
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
fontSize: "12px",
|
|
color: "#94a3b8",
|
|
}}
|
|
>
|
|
<span>{formatYear(safeWindowStart)}</span>
|
|
<span>{formatYear(safeWindowEnd)}</span>
|
|
</div>
|
|
|
|
<div style={{ fontSize: "12px", color: "#cbd5e1", marginTop: "8px", marginBottom: "6px" }}>
|
|
Mốc thời gian chi tiết
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min={safeWindowStart}
|
|
max={safeWindowEnd}
|
|
step={1}
|
|
value={safeYear}
|
|
onChange={(event) => onYearChange(Number(event.target.value))}
|
|
disabled={pointDisabled}
|
|
aria-label="Timeline year"
|
|
style={{
|
|
width: "100%",
|
|
accentColor: "#22c55e",
|
|
cursor: pointDisabled ? "not-allowed" : "pointer",
|
|
opacity: pointDisabled ? 0.6 : 1,
|
|
}}
|
|
/>
|
|
|
|
<div
|
|
style={{
|
|
marginTop: "8px",
|
|
display: "grid",
|
|
gridTemplateColumns: "1fr auto 1fr",
|
|
alignItems: "center",
|
|
columnGap: "10px",
|
|
fontSize: "12px",
|
|
}}
|
|
>
|
|
<span style={{ color: "#94a3b8" }}>{formatYear(safeWindowStart)}</span>
|
|
<span style={{ color: "#cbd5e1", textAlign: "center", whiteSpace: "nowrap" }}>
|
|
{helperText}
|
|
</span>
|
|
<span style={{ color: "#94a3b8", textAlign: "right" }}>{formatYear(safeWindowEnd)}</span>
|
|
</div>
|
|
|
|
<style jsx>{`
|
|
.dual-range {
|
|
position: relative;
|
|
height: 22px;
|
|
}
|
|
|
|
.dual-range-track {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
height: 4px;
|
|
border-radius: 999px;
|
|
background: rgba(148, 163, 184, 0.35);
|
|
}
|
|
|
|
.dual-range-selected {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
height: 4px;
|
|
border-radius: 999px;
|
|
background: #22c55e;
|
|
}
|
|
|
|
.dual-range-input {
|
|
pointer-events: none;
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 22px;
|
|
margin: 0;
|
|
background: transparent;
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
}
|
|
|
|
.dual-range-input::-webkit-slider-runnable-track {
|
|
height: 4px;
|
|
background: transparent;
|
|
}
|
|
|
|
.dual-range-input::-webkit-slider-thumb {
|
|
pointer-events: auto;
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 14px;
|
|
height: 14px;
|
|
margin-top: -5px;
|
|
border-radius: 999px;
|
|
border: 2px solid #0f172a;
|
|
background: #22c55e;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.dual-range-input::-moz-range-track {
|
|
height: 4px;
|
|
background: transparent;
|
|
}
|
|
|
|
.dual-range-input::-moz-range-thumb {
|
|
pointer-events: auto;
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 999px;
|
|
border: 2px solid #0f172a;
|
|
background: #22c55e;
|
|
cursor: pointer;
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function clampYear(year: number, minYear: number, maxYear: number): number {
|
|
if (year < minYear) return minYear;
|
|
if (year > maxYear) return maxYear;
|
|
return year;
|
|
}
|
|
|
|
function formatYear(year: number): string {
|
|
if (year < 0) {
|
|
return `${Math.abs(year)} TCN`;
|
|
}
|
|
return `${year}`;
|
|
}
|
|
|
|
function toPercent(value: number, minValue: number, maxValue: number): number {
|
|
if (maxValue <= minValue) return 0;
|
|
return ((value - minValue) / (maxValue - minValue)) * 100;
|
|
}
|