diff --git a/src/components/Calendar/handlers/eventHandlers.ts b/src/components/Calendar/handlers/eventHandlers.ts
index 67e875d..11dc84c 100644
--- a/src/components/Calendar/handlers/eventHandlers.ts
+++ b/src/components/Calendar/handlers/eventHandlers.ts
@@ -16,7 +16,7 @@ import { getEvent } from "../../../features/Events/EventApi";
import { refreshCalendars } from "../../Event/utils/eventUtils";
import { updateTempCalendar } from "../utils/calendarUtils";
import { User } from "../../Attendees/PeopleSearch";
-import { formatLocalDateTime } from "../../Event/EventFormFields";
+import { formatLocalDateTime } from "../../Event/utils/dateTimeFormatters";
export interface EventHandlersProps {
setSelectedRange: (range: DateSelectArg | null) => void;
diff --git a/src/components/Event/EventFormFields.tsx b/src/components/Event/EventFormFields.tsx
index c10b22e..bba2d7a 100644
--- a/src/components/Event/EventFormFields.tsx
+++ b/src/components/Event/EventFormFields.tsx
@@ -34,75 +34,16 @@ import {
} from "../../utils/videoConferenceUtils";
import { TimezoneAutocomplete } from "../Timezone/TimezoneAutocomplete";
import { CalendarItemList } from "../Calendar/CalendarItemList";
-
-// Helper to split datetime string (YYYY-MM-DDTHH:mm) to date and time
-function splitDateTime(datetime: string): { date: string; time: string } {
- if (!datetime) return { date: "", time: "" };
- const parts = datetime.split("T");
- return {
- date: parts[0] || "",
- time: parts[1]?.slice(0, 5) || "", // HH:mm only
- };
-}
-
-// Helper to combine date and time to datetime string
-function combineDateTime(date: string, time: string): string {
- if (!date) return "";
- if (!time) return date; // Date only for all-day
- return `${date}T${time}`;
-}
-
-// Helper component for field with label
-export const FieldWithLabel = React.memo(
- ({
- label,
- isExpanded,
- children,
- }: {
- label: string | React.ReactNode;
- isExpanded: boolean;
- children: React.ReactNode;
- }) => {
- if (!isExpanded) {
- // Normal mode: label on top
- return (
-
-
- {label}
-
- {children}
-
- );
- }
-
- // Extended mode: label on left
- return (
-
-
- {label}
-
- {children}
-
- );
- }
-);
-
-FieldWithLabel.displayName = "FieldWithLabel";
+import { FieldWithLabel } from "./components/FieldWithLabel";
+import { DateTimeFields } from "./components/DateTimeFields";
+import { useAllDayToggle } from "./hooks/useAllDayToggle";
+import { splitDateTime, combineDateTime } from "./utils/dateTimeHelpers";
+import {
+ formatLocalDateTime,
+ formatDateTimeInTimezone,
+ getRoundedCurrentTime,
+} from "./utils/dateTimeFormatters";
+import { validateEventForm } from "./utils/formValidation";
interface EventFormFieldsProps {
// Form state
@@ -216,20 +157,30 @@ export default function EventFormFields({
onValidationChange,
showValidationErrors = false,
}: EventFormFieldsProps) {
- // Store original time before toggling to all-day
- const originalTimeRef = React.useRef<{
- start: string;
- end: string;
- endDate?: string; // Add this field
- fromAllDaySlot?: boolean; // Track if came from all-day slot click
- } | null>(null);
-
// Internal state for 4 separate fields
const [startDate, setStartDate] = React.useState("");
const [startTime, setStartTime] = React.useState("");
const [endDate, setEndDate] = React.useState("");
const [endTime, setEndTime] = React.useState("");
+ // Use all-day toggle hook
+ const { originalTimeRef, handleAllDayToggle } = useAllDayToggle({
+ allday,
+ start,
+ end,
+ startDate,
+ startTime,
+ endDate,
+ endTime,
+ setStartTime,
+ setEndTime,
+ setEndDate,
+ setStart,
+ setEnd,
+ setAllDay,
+ onAllDayChange,
+ });
+
// Ref for title input field to enable auto-focus
const titleInputRef = React.useRef(null);
@@ -367,59 +318,15 @@ export default function EventFormFields({
// Validation logic
const validateForm = React.useCallback(() => {
- const isTitleValid = title.trim().length > 0;
- const shouldShowTitleError = showValidationErrors && !isTitleValid;
-
- let isDateTimeValid = true;
- let dateTimeError = "";
-
- // Validate start date
- if (!startDate || startDate.trim() === "") {
- isDateTimeValid = false;
- dateTimeError = "Start date is required";
- }
- // Validate start time (if not all-day)
- else if (!allday && (!startTime || startTime.trim() === "")) {
- isDateTimeValid = false;
- dateTimeError = "Start time is required";
- }
- // Validate end date
- else if (!endDate || endDate.trim() === "") {
- isDateTimeValid = false;
- dateTimeError = "End date is required";
- }
- // Validate end time (if not all-day)
- else if (!allday && (!endTime || endTime.trim() === "")) {
- isDateTimeValid = false;
- dateTimeError = "End time is required";
- }
- // Validate end > start
- else {
- const startDateTime = allday
- ? new Date(startDate + "T00:00:00")
- : new Date(combineDateTime(startDate, startTime));
- const endDateTime = allday
- ? new Date(endDate + "T00:00:00")
- : new Date(combineDateTime(endDate, endTime));
-
- if (isNaN(startDateTime.getTime()) || isNaN(endDateTime.getTime())) {
- isDateTimeValid = false;
- dateTimeError = "Invalid date/time";
- } else if (endDateTime <= startDateTime) {
- isDateTimeValid = false;
- dateTimeError = "End time must be after start time";
- }
- }
-
- const isValid = isTitleValid && isDateTimeValid;
-
- return {
- isValid,
- errors: {
- title: shouldShowTitleError ? "Title is required" : "",
- dateTime: showValidationErrors ? dateTimeError : "",
- },
- };
+ return validateEventForm({
+ title,
+ startDate,
+ startTime,
+ endDate,
+ endTime,
+ allday,
+ showValidationErrors,
+ });
}, [
title,
startDate,
@@ -470,30 +377,11 @@ export default function EventFormFields({
setMeetingLink(null);
};
- const handleAllDayChange = (
- newAllDay: boolean,
- newStart: string,
- newEnd: string
- ) => {
- setAllDay(newAllDay);
- onAllDayChange?.(newAllDay, newStart, newEnd);
- };
-
const handleCalendarChange = (newCalendarId: string) => {
setCalendarid(newCalendarId);
onCalendarChange?.(newCalendarId);
};
- const getRoundedCurrentTime = () => {
- const now = new Date();
- const minutes = now.getMinutes();
- const roundedMinutes = minutes < 30 ? 0 : 30;
- now.setMinutes(roundedMinutes);
- now.setSeconds(0);
- now.setMilliseconds(0);
- return now;
- };
-
return (
<>
-
- {/* First row: 4 fields */}
-
- {/* Start Date - 30% */}
-
- {showMore && (
-
- Start Date
-
- )}
- handleStartDateChange(e.target.value)}
- size="small"
- margin="dense"
- InputLabelProps={{ shrink: true }}
- />
-
-
- {/* Start Time - 20% */}
-
- {showMore && (
-
- Start Time
-
- )}
- handleStartTimeChange(e.target.value)}
- disabled={allday}
- size="small"
- margin="dense"
- InputLabelProps={{ shrink: true }}
- />
-
-
- {/* End Time - 20% */}
-
- {showMore && (
-
- End Time
-
- )}
- handleEndTimeChange(e.target.value)}
- disabled={allday}
- error={!!validation.errors.dateTime}
- size="small"
- margin="dense"
- InputLabelProps={{ shrink: true }}
- />
-
-
- {/* End Date - 30% */}
-
- {showMore && (
-
- End Date
-
- )}
- handleEndDateChange(e.target.value)}
- error={!!validation.errors.dateTime}
- size="small"
- margin="dense"
- InputLabelProps={{ shrink: true }}
- />
-
-
-
- {/* Second row: Error message - 2 columns 50% each */}
- {validation.errors.dateTime && (
-
- {/* Empty left column - 50% */}
-
-
- {/* Error message right column - 50% */}
-
-
- {validation.errors.dateTime}
-
-
-
- )}
-
+
{
- const newAllDay = !allday;
- let newStart = start;
- let newEnd = end;
-
- if (newAllDay) {
- // OFF => ON: Save original time AND original end date
- if (start.includes("T")) {
- originalTimeRef.current = {
- start: startTime,
- end: endTime,
- endDate: endDate, // Save original end date
- };
- }
-
- // Reset time to empty, keep only date part
- setStartTime("");
- setEndTime("");
- newStart = startDate;
- newEnd = endDate;
-
- // If same day, extend end to next day
- if (startDate === endDate) {
- const nextDay = new Date(endDate);
- nextDay.setDate(nextDay.getDate() + 1);
- newEnd = nextDay.toISOString().split("T")[0];
- setEndDate(newEnd); // Update internal endDate state
- }
- } else {
- // ON => OFF: Restore original time AND original end date
- if (originalTimeRef.current) {
- // Check if this came from all-day slot click
- if (originalTimeRef.current.fromAllDaySlot) {
- // From all-day slot: set endDate = startDate, use default time
- const currentTime = getRoundedCurrentTime();
- const hours = String(currentTime.getHours()).padStart(
- 2,
- "0"
- );
- const minutes = String(
- currentTime.getMinutes()
- ).padStart(2, "0");
- const timeStr = `${hours}:${minutes}`;
-
- newStart = combineDateTime(startDate, timeStr);
-
- // End time = start time + 1 hour
- const endTimeDate = new Date(currentTime);
- endTimeDate.setHours(endTimeDate.getHours() + 1);
- const endHours = String(
- endTimeDate.getHours()
- ).padStart(2, "0");
- const endMinutes = String(
- endTimeDate.getMinutes()
- ).padStart(2, "0");
- const endTimeStr = `${endHours}:${endMinutes}`;
-
- newEnd = combineDateTime(startDate, endTimeStr); // Use startDate as endDate
-
- // Update internal states
- setStartTime(timeStr);
- setEndTime(endTimeStr);
- setEndDate(startDate); // Set endDate = startDate
- } else {
- // Normal case: restore original time AND original end date
- const restoredEndDate =
- originalTimeRef.current.endDate || endDate;
-
- newStart = combineDateTime(
- startDate,
- originalTimeRef.current.start
- );
- newEnd = combineDateTime(
- restoredEndDate,
- originalTimeRef.current.end
- );
-
- // Update internal states
- setStartTime(originalTimeRef.current.start);
- setEndTime(originalTimeRef.current.end);
- setEndDate(restoredEndDate);
- }
-
- originalTimeRef.current = null;
- } else {
- // No original time: use rounded current time with 1 hour duration
- const currentTime = getRoundedCurrentTime();
- const hours = String(currentTime.getHours()).padStart(
- 2,
- "0"
- );
- const minutes = String(currentTime.getMinutes()).padStart(
- 2,
- "0"
- );
- const timeStr = `${hours}:${minutes}`;
-
- newStart = combineDateTime(startDate, timeStr);
-
- // End time = start time + 1 hour (not 30 mins)
- const endTimeDate = new Date(currentTime);
- endTimeDate.setHours(endTimeDate.getHours() + 1); // Changed from setMinutes +30
- const endHours = String(endTimeDate.getHours()).padStart(
- 2,
- "0"
- );
- const endMinutes = String(
- endTimeDate.getMinutes()
- ).padStart(2, "0");
- const endTimeStr = `${endHours}:${endMinutes}`;
-
- newEnd = combineDateTime(endDate, endTimeStr);
-
- // Update internal states
- setStartTime(timeStr);
- setEndTime(endTimeStr);
- }
- }
-
- if (!onAllDayChange) {
- setStart(newStart);
- setEnd(newEnd);
- setAllDay(newAllDay);
- } else {
- handleAllDayChange(newAllDay, newStart, newEnd);
- }
- }}
- />
+
}
label="All day"
/>
@@ -1017,54 +685,3 @@ export default function EventFormFields({
>
);
}
-
-export function formatLocalDateTime(date: Date, timeZone?: string): string {
- // Guard against invalid or undefined dates
- if (!date || isNaN(date.getTime())) {
- return "";
- }
-
- if (timeZone) {
- const formatter = new Intl.DateTimeFormat("en-CA", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- hour12: false,
- timeZone,
- });
- const formatted = formatter.format(date);
- return formatted.replace(", ", "T");
- }
-
- const pad = (n: number) => n.toString().padStart(2, "0");
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
- date.getDate()
- )}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
-}
-
-export function formatDateTimeInTimezone(
- isoString: string,
- timezone: string
-): string {
- // Parse the ISO string as UTC
- const utcDate = new Date(isoString);
-
- // Format the date in the target timezone
- const formatter = new Intl.DateTimeFormat("en-CA", {
- timeZone: timezone,
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- hour12: false,
- });
-
- const parts = formatter.formatToParts(utcDate);
- const getValue = (type: string) =>
- parts.find((p) => p.type === type)?.value || "";
-
- return `${getValue("year")}-${getValue("month")}-${getValue("day")}T${getValue("hour")}:${getValue("minute")}`;
-}
diff --git a/src/components/Event/components/DateTimeFields.tsx b/src/components/Event/components/DateTimeFields.tsx
new file mode 100644
index 0000000..818b0a8
--- /dev/null
+++ b/src/components/Event/components/DateTimeFields.tsx
@@ -0,0 +1,143 @@
+import React from "react";
+import { Box, TextField, Typography } from "@mui/material";
+
+/**
+ * Props for DateTimeFields component
+ */
+export interface DateTimeFieldsProps {
+ startDate: string;
+ startTime: string;
+ endDate: string;
+ endTime: string;
+ allday: boolean;
+ showMore: boolean;
+ validation: {
+ errors: {
+ dateTime: string;
+ };
+ };
+ onStartDateChange: (date: string) => void;
+ onStartTimeChange: (time: string) => void;
+ onEndDateChange: (date: string) => void;
+ onEndTimeChange: (time: string) => void;
+}
+
+/**
+ * DateTimeFields component - 4 separate date/time input fields
+ * Layout: Start Date (30%) | Start Time (20%) | End Time (20%) | End Date (30%)
+ */
+export const DateTimeFields: React.FC = ({
+ startDate,
+ startTime,
+ endDate,
+ endTime,
+ allday,
+ showMore,
+ validation,
+ onStartDateChange,
+ onStartTimeChange,
+ onEndDateChange,
+ onEndTimeChange,
+}) => {
+ return (
+
+ {/* First row: 4 fields */}
+
+ {/* Start Date - 30% */}
+
+ {showMore && (
+
+ Start Date
+
+ )}
+ onStartDateChange(e.target.value)}
+ size="small"
+ margin="dense"
+ InputLabelProps={{ shrink: true }}
+ />
+
+
+ {/* Start Time - 20% */}
+
+ {showMore && (
+
+ Start Time
+
+ )}
+ onStartTimeChange(e.target.value)}
+ disabled={allday}
+ size="small"
+ margin="dense"
+ InputLabelProps={{ shrink: true }}
+ />
+
+
+ {/* End Time - 20% */}
+
+ {showMore && (
+
+ End Time
+
+ )}
+ onEndTimeChange(e.target.value)}
+ disabled={allday}
+ error={!!validation.errors.dateTime}
+ size="small"
+ margin="dense"
+ InputLabelProps={{ shrink: true }}
+ />
+
+
+ {/* End Date - 30% */}
+
+ {showMore && (
+
+ End Date
+
+ )}
+ onEndDateChange(e.target.value)}
+ error={!!validation.errors.dateTime}
+ size="small"
+ margin="dense"
+ InputLabelProps={{ shrink: true }}
+ />
+
+
+
+ {/* Second row: Error message - 2 columns 50% each */}
+ {validation.errors.dateTime && (
+
+ {/* Empty left column - 50% */}
+
+
+ {/* Error message right column - 50% */}
+
+
+ {validation.errors.dateTime}
+
+
+
+ )}
+
+ );
+};
diff --git a/src/components/Event/components/FieldWithLabel.tsx b/src/components/Event/components/FieldWithLabel.tsx
new file mode 100644
index 0000000..85b4dc0
--- /dev/null
+++ b/src/components/Event/components/FieldWithLabel.tsx
@@ -0,0 +1,55 @@
+import React from "react";
+import { Box, Typography } from "@mui/material";
+
+/**
+ * Helper component for field with label
+ * Supports two modes: normal (label on top) and expanded (label on left)
+ */
+export const FieldWithLabel = React.memo(
+ ({
+ label,
+ isExpanded,
+ children,
+ }: {
+ label: string | React.ReactNode;
+ isExpanded: boolean;
+ children: React.ReactNode;
+ }) => {
+ if (!isExpanded) {
+ // Normal mode: label on top
+ return (
+
+
+ {label}
+
+ {children}
+
+ );
+ }
+
+ // Extended mode: label on left
+ return (
+
+
+ {label}
+
+ {children}
+
+ );
+ }
+);
diff --git a/src/components/Event/hooks/useAllDayToggle.ts b/src/components/Event/hooks/useAllDayToggle.ts
new file mode 100644
index 0000000..5a2e953
--- /dev/null
+++ b/src/components/Event/hooks/useAllDayToggle.ts
@@ -0,0 +1,193 @@
+import React from "react";
+import { combineDateTime } from "../utils/dateTimeHelpers";
+import { getRoundedCurrentTime } from "../utils/dateTimeFormatters";
+
+/**
+ * Parameters for all-day toggle hook
+ */
+export interface AllDayToggleParams {
+ allday: boolean;
+ start: string;
+ end: string;
+ startDate: string;
+ startTime: string;
+ endDate: string;
+ endTime: string;
+ setStartTime: (time: string) => void;
+ setEndTime: (time: string) => void;
+ setEndDate: (date: string) => void;
+ setStart: (start: string) => void;
+ setEnd: (end: string) => void;
+ setAllDay: (allday: boolean) => void;
+ onAllDayChange?: (allday: boolean, start: string, end: string) => void;
+}
+
+/**
+ * Handlers returned by all-day toggle hook
+ */
+export interface AllDayToggleHandlers {
+ originalTimeRef: React.MutableRefObject<{
+ start: string;
+ end: string;
+ endDate?: string;
+ fromAllDaySlot?: boolean;
+ } | null>;
+ handleAllDayToggle: () => void;
+}
+
+/**
+ * Custom hook for managing all-day toggle logic
+ * Handles saving/restoring time values and endDate logic
+ */
+export function useAllDayToggle(
+ params: AllDayToggleParams
+): AllDayToggleHandlers {
+ const {
+ allday,
+ start,
+ end,
+ startDate,
+ startTime,
+ endDate,
+ endTime,
+ setStartTime,
+ setEndTime,
+ setEndDate,
+ setStart,
+ setEnd,
+ setAllDay,
+ onAllDayChange,
+ } = params;
+
+ // Store original time before toggling to all-day
+ const originalTimeRef = React.useRef<{
+ start: string;
+ end: string;
+ endDate?: string;
+ fromAllDaySlot?: boolean;
+ } | null>(null);
+
+ const handleAllDayToggle = React.useCallback(() => {
+ const newAllDay = !allday;
+ let newStart = start;
+ let newEnd = end;
+
+ if (newAllDay) {
+ // OFF => ON: Save original time AND original end date
+ if (start.includes("T")) {
+ originalTimeRef.current = {
+ start: startTime,
+ end: endTime,
+ endDate: endDate, // Save original end date
+ };
+ }
+
+ // Reset time to empty, keep only date part
+ setStartTime("");
+ setEndTime("");
+ newStart = startDate;
+ newEnd = endDate;
+
+ // If same day, extend end to next day
+ if (startDate === endDate) {
+ const nextDay = new Date(endDate);
+ nextDay.setDate(nextDay.getDate() + 1);
+ newEnd = nextDay.toISOString().split("T")[0];
+ setEndDate(newEnd); // Update internal endDate state
+ }
+ } else {
+ // ON => OFF: Restore original time AND original end date
+ if (originalTimeRef.current) {
+ // Check if this came from all-day slot click
+ if (originalTimeRef.current.fromAllDaySlot) {
+ // From all-day slot: set endDate = startDate, use default time
+ const currentTime = getRoundedCurrentTime();
+ const hours = String(currentTime.getHours()).padStart(2, "0");
+ const minutes = String(currentTime.getMinutes()).padStart(2, "0");
+ const timeStr = `${hours}:${minutes}`;
+
+ newStart = combineDateTime(startDate, timeStr);
+
+ // End time = start time + 1 hour
+ const endTimeDate = new Date(currentTime);
+ endTimeDate.setHours(endTimeDate.getHours() + 1);
+ const endHours = String(endTimeDate.getHours()).padStart(2, "0");
+ const endMinutes = String(endTimeDate.getMinutes()).padStart(2, "0");
+ const endTimeStr = `${endHours}:${endMinutes}`;
+
+ newEnd = combineDateTime(startDate, endTimeStr); // Use startDate as endDate
+
+ // Update internal states
+ setStartTime(timeStr);
+ setEndTime(endTimeStr);
+ setEndDate(startDate); // Set endDate = startDate
+ } else {
+ // Normal case: restore original time AND original end date
+ const restoredEndDate = originalTimeRef.current.endDate || endDate;
+
+ newStart = combineDateTime(startDate, originalTimeRef.current.start);
+ newEnd = combineDateTime(
+ restoredEndDate,
+ originalTimeRef.current.end
+ );
+
+ // Update internal states
+ setStartTime(originalTimeRef.current.start);
+ setEndTime(originalTimeRef.current.end);
+ setEndDate(restoredEndDate);
+ }
+
+ originalTimeRef.current = null;
+ } else {
+ // No original time: use rounded current time with 1 hour duration
+ const currentTime = getRoundedCurrentTime();
+ const hours = String(currentTime.getHours()).padStart(2, "0");
+ const minutes = String(currentTime.getMinutes()).padStart(2, "0");
+ const timeStr = `${hours}:${minutes}`;
+
+ newStart = combineDateTime(startDate, timeStr);
+
+ // End time = start time + 1 hour (not 30 mins)
+ const endTimeDate = new Date(currentTime);
+ endTimeDate.setHours(endTimeDate.getHours() + 1);
+ const endHours = String(endTimeDate.getHours()).padStart(2, "0");
+ const endMinutes = String(endTimeDate.getMinutes()).padStart(2, "0");
+ const endTimeStr = `${endHours}:${endMinutes}`;
+
+ newEnd = combineDateTime(endDate, endTimeStr);
+
+ // Update internal states
+ setStartTime(timeStr);
+ setEndTime(endTimeStr);
+ }
+ }
+
+ if (!onAllDayChange) {
+ setStart(newStart);
+ setEnd(newEnd);
+ setAllDay(newAllDay);
+ } else {
+ onAllDayChange(newAllDay, newStart, newEnd);
+ }
+ }, [
+ allday,
+ start,
+ end,
+ startDate,
+ startTime,
+ endDate,
+ endTime,
+ setStartTime,
+ setEndTime,
+ setEndDate,
+ setStart,
+ setEnd,
+ setAllDay,
+ onAllDayChange,
+ ]);
+
+ return {
+ originalTimeRef,
+ handleAllDayToggle,
+ };
+}
diff --git a/src/components/Event/utils/dateTimeFormatters.ts b/src/components/Event/utils/dateTimeFormatters.ts
new file mode 100644
index 0000000..cba91a5
--- /dev/null
+++ b/src/components/Event/utils/dateTimeFormatters.ts
@@ -0,0 +1,80 @@
+/**
+ * Date/time formatting utilities
+ */
+
+/**
+ * Format a Date object to local datetime string (YYYY-MM-DDTHH:mm)
+ * @param date - Date object to format
+ * @param timeZone - Optional timezone for formatting
+ * @returns Formatted datetime string
+ */
+export function formatLocalDateTime(date: Date, timeZone?: string): string {
+ // Guard against invalid or undefined dates
+ if (!date || isNaN(date.getTime())) {
+ return "";
+ }
+
+ if (timeZone) {
+ const formatter = new Intl.DateTimeFormat("en-CA", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
+ timeZone,
+ });
+ const formatted = formatter.format(date);
+ return formatted.replace(", ", "T");
+ }
+
+ const pad = (n: number) => n.toString().padStart(2, "0");
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
+ date.getDate()
+ )}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
+}
+
+/**
+ * Format an ISO datetime string in a specific timezone
+ * @param isoString - ISO datetime string
+ * @param timezone - Target timezone
+ * @returns Formatted datetime string in target timezone
+ */
+export function formatDateTimeInTimezone(
+ isoString: string,
+ timezone: string
+): string {
+ // Parse the ISO string as UTC
+ const utcDate = new Date(isoString);
+
+ // Format the date in the target timezone
+ const formatter = new Intl.DateTimeFormat("en-CA", {
+ timeZone: timezone,
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
+ });
+
+ const parts = formatter.formatToParts(utcDate);
+ const getValue = (type: string) =>
+ parts.find((p) => p.type === type)?.value || "";
+
+ return `${getValue("year")}-${getValue("month")}-${getValue("day")}T${getValue("hour")}:${getValue("minute")}`;
+}
+
+/**
+ * Get current time rounded to nearest 30 minutes
+ * @returns Rounded Date object
+ */
+export function getRoundedCurrentTime(): Date {
+ const now = new Date();
+ const minutes = now.getMinutes();
+ const roundedMinutes = minutes < 30 ? 0 : 30;
+ now.setMinutes(roundedMinutes);
+ now.setSeconds(0);
+ now.setMilliseconds(0);
+ return now;
+}
diff --git a/src/components/Event/utils/dateTimeHelpers.ts b/src/components/Event/utils/dateTimeHelpers.ts
new file mode 100644
index 0000000..78454d7
--- /dev/null
+++ b/src/components/Event/utils/dateTimeHelpers.ts
@@ -0,0 +1,32 @@
+/**
+ * Helper functions for date/time string manipulation
+ */
+
+/**
+ * Split datetime string (YYYY-MM-DDTHH:mm) into date and time parts
+ * @param datetime - ISO datetime string
+ * @returns Object with date and time strings
+ */
+export function splitDateTime(datetime: string): {
+ date: string;
+ time: string;
+} {
+ if (!datetime) return { date: "", time: "" };
+ const parts = datetime.split("T");
+ return {
+ date: parts[0] || "",
+ time: parts[1]?.slice(0, 5) || "", // HH:mm only
+ };
+}
+
+/**
+ * Combine date and time strings into datetime string
+ * @param date - Date string (YYYY-MM-DD)
+ * @param time - Time string (HH:mm)
+ * @returns Combined datetime string or date only if no time
+ */
+export function combineDateTime(date: string, time: string): string {
+ if (!date) return "";
+ if (!time) return date; // Date only for all-day
+ return `${date}T${time}`;
+}
diff --git a/src/components/Event/utils/formValidation.ts b/src/components/Event/utils/formValidation.ts
new file mode 100644
index 0000000..ad3f370
--- /dev/null
+++ b/src/components/Event/utils/formValidation.ts
@@ -0,0 +1,96 @@
+import { combineDateTime } from "./dateTimeHelpers";
+
+/**
+ * Validation parameters for event form
+ */
+export interface ValidationParams {
+ title: string;
+ startDate: string;
+ startTime: string;
+ endDate: string;
+ endTime: string;
+ allday: boolean;
+ showValidationErrors: boolean;
+}
+
+/**
+ * Validation result for event form
+ */
+export interface ValidationResult {
+ isValid: boolean;
+ errors: {
+ title: string;
+ dateTime: string;
+ };
+}
+
+/**
+ * Validate event form fields
+ * @param params - Validation parameters
+ * @returns Validation result with errors
+ */
+export function validateEventForm(params: ValidationParams): ValidationResult {
+ const {
+ title,
+ startDate,
+ startTime,
+ endDate,
+ endTime,
+ allday,
+ showValidationErrors,
+ } = params;
+
+ const isTitleValid = title.trim().length > 0;
+ const shouldShowTitleError = showValidationErrors && !isTitleValid;
+
+ let isDateTimeValid = true;
+ let dateTimeError = "";
+
+ // Validate start date
+ if (!startDate || startDate.trim() === "") {
+ isDateTimeValid = false;
+ dateTimeError = "Start date is required";
+ }
+ // Validate start time (if not all-day)
+ else if (!allday && (!startTime || startTime.trim() === "")) {
+ isDateTimeValid = false;
+ dateTimeError = "Start time is required";
+ }
+ // Validate end date
+ else if (!endDate || endDate.trim() === "") {
+ isDateTimeValid = false;
+ dateTimeError = "End date is required";
+ }
+ // Validate end time (if not all-day)
+ else if (!allday && (!endTime || endTime.trim() === "")) {
+ isDateTimeValid = false;
+ dateTimeError = "End time is required";
+ }
+ // Validate end > start
+ else {
+ const startDateTime = allday
+ ? new Date(startDate + "T00:00:00")
+ : new Date(combineDateTime(startDate, startTime));
+ const endDateTime = allday
+ ? new Date(endDate + "T00:00:00")
+ : new Date(combineDateTime(endDate, endTime));
+
+ if (isNaN(startDateTime.getTime()) || isNaN(endDateTime.getTime())) {
+ isDateTimeValid = false;
+ dateTimeError = "Invalid date/time";
+ } else if (endDateTime <= startDateTime) {
+ isDateTimeValid = false;
+ dateTimeError = "End time must be after start time";
+ }
+ }
+
+ const isValid = isTitleValid && isDateTimeValid;
+
+ return {
+ isValid,
+ errors: {
+ title: shouldShowTitleError ? "Title is required" : "",
+ dateTime: showValidationErrors ? dateTimeError : "",
+ },
+ };
+}
diff --git a/src/features/Events/EventDisplay.tsx b/src/features/Events/EventDisplay.tsx
index 9c889b8..37ec911 100644
--- a/src/features/Events/EventDisplay.tsx
+++ b/src/features/Events/EventDisplay.tsx
@@ -46,7 +46,7 @@ import {
import { Calendars } from "../Calendars/CalendarTypes";
import { userAttendee } from "../User/userDataTypes";
import { getEvent } from "./EventApi";
-import { formatLocalDateTime } from "../../components/Event/EventFormFields";
+import { formatLocalDateTime } from "../../components/Event/utils/dateTimeFormatters";
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
export default function EventDisplayModal({
diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx
index ee794cc..402f22a 100644
--- a/src/features/Events/EventModal.tsx
+++ b/src/features/Events/EventModal.tsx
@@ -24,10 +24,11 @@ import {
} from "../../components/Calendar/TimezoneSelector";
import { getCalendarRange } from "../../utils/dateUtils";
import { updateTempCalendar } from "../../components/Calendar/utils/calendarUtils";
-import EventFormFields, {
+import EventFormFields from "../../components/Event/EventFormFields";
+import {
formatLocalDateTime,
formatDateTimeInTimezone,
-} from "../../components/Event/EventFormFields";
+} from "../../components/Event/utils/dateTimeFormatters";
function EventPopover({
anchorEl,
diff --git a/src/features/Events/EventUpdateModal.tsx b/src/features/Events/EventUpdateModal.tsx
index 5ef98dc..52cc63a 100644
--- a/src/features/Events/EventUpdateModal.tsx
+++ b/src/features/Events/EventUpdateModal.tsx
@@ -17,9 +17,8 @@ import { userAttendee } from "../User/userDataTypes";
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
import { TIMEZONES } from "../../utils/timezone-data";
import { addVideoConferenceToDescription } from "../../utils/videoConferenceUtils";
-import EventFormFields, {
- formatDateTimeInTimezone,
-} from "../../components/Event/EventFormFields";
+import EventFormFields from "../../components/Event/EventFormFields";
+import { formatDateTimeInTimezone } from "../../components/Event/utils/dateTimeFormatters";
import { getEvent, deleteEvent, putEvent } from "./EventApi";
import { refreshCalendars } from "../../components/Event/utils/eventUtils";
import { getCalendarRange } from "../../utils/dateUtils";