From de91f8129ec4ca7b765fde5245f313190b10aac5 Mon Sep 17 00:00:00 2001 From: taDuc Date: Mon, 25 May 2026 08:19:53 +0700 Subject: [PATCH] refactor: simplify timeline stage creation form by removing redundant validation logic and improving layout --- src/app/editor/[id]/page.tsx | 1 + .../editor/ReplayEffectsSidebar.tsx | 24 +- .../editor/ReplayTimelineSidebar.tsx | 556 ++++++++---------- src/uhm/doc/commit_snapshot.ts | 1 + src/uhm/lib/editor/snapshot/editorSnapshot.ts | 12 +- src/uhm/types/geo.ts | 1 + 6 files changed, 266 insertions(+), 329 deletions(-) diff --git a/src/app/editor/[id]/page.tsx b/src/app/editor/[id]/page.tsx index a9aed89..e3d6ae1 100644 --- a/src/app/editor/[id]/page.tsx +++ b/src/app/editor/[id]/page.tsx @@ -1801,6 +1801,7 @@ function EditorPageContent() { type: "Feature", properties: { id: geoId, + source: "ref", type: typeKey, time_start: normalizeTimelineYearValue(geo.time_start), time_end: normalizeTimelineYearValue(geo.time_end), diff --git a/src/uhm/components/editor/ReplayEffectsSidebar.tsx b/src/uhm/components/editor/ReplayEffectsSidebar.tsx index 89b2695..7f6155d 100644 --- a/src/uhm/components/editor/ReplayEffectsSidebar.tsx +++ b/src/uhm/components/editor/ReplayEffectsSidebar.tsx @@ -39,15 +39,15 @@ type ActionFieldConfig = { name: string; label: string; kind: - | "text" - | "textarea" - | "number" - | "boolean" - | "color" - | "select" - | "geometry" - | "geometry-multi" - | "wiki"; + | "text" + | "textarea" + | "number" + | "boolean" + | "color" + | "select" + | "geometry" + | "geometry-multi" + | "wiki"; placeholder?: string; options?: Array<{ label: string; value: string }>; visibleWhen?: (values: ActionFormValues) => boolean; @@ -207,9 +207,9 @@ export default function ReplayEffectsSidebar({ null; const selectedStep = selectedStage && - selectedStepIndex != null && - selectedStepIndex >= 0 && - selectedStepIndex < selectedStage.steps.length + selectedStepIndex != null && + selectedStepIndex >= 0 && + selectedStepIndex < selectedStage.steps.length ? selectedStage.steps[selectedStepIndex] : null; const mapCameraActions = useMemo( diff --git a/src/uhm/components/editor/ReplayTimelineSidebar.tsx b/src/uhm/components/editor/ReplayTimelineSidebar.tsx index 8704093..065f62c 100644 --- a/src/uhm/components/editor/ReplayTimelineSidebar.tsx +++ b/src/uhm/components/editor/ReplayTimelineSidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import type { BattleReplay, GeoFunctionName, @@ -43,46 +43,6 @@ type AnyStepAction = | ReplayAction | ReplayAction; -function validateReplayTimeFormat(value: string): boolean { - const trimmed = value.trim(); - if (!trimmed) return false; - - // 1. Check DD/MM/YYYY - const dmyRegex = /^(\d{2})\/(\d{2})\/(-?\d{4})$/; - const dmyMatch = trimmed.match(dmyRegex); - if (dmyMatch) { - const day = parseInt(dmyMatch[1], 10); - const month = parseInt(dmyMatch[2], 10); - const year = parseInt(dmyMatch[3], 10); - if (month < 1 || month > 12) return false; - if (day < 1 || day > 31) return false; - if (month === 2) { - const isLeap = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); - if (day > (isLeap ? 29 : 28)) return false; - } else if ([4, 6, 9, 11].includes(month)) { - if (day > 30) return false; - } - return true; - } - - // 2. Check MM/YYYY - const myRegex = /^(\d{2})\/(-?\d{4})$/; - const myMatch = trimmed.match(myRegex); - if (myMatch) { - const month = parseInt(myMatch[1], 10); - if (month < 1 || month > 12) return false; - return true; - } - - // 3. Check YYYY - const yRegex = /^(-?\d{4})$/; - if (yRegex.test(trimmed)) { - return true; - } - - return false; -} - type StageFormState = { title: string; detail_time_start: string; @@ -143,13 +103,6 @@ export default function ReplayTimelineSidebar({ detail_time_stop: "", }); const [createStagePanelKey, setCreateStagePanelKey] = useState(0); - - const isStartValid = validateReplayTimeFormat(createStageForm.detail_time_start); - const isStopValid = validateReplayTimeFormat(createStageForm.detail_time_stop); - const isCreateFormValid = createStageForm.title.trim() !== "" && isStartValid && isStopValid; - - const showStartError = createStageForm.detail_time_start.length > 0 && !isStartValid; - const showStopError = createStageForm.detail_time_stop.length > 0 && !isStopValid; const [openWeightEditorKey, setOpenWeightEditorKey] = useState(null); const [openActionDetailKey, setOpenActionDetailKey] = useState(null); @@ -401,26 +354,7 @@ export default function ReplayTimelineSidebar({ ); }; - const handleDuplicateAction = ( - stageId: number, - stepIndex: number, - groupKey: ActionGroupKey, - actionIndex: number, - actionTitle: string - ) => { - onMutateReplay( - `Replay: nhân bản ${actionTitle} ở step ${stepIndex + 1} của stage #${stageId}`, - (draftReplay) => { - const stage = draftReplay.detail.find((item) => item.id === stageId); - if (!stage || stepIndex < 0 || stepIndex >= stage.steps.length) return; - const step = stage.steps[stepIndex]; - const actions = [...getStepActionGroup(step, groupKey)]; - if (actionIndex < 0 || actionIndex >= actions.length) return; - actions.splice(actionIndex + 1, 0, cloneReplayAction(actions[actionIndex]) as AnyStepAction); - setStepActionGroup(step, groupKey, actions); - } - ); - }; + const handleUpdateActionParams = ( stageId: number, @@ -603,63 +537,66 @@ export default function ReplayTimelineSidebar({ onChange={(event) => setCreateStageForm((prev) => ({ ...prev, title: event.target.value })) } - placeholder="Title (bắt buộc)" + placeholder="Title" style={inputStyle} /> -
- - setCreateStageForm((prev) => ({ - ...prev, - detail_time_start: event.target.value, - })) - } - placeholder="Thời gian bắt đầu (detail_time_start)" - style={{ - ...inputStyle, - borderColor: showStartError ? "#ef4444" : "#334155", - }} - /> - {showStartError ? ( -
- Định dạng không hợp lệ (DD/MM/YYYY, MM/YYYY, hoặc YYYY) -
- ) : null} -
-
- - setCreateStageForm((prev) => ({ - ...prev, - detail_time_stop: event.target.value, - })) - } - placeholder="Thời gian kết thúc (detail_time_stop)" - style={{ - ...inputStyle, - borderColor: showStopError ? "#ef4444" : "#334155", - }} - /> - {showStopError ? ( -
- Định dạng không hợp lệ (DD/MM/YYYY, MM/YYYY, hoặc YYYY) -
- ) : null} -
-
- Cấu trúc bắt buộc: DD/MM/YYYY, MM/YYYY, hoặc YYYY. + + setCreateStageForm((prev) => ({ + ...prev, + detail_time_start: event.target.value, + })) + } + placeholder="detail_time_start (DD/MM/YYYY hoặc MM/YYYY hoặc YYYY)" + style={{ + ...inputStyle, + border: createStageForm.detail_time_start && !validateReplayTimeFormat(createStageForm.detail_time_start) + ? "1px solid #ef4444" + : undefined, + }} + /> + + setCreateStageForm((prev) => ({ + ...prev, + detail_time_stop: event.target.value, + })) + } + placeholder="detail_time_stop (DD/MM/YYYY hoặc MM/YYYY hoặc YYYY)" + style={{ + ...inputStyle, + border: createStageForm.detail_time_stop && !validateReplayTimeFormat(createStageForm.detail_time_stop) + ? "1px solid #ef4444" + : undefined, + }} + /> +
+ * Định dạng bắt buộc: ngày/tháng/năm (00/00/0000), tháng/năm (00/0000) hoặc năm (0000).
- -
- -
+ {isActionOpen ? ( +
+ {entry.summary} + + handleUpdateActionParams( + stage.id, + stepIndex, + entry.groupKey, + entry.actionIndex, + entry.title, + nextParams + ) + } + /> +
+ ) : null} - {isActionOpen ? ( -
- {entry.summary} - - handleUpdateActionParams( - stage.id, - stepIndex, - entry.groupKey, - entry.actionIndex, - entry.title, - nextParams - ) - } - /> -
- ) : null} - - ); + ); })} ) : null} @@ -1120,23 +1042,13 @@ function StageMetadataEditor({ detail_time_stop: stage.detail_time_stop || "", }); - useEffect(() => { - setForm({ - title: stage.title || "", - detail_time_start: stage.detail_time_start || "", - detail_time_stop: stage.detail_time_stop || "", - }); - }, [stage]); - const isStartValid = validateReplayTimeFormat(form.detail_time_start); const isStopValid = validateReplayTimeFormat(form.detail_time_stop); - const isEditFormValid = form.title.trim() !== "" && isStartValid && isStopValid; - - const showStartError = form.detail_time_start.length > 0 && !isStartValid; - const showStopError = form.detail_time_stop.length > 0 && !isStopValid; + const isTitleValid = form.title.trim().length > 0; + const isFormValid = isStartValid && isStopValid && isTitleValid; const handleApplyStageMetadata = () => { - if (!isEditFormValid) return; + if (!isFormValid) return; onMutateReplay(`Replay: cập nhật stage #${stage.id}`, (draftReplay) => { const targetStage = draftReplay.detail.find((item) => item.id === stage.id); if (!targetStage) return; @@ -1147,70 +1059,55 @@ function StageMetadataEditor({ }; return ( - -
+ +
setForm((prev) => ({ ...prev, title: event.target.value })) } - placeholder="Title (bắt buộc)" + placeholder="Title" style={inputStyle} /> -
- - setForm((prev) => ({ - ...prev, - detail_time_start: event.target.value, - })) - } - placeholder="Thời gian bắt đầu (detail_time_start)" - style={{ - ...inputStyle, - borderColor: showStartError ? "#ef4444" : "#334155", - }} - /> - {showStartError ? ( -
- Định dạng không hợp lệ (DD/MM/YYYY, MM/YYYY, hoặc YYYY) -
- ) : null} -
-
- - setForm((prev) => ({ - ...prev, - detail_time_stop: event.target.value, - })) - } - placeholder="Thời gian kết thúc (detail_time_stop)" - style={{ - ...inputStyle, - borderColor: showStopError ? "#ef4444" : "#334155", - }} - /> - {showStopError ? ( -
- Định dạng không hợp lệ (DD/MM/YYYY, MM/YYYY, hoặc YYYY) -
- ) : null} -
-
- Cấu trúc bắt buộc: DD/MM/YYYY, MM/YYYY, hoặc YYYY. + + setForm((prev) => ({ + ...prev, + detail_time_start: event.target.value, + })) + } + placeholder="detail_time_start (DD/MM/YYYY hoặc MM/YYYY hoặc YYYY)" + style={{ + ...inputStyle, + border: form.detail_time_start && !isStartValid ? "1px solid #ef4444" : undefined, + }} + /> + + setForm((prev) => ({ + ...prev, + detail_time_stop: event.target.value, + })) + } + placeholder="detail_time_stop (DD/MM/YYYY hoặc MM/YYYY hoặc YYYY)" + style={{ + ...inputStyle, + border: form.detail_time_stop && !isStopValid ? "1px solid #ef4444" : undefined, + }} + /> +
+ * Định dạng bắt buộc: ngày/tháng/năm (00/00/0000), tháng/năm (00/0000) hoặc năm (0000).