{/* Current time indicator for events column */}
{
))
)}
+
+ {/* Event creation form */}
+
);
};
diff --git a/packages/web/src/views/Day/context/DayDraftContext.tsx b/packages/web/src/views/Day/context/DayDraftContext.tsx
new file mode 100644
index 000000000..1b1fee3e8
--- /dev/null
+++ b/packages/web/src/views/Day/context/DayDraftContext.tsx
@@ -0,0 +1,172 @@
+import React, { createContext, useCallback, useContext, useState } from "react";
+import {
+ FloatingContext,
+ autoUpdate,
+ flip,
+ offset,
+ shift,
+ useDismiss,
+ useFloating,
+} from "@floating-ui/react";
+import { Priorities } from "@core/constants/core.constants";
+import { Categories_Event, Schema_Event } from "@core/types/event.types";
+import dayjs, { Dayjs } from "@core/util/date/dayjs";
+import { getUserId } from "@web/auth/auth.util";
+import { createEventSlice } from "@web/ducks/events/slices/event.slice";
+import { useAppDispatch } from "@web/store/store.hooks";
+import { getTimeFromPosition } from "@web/views/Day/util/agenda/agenda.util";
+
+const DEFAULT_DURATION_MINUTES = 60;
+
+interface DayDraftContextValue {
+ draftEvent: Schema_Event | null;
+ isFormOpen: boolean;
+ floatingProps: {
+ refs: ReturnType
["refs"];
+ x: number | null;
+ y: number | null;
+ strategy: "fixed" | "absolute";
+ context: FloatingContext;
+ getFloatingProps: (
+ props?: React.HTMLProps,
+ ) => Record;
+ };
+ openFormAtPosition: (
+ yPosition: number,
+ dateInView: Dayjs,
+ clickX: number,
+ clickY: number,
+ ) => Promise;
+ closeForm: () => void;
+ setDraftEvent: (event: Schema_Event | null) => void;
+ submitDraft: (event: Schema_Event) => void;
+}
+
+const DayDraftContext = createContext(
+ undefined,
+);
+
+export const useDayDraftContext = () => {
+ const context = useContext(DayDraftContext);
+ if (!context) {
+ throw new Error("useDayDraftContext must be used within DayDraftProvider");
+ }
+ return context;
+};
+
+export const DayDraftProvider = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ const dispatch = useAppDispatch();
+ const [draftEvent, setDraftEvent] = useState(null);
+ const [isFormOpen, setIsFormOpen] = useState(false);
+
+ const { refs, x, y, strategy, context } = useFloating({
+ placement: "right-start",
+ middleware: [offset(10), flip(), shift({ padding: 8 })],
+ open: isFormOpen,
+ onOpenChange: setIsFormOpen,
+ whileElementsMounted: autoUpdate,
+ });
+
+ useDismiss(context, {
+ escapeKey: true,
+ outsidePress: true,
+ });
+
+ const closeForm = useCallback(() => {
+ setIsFormOpen(false);
+ setDraftEvent(null);
+ }, []);
+
+ const openFormAtPosition = useCallback(
+ async (
+ yPosition: number,
+ dateInView: Dayjs,
+ clickX: number,
+ clickY: number,
+ ) => {
+ const startTime = getTimeFromPosition(yPosition, dateInView);
+ const endTime = dayjs(startTime)
+ .add(DEFAULT_DURATION_MINUTES, "minutes")
+ .toDate();
+
+ const userId = await getUserId();
+
+ const newDraft: Schema_Event = {
+ title: "",
+ description: "",
+ startDate: startTime.toISOString(),
+ endDate: endTime.toISOString(),
+ isAllDay: false,
+ isSomeday: false,
+ user: userId,
+ priority: Priorities.UNASSIGNED,
+ };
+
+ // Set the virtual reference element where the user clicked
+ refs.setReference({
+ getBoundingClientRect: () => ({
+ x: clickX,
+ y: clickY,
+ top: clickY,
+ left: clickX,
+ bottom: clickY,
+ right: clickX,
+ width: 0,
+ height: 0,
+ toJSON: () => ({}),
+ }),
+ });
+
+ setDraftEvent(newDraft);
+ setIsFormOpen(true);
+ },
+ [refs],
+ );
+
+ const submitDraft = useCallback(
+ (event: Schema_Event) => {
+ dispatch(
+ createEventSlice.actions.request({
+ ...event,
+ recurrence: undefined,
+ }),
+ );
+ closeForm();
+ },
+ [dispatch, closeForm],
+ );
+
+ const getFloatingProps = useCallback(
+ (props?: React.HTMLProps) => {
+ return { ...props };
+ },
+ [],
+ );
+
+ const value: DayDraftContextValue = {
+ draftEvent,
+ isFormOpen,
+ floatingProps: {
+ refs,
+ x,
+ y,
+ strategy,
+ context,
+ getFloatingProps,
+ },
+ openFormAtPosition,
+ closeForm,
+ setDraftEvent,
+ submitDraft,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/web/src/views/Day/util/agenda/agenda.util.test.ts b/packages/web/src/views/Day/util/agenda/agenda.util.test.ts
index 16fafdced..e21a926df 100644
--- a/packages/web/src/views/Day/util/agenda/agenda.util.test.ts
+++ b/packages/web/src/views/Day/util/agenda/agenda.util.test.ts
@@ -1,5 +1,6 @@
+import dayjs from "@core/util/date/dayjs";
import { MINUTES_PER_SLOT, SLOT_HEIGHT } from "../../constants/day.constants";
-import { getNowLinePosition } from "./agenda.util";
+import { getNowLinePosition, getTimeFromPosition } from "./agenda.util";
describe("agenda.util", () => {
describe("getNowLinePosition", () => {
@@ -147,4 +148,69 @@ describe("agenda.util", () => {
expect(percentage).toBeCloseTo(0.4333, 2);
});
});
+
+ describe("getTimeFromPosition", () => {
+ const dateInView = dayjs("2024-01-15");
+
+ it("should calculate time at midnight (position 0)", () => {
+ const result = getTimeFromPosition(0, dateInView);
+ expect(result.getHours()).toBe(0);
+ expect(result.getMinutes()).toBe(0);
+ });
+
+ it("should calculate time at 1am (position 80px)", () => {
+ // 1 hour = 4 slots = 80px
+ const result = getTimeFromPosition(80, dateInView);
+ expect(result.getHours()).toBe(1);
+ expect(result.getMinutes()).toBe(0);
+ });
+
+ it("should calculate time at 12pm (position 960px)", () => {
+ // 12 hours = 48 slots = 960px
+ const result = getTimeFromPosition(960, dateInView);
+ expect(result.getHours()).toBe(12);
+ expect(result.getMinutes()).toBe(0);
+ });
+
+ it("should calculate time at 3:15pm (position 1220px)", () => {
+ // 15:15 = 61 slots = 1220px
+ const result = getTimeFromPosition(1220, dateInView);
+ expect(result.getHours()).toBe(15);
+ expect(result.getMinutes()).toBe(15);
+ });
+
+ it("should snap to 15-minute slots", () => {
+ // Position 1230 is between 15:15 (1220) and 15:30 (1240)
+ // Should snap to 15:15
+ const result = getTimeFromPosition(1230, dateInView);
+ expect(result.getHours()).toBe(15);
+ expect(result.getMinutes()).toBe(15);
+ });
+
+ it("should clamp to 23:45 at extreme positions", () => {
+ // Very large position should clamp to 23:45
+ const result = getTimeFromPosition(9999, dateInView);
+ expect(result.getHours()).toBe(23);
+ expect(result.getMinutes()).toBe(45);
+ });
+
+ it("should use the dateInView for the date portion", () => {
+ const specificDate = dayjs("2024-06-20");
+ const result = getTimeFromPosition(480, specificDate);
+ expect(result.getFullYear()).toBe(2024);
+ expect(result.getMonth()).toBe(5); // June (0-indexed)
+ expect(result.getDate()).toBe(20);
+ });
+
+ it("should be inverse of getAgendaEventPosition for slot-aligned times", () => {
+ // Test round-trip: position -> time -> position
+ const testPositions = [0, 80, 240, 480, 960, 1200, 1280, 1800];
+
+ for (const position of testPositions) {
+ const time = getTimeFromPosition(position, dateInView);
+ const resultPosition = getNowLinePosition(time);
+ expect(resultPosition).toBe(position);
+ }
+ });
+ });
});
diff --git a/packages/web/src/views/Day/util/agenda/agenda.util.ts b/packages/web/src/views/Day/util/agenda/agenda.util.ts
index 152eba0a7..d938876d2 100644
--- a/packages/web/src/views/Day/util/agenda/agenda.util.ts
+++ b/packages/web/src/views/Day/util/agenda/agenda.util.ts
@@ -1,4 +1,5 @@
import { Schema_Event } from "@core/types/event.types";
+import dayjs, { Dayjs } from "@core/util/date/dayjs";
import { MINUTES_PER_SLOT, SLOT_HEIGHT } from "../../constants/day.constants";
export const getAgendaEventTitle = (event: Schema_Event) =>
@@ -27,3 +28,24 @@ export const getNowLinePosition = (date: Date) => {
const minutes = date.getMinutes();
return (hours * 4 + minutes / MINUTES_PER_SLOT) * SLOT_HEIGHT;
};
+
+// Get time (Date) from Y position on the agenda grid, snapped to 15-minute slots
+export const getTimeFromPosition = (
+ yPosition: number,
+ dateInView: Dayjs,
+): Date => {
+ // Calculate which slot the position corresponds to
+ const slot = Math.floor(yPosition / SLOT_HEIGHT);
+ const hours = Math.floor(slot / 4);
+ const minutes = (slot % 4) * MINUTES_PER_SLOT;
+
+ // Clamp hours to valid range (0-23)
+ const clampedHours = Math.max(0, Math.min(23, hours));
+ const clampedMinutes = clampedHours === 23 ? Math.min(45, minutes) : minutes;
+
+ return dateInView
+ .startOf("day")
+ .hour(clampedHours)
+ .minute(clampedMinutes)
+ .toDate();
+};
diff --git a/packages/web/src/views/Day/util/day.test-util.tsx b/packages/web/src/views/Day/util/day.test-util.tsx
index 3bf1291cb..8775fb722 100644
--- a/packages/web/src/views/Day/util/day.test-util.tsx
+++ b/packages/web/src/views/Day/util/day.test-util.tsx
@@ -8,6 +8,7 @@ import { ROOT_ROUTES } from "@web/common/constants/routes";
import { loadSpecificDayData, loadTodayData } from "@web/routers/loaders";
import { store as defaultStore } from "@web/store";
import { DateNavigationProvider } from "@web/views/Day/context/DateNavigationProvider";
+import { DayDraftProvider } from "@web/views/Day/context/DayDraftContext";
import { StorageInfoModalProvider } from "@web/views/Day/context/StorageInfoModalContext";
import { TaskProvider } from "@web/views/Day/context/TaskProvider";
@@ -15,7 +16,9 @@ export const TaskProviderWrapper = ({ children }: PropsWithChildren) => {
return (
- {children}
+
+ {children}
+
);
diff --git a/packages/web/src/views/Day/view/DayView.tsx b/packages/web/src/views/Day/view/DayView.tsx
index adbf629ef..8875c776f 100644
--- a/packages/web/src/views/Day/view/DayView.tsx
+++ b/packages/web/src/views/Day/view/DayView.tsx
@@ -1,5 +1,6 @@
import { Outlet } from "react-router-dom";
import { DateNavigationProvider } from "@web/views/Day/context/DateNavigationProvider";
+import { DayDraftProvider } from "@web/views/Day/context/DayDraftContext";
import { StorageInfoModalProvider } from "@web/views/Day/context/StorageInfoModalContext";
import { TaskProvider } from "@web/views/Day/context/TaskProvider";
@@ -8,7 +9,9 @@ export function DayView() {
-
+
+
+
From df4680aff5354d055eee2c7bf7001d7f86ec1d1c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 29 Nov 2025 23:56:16 +0000
Subject: [PATCH 3/4] refactor(web): address code review feedback for day draft
context
Co-authored-by: victor-enogwe <23452630+victor-enogwe@users.noreply.github.com>
---
.../src/views/Day/context/DayDraftContext.tsx | 18 +++---------------
1 file changed, 3 insertions(+), 15 deletions(-)
diff --git a/packages/web/src/views/Day/context/DayDraftContext.tsx b/packages/web/src/views/Day/context/DayDraftContext.tsx
index 1b1fee3e8..9ca3c8b6f 100644
--- a/packages/web/src/views/Day/context/DayDraftContext.tsx
+++ b/packages/web/src/views/Day/context/DayDraftContext.tsx
@@ -9,7 +9,7 @@ import {
useFloating,
} from "@floating-ui/react";
import { Priorities } from "@core/constants/core.constants";
-import { Categories_Event, Schema_Event } from "@core/types/event.types";
+import { Schema_Event } from "@core/types/event.types";
import dayjs, { Dayjs } from "@core/util/date/dayjs";
import { getUserId } from "@web/auth/auth.util";
import { createEventSlice } from "@web/ducks/events/slices/event.slice";
@@ -129,24 +129,12 @@ export const DayDraftProvider = ({
const submitDraft = useCallback(
(event: Schema_Event) => {
- dispatch(
- createEventSlice.actions.request({
- ...event,
- recurrence: undefined,
- }),
- );
+ dispatch(createEventSlice.actions.request(event));
closeForm();
},
[dispatch, closeForm],
);
- const getFloatingProps = useCallback(
- (props?: React.HTMLProps) => {
- return { ...props };
- },
- [],
- );
-
const value: DayDraftContextValue = {
draftEvent,
isFormOpen,
@@ -156,7 +144,7 @@ export const DayDraftProvider = ({
y,
strategy,
context,
- getFloatingProps,
+ getFloatingProps: (props) => props ?? {},
},
openFormAtPosition,
closeForm,
From a9d8217905d4a7db038e7d420133296fb23adaec Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 30 Nov 2025 00:07:14 +0000
Subject: [PATCH 4/4] fix(web): add missing origin field to draft event for
backend validation
Co-authored-by: victor-enogwe <23452630+victor-enogwe@users.noreply.github.com>
---
packages/web/src/views/Day/context/DayDraftContext.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/web/src/views/Day/context/DayDraftContext.tsx b/packages/web/src/views/Day/context/DayDraftContext.tsx
index 9ca3c8b6f..f9d3cd5cb 100644
--- a/packages/web/src/views/Day/context/DayDraftContext.tsx
+++ b/packages/web/src/views/Day/context/DayDraftContext.tsx
@@ -8,7 +8,7 @@ import {
useDismiss,
useFloating,
} from "@floating-ui/react";
-import { Priorities } from "@core/constants/core.constants";
+import { Origin, Priorities } from "@core/constants/core.constants";
import { Schema_Event } from "@core/types/event.types";
import dayjs, { Dayjs } from "@core/util/date/dayjs";
import { getUserId } from "@web/auth/auth.util";
@@ -104,6 +104,7 @@ export const DayDraftProvider = ({
isSomeday: false,
user: userId,
priority: Priorities.UNASSIGNED,
+ origin: Origin.COMPASS,
};
// Set the virtual reference element where the user clicked