Skip to content

Commit 4872b45

Browse files
committed
Update v0.5 Bug fixes by GPT5.2-Codex High
1 parent 4293155 commit 4872b45

6 files changed

Lines changed: 375 additions & 112 deletions

File tree

App.tsx

Lines changed: 126 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ import {
2020
ZOOM_LEVELS,
2121
DEFAULT_ZOOM_INDEX,
2222
COLOR_PALETTE,
23+
DEFAULT_MIN_YEAR,
24+
DEFAULT_MAX_YEAR,
25+
DRAG_THRESHOLD_PX,
2326
} from "./constants";
2427
import {
2528
parseDate,
2629
formatDate,
30+
normalizeDateString,
2731
addDays,
2832
getDaysDiff,
2933
getMonthYear,
@@ -44,13 +48,43 @@ import { calculateTrackLanes } from "./utils/eventLanes";
4448

4549
// -- Main App Component --
4650

51+
const normalizeEventDates = (event: IEvent): IEvent => {
52+
const startDate = normalizeDateString(event.startDate) || event.startDate;
53+
const endDate =
54+
normalizeDateString(event.endDate || event.startDate) ||
55+
event.endDate ||
56+
event.startDate;
57+
return {
58+
...event,
59+
startDate,
60+
endDate,
61+
};
62+
};
63+
64+
const normalizeEventList = (events: IEvent[]) =>
65+
events.map((event) => normalizeEventDates(event));
66+
4767
export default function App() {
4868
// -- State --
4969
const [data, setData] = useState<ITimelineData>(() => {
5070
const saved = localStorage.getItem("chrono_data");
51-
return saved
52-
? JSON.parse(saved)
53-
: { tracks: DEFAULT_TRACKS, events: DEFAULT_EVENTS };
71+
if (saved) {
72+
try {
73+
const parsed = JSON.parse(saved);
74+
if (parsed?.tracks && parsed?.events) {
75+
return {
76+
tracks: parsed.tracks,
77+
events: normalizeEventList(parsed.events),
78+
};
79+
}
80+
} catch {
81+
// fall through to defaults
82+
}
83+
}
84+
return {
85+
tracks: DEFAULT_TRACKS,
86+
events: normalizeEventList(DEFAULT_EVENTS),
87+
};
5488
});
5589

5690
const [zoomIndex, setZoomIndex] = useState(DEFAULT_ZOOM_INDEX);
@@ -124,11 +158,11 @@ export default function App() {
124158
// fall through to defaults
125159
}
126160
}
127-
return { minYear: 2020, maxYear: 2030 };
161+
return { minYear: DEFAULT_MIN_YEAR, maxYear: DEFAULT_MAX_YEAR };
128162
});
129163
const [boundsDraft, setBoundsDraft] = useState<IViewBounds>(() => ({
130-
minYear: 2020,
131-
maxYear: 2030,
164+
minYear: DEFAULT_MIN_YEAR,
165+
maxYear: DEFAULT_MAX_YEAR,
132166
}));
133167

134168
const timelineRange = useMemo(() => {
@@ -185,6 +219,26 @@ export default function App() {
185219
}
186220
}, [zoomIndex, timelineRange, pixelsPerDay]);
187221

