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;
+}