diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index a54a946..1174bf8 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -84,18 +84,15 @@ export default function App() { const { colorScheme, setColorScheme } = useColorScheme(); - if (storage.getString("colorScheme")) { - Appearance.setColorScheme(storage.getString("colorScheme") as ColorSchemeName); - setColorScheme(storage.getString("colorScheme") as ColorSchemeName); - } else { - if (Appearance.getColorScheme()) { - storage.set("colorScheme", Appearance.getColorScheme()); - setColorScheme(Appearance.getColorScheme()); - } else { - storage.set("colorScheme", "light"); - setColorScheme("light"); - } - } + useEffect(() => { + const savedScheme = storage.getString("colorScheme"); + const scheme = savedScheme ?? Appearance.getColorScheme() ?? "light"; + + if (!savedScheme) storage.set("colorScheme", scheme); + + Appearance.setColorScheme(scheme as ColorSchemeName); + setColorScheme(scheme as ColorSchemeName); + }, [setColorScheme]); if (storage.getString("passingPeriods") == "undefined") storage.set("passingPeriods", "true"); @@ -110,7 +107,7 @@ export default function App() { return ( <> - + { - if (colorScheme == "dark") { - setColorScheme("light"); - storage.set("colorScheme", "light"); - } else { - setColorScheme("dark"); - storage.set("colorScheme", "dark"); - } + const nextScheme = colorScheme == "dark" ? "light" : "dark"; + + storage.set("colorScheme", nextScheme); + Appearance.setColorScheme(nextScheme); + setColorScheme(nextScheme); WidgetUpdaterModule.update(); impactAsync(); diff --git a/apps/mobile/src/components/settings/AddEventModal.tsx b/apps/mobile/src/components/settings/AddEventModal.tsx index 9de971d..453226a 100644 --- a/apps/mobile/src/components/settings/AddEventModal.tsx +++ b/apps/mobile/src/components/settings/AddEventModal.tsx @@ -14,6 +14,7 @@ import { } from "react-native"; import DateTimePickerModal from "react-native-modal-datetime-picker"; +import { findEventConflict } from "@/src/utils/eventValidation"; import storage from "@/src/utils/storage"; import TextModal from "./TextModal"; @@ -43,16 +44,6 @@ function createCustomDate(timestamp: number) { return timeString; } -function createCustomTime(inputTime: string) { - const currentDate = new Date(); - - const [inputHourRaw, inputMinuteRaw] = inputTime.split(":"); - const inputMinute = inputMinuteRaw.replace(/[A-Za-z]/g, ""); // Remove any non-numeric characters - - currentDate.setHours(parseInt(inputHourRaw), parseInt(inputMinute), 0, 0); - return currentDate.getTime(); -} - function sortByStartTime(array: UnparsedEvent[]) { return array.sort((a, b) => { const startTimeA = a.startTime.split(":").map(Number); @@ -66,35 +57,6 @@ function sortByStartTime(array: UnparsedEvent[]) { }); } -function areEventsValid(events: UnparsedEvent[]) { - if (events.length <= 1) { - return true; // Single event is always valid - } - - for (let i = 0; i < events.length; i++) { - const currentEvent = events[i]; - - const startTime = createCustomTime(currentEvent.startTime); - const endTime = createCustomTime(currentEvent.endTime); - - if (startTime >= endTime) { - return false; // End time is not after start time - } - - if (i < events.length - 1) { - // Check for event overlap - const nextEvent = events[i + 1]; - const nextStartTime = createCustomTime(nextEvent.startTime); - - if (endTime > nextStartTime) { - return false; // Events overlap - } - } - } - - return true; // All events are valid -} - function addEvent( schedule: UnparsedSchedule, setSchedule: React.Dispatch>, @@ -116,15 +78,15 @@ function addEvent( newSchedule["routines"][currentRoutine]["events"], ); - if (!areEventsValid(newSchedule["routines"][currentRoutine]["events"])) { - return Alert.alert( - "Error", - "This event overlaps with another event or has an invalid start/end time.", - ); - } else { - setSchedule(newSchedule); - storage.set("currentSchedule", JSON.stringify(newSchedule)); + const conflict = findEventConflict(newSchedule["routines"][currentRoutine]["events"]); + if (conflict) { + Alert.alert("Error", conflict); + return false; } + + setSchedule(newSchedule); + storage.set("currentSchedule", JSON.stringify(newSchedule)); + return true; } export default function AddEventModal(props: { @@ -286,7 +248,7 @@ export default function AddEventModal(props: { accessibilityLabel="Finish" className="mt-3 bg-wedgewood-300 rounded shadow-xl p-4 border-2 border-wedgewood-400 active:bg-wedgewood-500 dark:active:bg-wedgewood-800 flex flex-row items-center justify-center dark:bg-wedgewood-950 dark:border-wedgewood-600" onPress={() => { - addEvent( + const added = addEvent( props.scheduleDB, props.setScheduleDB, props.currentRoutine, @@ -294,7 +256,8 @@ export default function AddEventModal(props: { createCustomDate(startTime.getTime()), createCustomDate(endTime.getTime()), ); - props.setModalVisible(false); + + if (added) props.setModalVisible(false); }} > diff --git a/apps/mobile/src/components/settings/AddRoutineModal.tsx b/apps/mobile/src/components/settings/AddRoutineModal.tsx index 18db31a..59d8254 100644 --- a/apps/mobile/src/components/settings/AddRoutineModal.tsx +++ b/apps/mobile/src/components/settings/AddRoutineModal.tsx @@ -29,14 +29,18 @@ function createNewRoutine( name: string, weekdays: number[], ) { - if (!name) return Alert.alert("Error", "You must provide a name."); - - const newSchedule = { ...schedule }; + if (!name) { + Alert.alert("Error", "You must provide a name."); + return false; + } - if (newSchedule["routines"][name]) { - return Alert.alert("Error", "Routine with same name already exists."); + if (schedule["routines"][name]) { + Alert.alert("Error", `A routine named "${name}" already exists. Pick a different name.`); + return false; } + const newSchedule = JSON.parse(JSON.stringify(schedule)) as UnparsedSchedule; + newSchedule["routines"][name] = { officialName: name, days: weekdays, @@ -55,6 +59,7 @@ function createNewRoutine( storage.set("currentSchedule", JSON.stringify(newSchedule)); Alert.alert("Success", "Successfully created a new routine."); + return true; } export default function AddRoutineModal(props: { @@ -138,7 +143,17 @@ export default function AddRoutineModal(props: { accessibilityLabel="Finish" className="mt-4 bg-wedgewood-300 rounded shadow-xl p-4 border-2 border-wedgewood-400 active:bg-wedgewood-500 dark:active:bg-wedgewood-800 flex flex-row items-center justify-center dark:bg-wedgewood-950 dark:border-wedgewood-600" onPress={() => { - createNewRoutine(props.scheduleDB, props.setScheduleDB, name, weekdays); + const created = createNewRoutine( + props.scheduleDB, + props.setScheduleDB, + name, + weekdays, + ); + + if (!created) return; + + setName("New Routine!"); + setWeekdays([]); props.setModalVisible(false); }} > diff --git a/apps/mobile/src/components/settings/EventModal.tsx b/apps/mobile/src/components/settings/EventModal.tsx index 840e49f..a663266 100644 --- a/apps/mobile/src/components/settings/EventModal.tsx +++ b/apps/mobile/src/components/settings/EventModal.tsx @@ -14,6 +14,7 @@ import { } from "react-native"; import DateTimePickerModal from "react-native-modal-datetime-picker"; +import { findEventConflict } from "@/src/utils/eventValidation"; import storage from "@/src/utils/storage"; import TextModal from "./TextModal"; @@ -56,45 +57,6 @@ function createCustomDateString(timestamp: number) { return timeString; } -function createCustomTime(inputTime: string) { - const currentDate = new Date(); - - const [inputHourRaw, inputMinuteRaw] = inputTime.split(":"); - const inputMinute = inputMinuteRaw.replace(/[A-Za-z]/g, ""); // Remove any non-numeric characters - - currentDate.setHours(parseInt(inputHourRaw), parseInt(inputMinute), 0, 0); - return currentDate.getTime(); -} - -function areEventsValid(events: UnparsedEvent[]) { - if (events.length <= 1) { - return true; // Single event is always valid - } - - for (let i = 0; i < events.length; i++) { - const currentEvent = events[i]; - - const startTime = createCustomTime(currentEvent.startTime); - const endTime = createCustomTime(currentEvent.endTime); - - if (startTime >= endTime) { - return false; // End time is not after start time - } - - if (i < events.length - 1) { - // Check for event overlap - const nextEvent = events[i + 1]; - const nextStartTime = createCustomTime(nextEvent.startTime); - - if (endTime > nextStartTime) { - return false; // Events overlap - } - } - } - - return true; // All events are valid -} - function modifyEvent( schedule: UnparsedSchedule, setSchedule: React.Dispatch>, @@ -127,15 +89,15 @@ function modifyEventTimes( newSchedule["routines"][currentRoutine]["events"], ); - if (!areEventsValid(newSchedule["routines"][currentRoutine]["events"])) { - return Alert.alert( - "Error", - "This event overlaps with another event or has an invalid start/end time.", - ); + const conflict = findEventConflict(newSchedule["routines"][currentRoutine]["events"]); + if (conflict) { + Alert.alert("Error", conflict); + return false; } setSchedule(newSchedule); storage.set("currentSchedule", JSON.stringify(newSchedule)); + return true; } function removeEvent( @@ -420,10 +382,7 @@ export default function EventModal(props: { return Alert.alert("Error", "End time must be after start time."); } - props.setStartTime(updatedStartTime); - props.setEndTime(updatedEndTime); - - modifyEventTimes( + const saved = modifyEventTimes( props.scheduleDB, props.setScheduleDB, props.currentRoutine, @@ -432,6 +391,10 @@ export default function EventModal(props: { createCustomDateString(updatedEndTime.getTime()), ); + if (!saved) return; + + props.setStartTime(updatedStartTime); + props.setEndTime(updatedEndTime); props.setModalVisible(false); }} > diff --git a/apps/mobile/src/utils/eventValidation.ts b/apps/mobile/src/utils/eventValidation.ts new file mode 100644 index 0000000..9949234 --- /dev/null +++ b/apps/mobile/src/utils/eventValidation.ts @@ -0,0 +1,28 @@ +import { UnparsedEvent } from "@scheduli/types"; + +function toTimestamp(inputTime: string) { + const date = new Date(); + + const [hourRaw, minuteRaw] = inputTime.split(":"); + const minute = minuteRaw.replace(/[A-Za-z]/g, ""); + + date.setHours(parseInt(hourRaw), parseInt(minute), 0, 0); + return date.getTime(); +} + +export function findEventConflict(events: UnparsedEvent[]) { + for (let i = 0; i < events.length; i++) { + const current = events[i]; + + if (toTimestamp(current.startTime) >= toTimestamp(current.endTime)) { + return `"${current.name}" has to end after it starts.`; + } + + const next = events[i + 1]; + if (next && toTimestamp(current.endTime) > toTimestamp(next.startTime)) { + return `"${current.name}" overlaps with "${next.name}". Change one of their times so they don't run at the same time.`; + } + } + + return null; +}