draw path | draw area | localstorage layer state | add entities property parallel | timeline bar
This commit is contained in:
269
components/TimelineBar.tsx
Normal file
269
components/TimelineBar.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
"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;
|
||||
}
|
||||
Reference in New Issue
Block a user