222+
// Scroll to center on today's date on initial load
223+
const hasScrolledToTodayRef = useRef(false);
224+
useLayoutEffect(() => {
225+
if (!hasScrolledToTodayRef.current && scrollContainerRef.current) {
226+
const container = scrollContainerRef.current;
227+
const today = new Date();
228+
today.setHours(0, 0, 0, 0); // Normalize to midnight
229+
230+
// Calculate days from timeline start to today
231+
const daysFromStart = getDaysDiff(timelineRange.min, today);
232+
const todayPixel = daysFromStart * pixelsPerDay;
233+
234+
// Center the view on today
235+
const newScrollLeft = todayPixel - container.clientWidth / 2;
236+
container.scrollLeft = Math.max(0, newScrollLeft);
237+
238+
hasScrolledToTodayRef.current = true;
239+
}
240+
}, [timelineRange, pixelsPerDay]);
241+
188242
// -- Panning Logic --
189243
useEffect(() => {
190244
const handleGlobalMouseMove = (e: MouseEvent) => {
@@ -240,7 +294,7 @@ export default function App() {
240294
}
241295
};
242296

243-
const handleGlobalMouseUp = () => {
297+
const handleGlobalMouseUp = (e: MouseEvent) => {
244298
// Panning Cleanup
245299
if (panStateRef.current.isActive) {
246300
panStateRef.current.isActive = false;
@@ -254,13 +308,27 @@ export default function App() {
254308
// Event Dragging Cleanup
255309
const state = dragStateRef.current;
256310
if (state.isDragging && state.eventId) {
257-
const deltaX = state.currentX - state.startX;
258-
259-
if (
260-
Math.abs(deltaX) > 2 ||
311+
// Use e.clientX directly to avoid race condition with React state updates
312+
const deltaX = e.clientX - state.startX;
313+
const deltaY = e.clientY - state.startY;
314+
const movedEnough =
315+
Math.abs(deltaX) > DRAG_THRESHOLD_PX ||
261316
(state.mode === "move" &&
262-
state.targetTrackId !== state.initialTrackId)
263-
) {
317+
Math.abs(deltaY) > DRAG_THRESHOLD_PX);
318+
319+
// Detect target track from actual mouse position at release
320+
let finalTargetTrackId = state.initialTrackId;
321+
if (state.mode === "move") {
322+
const el = document.elementFromPoint(e.clientX, e.clientY);
323+
const trackRow = el?.closest("[data-track-id]");
324+
if (trackRow) {
325+
finalTargetTrackId =
326+
trackRow.getAttribute("data-track-id") ||
327+
state.initialTrackId;
328+
}
329+
}
330+
331+
if (movedEnough) {
264332
setSuppressClick(true);
265333
setTimeout(() => setSuppressClick(false), 50);
266334

@@ -281,8 +349,7 @@ export default function App() {
281349
parseDate(state.initialEndDate)
282350
);
283351
newEndDate = addDays(newStartDate, duration);
284-
if (state.targetTrackId)
285-
newTrackId = state.targetTrackId;
352+
newTrackId = finalTargetTrackId;
286353
} else if (state.mode === "resize-start") {
287354
newStartDate = addDays(
288355
parseDate(state.initialStartDate),
@@ -381,6 +448,7 @@ export default function App() {
381448
event: IEvent,
382449
mode: DragMode
383450
) => {
451+
if (e.button !== 0) return;
384452
e.stopPropagation();
385453
// Note: Don't call e.preventDefault() here as it blocks double-click events
386454

@@ -471,6 +539,26 @@ export default function App() {
471539
}
472540
};
473541

542+
const handleReorderTracks = (fromIndex: number, toIndex: number) => {
543+
if (fromIndex === toIndex) return;
544+
545+
setData((prev) => {
546+
const sortedTracks = [...prev.tracks].sort(
547+
(a, b) => a.order - b.order
548+
);
549+
const [movedTrack] = sortedTracks.splice(fromIndex, 1);
550+
sortedTracks.splice(toIndex, 0, movedTrack);
551+
552+
// Update order property for all tracks
553+
const updatedTracks = sortedTracks.map((track, index) => ({
554+
...track,
555+
order: index,
556+
}));
557+
558+
return { ...prev, tracks: updatedTracks };
559+
});
560+
};
561+
474562
const handleAddEvent = (trackId: string, dateStr?: string) => {
475563
const startDate = dateStr ? parseDate(dateStr) : new Date();
476564
const endDate = addDays(startDate, 7); // Default 7-day duration
@@ -489,17 +577,17 @@ export default function App() {
489577

490578
const handleEditEvent = (event: IEvent) => {
491579
if (suppressClick) return;
492-
setEditingEvent({ ...event });
580+
setEditingEvent(normalizeEventDates(event));
493581
setActiveModal(ModalType.EDIT_EVENT);
494582
};
495583

496584
const handleSaveEvent = () => {
497585
if (!editingEvent) return;
498586
// Ensure endDate is always set (default to startDate if empty)
499-
const eventToSave = {
587+
const eventToSave = normalizeEventDates({
500588
...editingEvent,
501589
endDate: editingEvent.endDate || editingEvent.startDate,
502-
};
590+
});
503591
setData((prev) => {
504592
const exists = prev.events.find((e) => e.id === eventToSave.id);
505593
if (exists) {
@@ -543,7 +631,10 @@ export default function App() {
543631
try {
544632
const imported = JSON.parse(evt.target?.result as string);
545633
if (imported.tracks && imported.events) {
546-
setData(imported);
634+
setData({
635+
tracks: imported.tracks,
636+
events: normalizeEventList(imported.events),
637+
});
547638
} else {
548639
alert("Invalid JSON format");
549640
}
@@ -850,6 +941,13 @@ export default function App() {
850941
if (!dragState.isDragging || !dragState.eventId) return null;
851942

852943
const deltaX = dragState.currentX - dragState.startX;
944+
const deltaY = dragState.currentY - dragState.startY;
945+
const movedEnough =
946+
dragState.mode === "move"
947+
? Math.abs(deltaX) > DRAG_THRESHOLD_PX ||
948+
Math.abs(deltaY) > DRAG_THRESHOLD_PX
949+
: Math.abs(deltaX) > DRAG_THRESHOLD_PX;
950+
if (!movedEnough) return null;
853951
const deltaDays = Math.round(deltaX / pixelsPerDay);
854952
const initialStart = parseDate(dragState.initialStartDate);
855953
const initialEnd = parseDate(dragState.initialEndDate);
@@ -966,6 +1064,12 @@ export default function App() {
9661064
[data.events, data.tracks]
9671065
);
9681066

1067+
// Sort tracks by order for consistent display
1068+
const sortedTracks = useMemo(
1069+
() => [...data.tracks].sort((a, b) => a.order - b.order),
1070+
[data.tracks]
1071+
);
1072+
9691073
const isTrackModalOpen = activeModal === ModalType.EDIT_TRACK;
9701074
const isEventModalOpen = activeModal === ModalType.EDIT_EVENT;
9711075
const isDataModalOpen = activeModal === ModalType.IMPORT_EXPORT;
@@ -987,14 +1091,15 @@ export default function App() {
9871091
<div className="flex-1 overflow-hidden relative flex flex-col cursor-default">
9881092
<div className="flex-1 flex overflow-hidden">
9891093
<TrackSidebar
990-
tracks={data.tracks}
1094+
tracks={sortedTracks}
9911095
trackLaneInfo={trackLaneInfo}
9921096
onAddTrack={handleAddTrack}
9931097
onEditTrack={handleEditTrack}
1098+
onReorderTracks={handleReorderTracks}
9941099
/>
9951100

9961101
<TimelineCanvas
997-
tracks={data.tracks}
1102+
tracks={sortedTracks}
9981103
trackLaneInfo={trackLaneInfo}
9991104
dragState={dragState}
10001105
isPanning={isPanning}

components/EventModal.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Button } from "./Button";
33
import { Modal } from "./Modal";
44
import { ColorPicker } from "./color/ColorPicker";
55
import { IEvent, ITrack } from "../types";
6+
import { normalizeDateString } from "../utils/dateUtils";
67

78
type EventModalProps = {
89
isOpen: boolean;
@@ -25,6 +26,11 @@ export const EventModal: React.FC<EventModalProps> = ({
2526
}) => {
2627
if (!isOpen || !editingEvent) return null;
2728

29+
const startDateValue = normalizeDateString(editingEvent.startDate);
30+
const endDateValue = normalizeDateString(
31+
editingEvent.endDate || editingEvent.startDate
32+
);
33+
2834
return (
2935
<Modal
3036
isOpen={isOpen}
@@ -65,10 +71,10 @@ export const EventModal: React.FC<EventModalProps> = ({
6571
<input
6672
type="date"
6773
className="block w-full rounded-lg border-slate-200 bg-slate-50 px-3 py-2.5 text-sm shadow-sm focus:border-brand-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-brand-500/20 transition-all"
68-
value={editingEvent.startDate || ""}
74+
value={startDateValue}
6975
onChange={(e) => {
7076
const newStartDate = e.target.value;
71-
const currentEndDate = editingEvent.endDate || editingEvent.startDate;
77+
const currentEndDate = endDateValue || newStartDate;
7278
// If new start date is after current end date, adjust end date
7379
const updatedEndDate = newStartDate > currentEndDate ? newStartDate : currentEndDate;
7480
onChange({
@@ -86,8 +92,8 @@ export const EventModal: React.FC<EventModalProps> = ({
8692
<input
8793
type="date"
8894
className="block w-full rounded-lg border-slate-200 bg-slate-50 px-3 py-2.5 text-sm shadow-sm focus:border-brand-500 focus:bg-white focus:outline-none focus:ring-2 focus:ring-brand-500/20 transition-all"
89-
value={editingEvent.endDate || ""}
90-
min={editingEvent.startDate}
95+
value={endDateValue}
96+
min={startDateValue}
9197
onChange={(e) =>
9298
onChange({
9399
...editingEvent,
@@ -150,4 +156,3 @@ export const EventModal: React.FC<EventModalProps> = ({
150156
</Modal>
151157
);
152158
};
153-

0 commit comments

Comments
 (0)