refactor
This commit is contained in:
1186
app/editor/page.tsx
1186
app/editor/page.tsx
File diff suppressed because it is too large
Load Diff
177
app/page.tsx
177
app/page.tsx
@@ -9,12 +9,16 @@ import { fetchGeometriesByBBox } from "@/api/geometries";
|
||||
import { ApiError } from "@/api/http";
|
||||
import { findEntityTypeOption } from "@/lib/entityTypeOptions";
|
||||
import {
|
||||
BACKGROUND_LAYER_OPTIONS,
|
||||
BackgroundLayerId,
|
||||
BackgroundLayerVisibility,
|
||||
DEFAULT_BACKGROUND_LAYER_VISIBILITY,
|
||||
HIDDEN_BACKGROUND_LAYER_VISIBILITY,
|
||||
} from "@/lib/backgroundLayers";
|
||||
import { Feature, FeatureCollection } from "@/lib/useEditorState";
|
||||
import {
|
||||
loadBackgroundLayerVisibilityFromStorage,
|
||||
persistBackgroundLayerVisibility,
|
||||
} from "@/lib/editor/background/backgroundVisibilityStorage";
|
||||
|
||||
const EMPTY_FC: FeatureCollection = { type: "FeatureCollection", features: [] };
|
||||
const WORLD_BBOX = {
|
||||
@@ -25,11 +29,10 @@ const WORLD_BBOX = {
|
||||
} as const;
|
||||
const CURRENT_YEAR = new Date().getUTCFullYear();
|
||||
const FALLBACK_TIMELINE_RANGE: TimelineRange = {
|
||||
min: CURRENT_YEAR - 5000,
|
||||
max: CURRENT_YEAR + 100,
|
||||
min: -2000,
|
||||
max: 2000,
|
||||
};
|
||||
const TIMELINE_DEBOUNCE_MS = 180;
|
||||
const BACKGROUND_LAYER_VISIBILITY_STORAGE_KEY = "uhm.backgroundLayerVisibility.v1";
|
||||
|
||||
type TimelineRange = {
|
||||
min: number;
|
||||
@@ -39,16 +42,12 @@ type TimelineRange = {
|
||||
export default function Page() {
|
||||
const [initialData, setInitialData] = useState<FeatureCollection>(EMPTY_FC);
|
||||
const [selectedFeatureId, setSelectedFeatureId] = useState<string | number | null>(null);
|
||||
const [timelineRange, setTimelineRange] = useState<TimelineRange>(FALLBACK_TIMELINE_RANGE);
|
||||
const [timelineWindowStart, setTimelineWindowStart] = useState<number>(FALLBACK_TIMELINE_RANGE.min);
|
||||
const [timelineWindowEnd, setTimelineWindowEnd] = useState<number>(FALLBACK_TIMELINE_RANGE.max);
|
||||
const [timelineYear, setTimelineYear] = useState<number>(() =>
|
||||
clampYearValue(CURRENT_YEAR, FALLBACK_TIMELINE_RANGE.min, FALLBACK_TIMELINE_RANGE.max)
|
||||
);
|
||||
const [timelineDraftYear, setTimelineDraftYear] = useState<number>(() =>
|
||||
clampYearValue(CURRENT_YEAR, FALLBACK_TIMELINE_RANGE.min, FALLBACK_TIMELINE_RANGE.max)
|
||||
);
|
||||
const [isTimelineReady, setIsTimelineReady] = useState(false);
|
||||
const [isTimelineLoading, setIsTimelineLoading] = useState(false);
|
||||
const [timelineStatus, setTimelineStatus] = useState<string | null>(null);
|
||||
const [backgroundVisibility, setBackgroundVisibility] = useState<BackgroundLayerVisibility>(
|
||||
@@ -75,53 +74,6 @@ export default function Page() {
|
||||
}, [initialData, selectedFeatureId]);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
|
||||
async function loadTimelineBounds() {
|
||||
setIsTimelineLoading(true);
|
||||
try {
|
||||
const data = await fetchGeometriesByBBox({
|
||||
...WORLD_BBOX,
|
||||
});
|
||||
if (disposed) return;
|
||||
|
||||
const range = deriveTimelineRange(data);
|
||||
const initialYear = clampYear(CURRENT_YEAR, range);
|
||||
|
||||
setTimelineRange(range);
|
||||
setTimelineWindowStart(range.min);
|
||||
setTimelineWindowEnd(range.max);
|
||||
setTimelineYear(initialYear);
|
||||
setTimelineDraftYear(initialYear);
|
||||
setTimelineStatus(null);
|
||||
} catch (err) {
|
||||
if (disposed) return;
|
||||
console.error("Load timeline bounds failed", err);
|
||||
const fallbackYear = clampYear(CURRENT_YEAR, FALLBACK_TIMELINE_RANGE);
|
||||
setTimelineRange(FALLBACK_TIMELINE_RANGE);
|
||||
setTimelineWindowStart(FALLBACK_TIMELINE_RANGE.min);
|
||||
setTimelineWindowEnd(FALLBACK_TIMELINE_RANGE.max);
|
||||
setTimelineYear(fallbackYear);
|
||||
setTimelineDraftYear(fallbackYear);
|
||||
setTimelineStatus("Không thể lấy phạm vi thời gian, đang dùng mốc mặc định.");
|
||||
} finally {
|
||||
if (!disposed) {
|
||||
setIsTimelineLoading(false);
|
||||
setIsTimelineReady(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadTimelineBounds();
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTimelineReady) return;
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
if (timelineDraftYear !== timelineYear) {
|
||||
setTimelineYear(timelineDraftYear);
|
||||
@@ -129,7 +81,7 @@ export default function Page() {
|
||||
}, TIMELINE_DEBOUNCE_MS);
|
||||
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [timelineDraftYear, timelineYear, isTimelineReady]);
|
||||
}, [timelineDraftYear, timelineYear]);
|
||||
|
||||
useEffect(() => {
|
||||
setBackgroundVisibility(loadBackgroundLayerVisibilityFromStorage());
|
||||
@@ -137,14 +89,6 @@ export default function Page() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const lower = Math.min(timelineWindowStart, timelineWindowEnd);
|
||||
const upper = Math.max(timelineWindowStart, timelineWindowEnd);
|
||||
setTimelineDraftYear((prev) => clampYearValue(prev, lower, upper));
|
||||
}, [timelineWindowStart, timelineWindowEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTimelineReady) return;
|
||||
|
||||
let disposed = false;
|
||||
const requestId = ++timelineFetchRequestRef.current;
|
||||
|
||||
@@ -182,7 +126,7 @@ export default function Page() {
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, [timelineYear, isTimelineReady]);
|
||||
}, [timelineYear]);
|
||||
|
||||
const updateBackgroundVisibility = (
|
||||
updater: (prev: BackgroundLayerVisibility) => BackgroundLayerVisibility
|
||||
@@ -194,7 +138,7 @@ export default function Page() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleBackgroundLayer = (id: (typeof BACKGROUND_LAYER_OPTIONS)[number]["id"]) => {
|
||||
const handleToggleBackgroundLayer = (id: BackgroundLayerId) => {
|
||||
updateBackgroundVisibility((prev) => ({
|
||||
...prev,
|
||||
[id]: !prev[id],
|
||||
@@ -209,23 +153,10 @@ export default function Page() {
|
||||
updateBackgroundVisibility(() => ({ ...HIDDEN_BACKGROUND_LAYER_VISIBILITY }));
|
||||
};
|
||||
|
||||
const handleTimelineWindowStartChange = (nextYear: number) => {
|
||||
const next = clampYearValue(Math.trunc(nextYear), timelineRange.min, timelineRange.max);
|
||||
setTimelineWindowStart(Math.min(next, timelineWindowEnd));
|
||||
};
|
||||
|
||||
const handleTimelineWindowEndChange = (nextYear: number) => {
|
||||
const next = clampYearValue(Math.trunc(nextYear), timelineRange.min, timelineRange.max);
|
||||
setTimelineWindowEnd(Math.max(next, timelineWindowStart));
|
||||
};
|
||||
|
||||
const handleTimelineYearChange = (nextYear: number) => {
|
||||
const lower = Math.min(timelineWindowStart, timelineWindowEnd);
|
||||
const upper = Math.max(timelineWindowStart, timelineWindowEnd);
|
||||
setTimelineDraftYear(clampYearValue(Math.trunc(nextYear), lower, upper));
|
||||
setTimelineDraftYear(clampYear(Math.trunc(nextYear), FALLBACK_TIMELINE_RANGE));
|
||||
};
|
||||
|
||||
const timelineDisabled = !isTimelineReady;
|
||||
const selectedType = resolveSelectedTypeSummary(selectedFeature);
|
||||
const selectedTimeSummary = formatSelectedTimeSummary(selectedFeature);
|
||||
const selectedEntitySummary = formatSelectedEntitySummary(selectedFeature);
|
||||
@@ -248,16 +179,10 @@ export default function Page() {
|
||||
<div style={{ width: "100%", height: "100%", background: "#0b1220" }} />
|
||||
)}
|
||||
<TimelineBar
|
||||
minYear={timelineRange.min}
|
||||
maxYear={timelineRange.max}
|
||||
windowStartYear={timelineWindowStart}
|
||||
windowEndYear={timelineWindowEnd}
|
||||
onWindowStartYearChange={handleTimelineWindowStartChange}
|
||||
onWindowEndYearChange={handleTimelineWindowEndChange}
|
||||
year={timelineDraftYear}
|
||||
onYearChange={handleTimelineYearChange}
|
||||
isLoading={isTimelineLoading}
|
||||
disabled={timelineDisabled}
|
||||
disabled={false}
|
||||
statusText={timelineStatus}
|
||||
/>
|
||||
</div>
|
||||
@@ -325,34 +250,6 @@ export default function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
function deriveTimelineRange(collection: FeatureCollection): TimelineRange {
|
||||
let min = Number.POSITIVE_INFINITY;
|
||||
let max = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const feature of collection.features) {
|
||||
const { time_start, time_end } = feature.properties;
|
||||
|
||||
if (isYearNumber(time_start)) {
|
||||
min = Math.min(min, time_start);
|
||||
max = Math.max(max, time_start);
|
||||
}
|
||||
|
||||
if (isYearNumber(time_end)) {
|
||||
min = Math.min(min, time_end);
|
||||
max = Math.max(max, time_end);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
||||
return FALLBACK_TIMELINE_RANGE;
|
||||
}
|
||||
|
||||
return {
|
||||
min: Math.floor(min),
|
||||
max: Math.ceil(max),
|
||||
};
|
||||
}
|
||||
|
||||
function clampYear(year: number, range: TimelineRange): number {
|
||||
return clampYearValue(year, range.min, range.max);
|
||||
}
|
||||
@@ -432,53 +329,3 @@ function normalizeTypeId(value: unknown): string | null {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized.length ? normalized : null;
|
||||
}
|
||||
|
||||
function loadBackgroundLayerVisibilityFromStorage(): BackgroundLayerVisibility {
|
||||
if (typeof window === "undefined") {
|
||||
return { ...DEFAULT_BACKGROUND_LAYER_VISIBILITY };
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem(BACKGROUND_LAYER_VISIBILITY_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return { ...DEFAULT_BACKGROUND_LAYER_VISIBILITY };
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
const normalized = normalizeBackgroundLayerVisibility(parsed);
|
||||
return normalized || { ...DEFAULT_BACKGROUND_LAYER_VISIBILITY };
|
||||
} catch (err) {
|
||||
console.warn("Load background layer visibility from storage failed", err);
|
||||
return { ...DEFAULT_BACKGROUND_LAYER_VISIBILITY };
|
||||
}
|
||||
}
|
||||
|
||||
function persistBackgroundLayerVisibility(visibility: BackgroundLayerVisibility) {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
BACKGROUND_LAYER_VISIBILITY_STORAGE_KEY,
|
||||
JSON.stringify(visibility)
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn("Persist background layer visibility failed", err);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBackgroundLayerVisibility(raw: unknown): BackgroundLayerVisibility | null {
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
|
||||
const source = raw as Record<string, unknown>;
|
||||
const next: BackgroundLayerVisibility = {
|
||||
...DEFAULT_BACKGROUND_LAYER_VISIBILITY,
|
||||
};
|
||||
|
||||
for (const layer of BACKGROUND_LAYER_OPTIONS) {
|
||||
const value = source[layer.id];
|
||||
if (typeof value === "boolean") {
|
||||
next[layer.id] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user