Skip to content

Commit 3b18228

Browse files
committed
feat: implement responsive design for BuildSchedule
1 parent 251e622 commit 3b18228

2 files changed

Lines changed: 209 additions & 12 deletions

File tree

src/containers/BuildSchedule/index.js

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { getCourses, getCoursesByKd } from "services/api";
2020
import { clearSchedule } from "redux/modules/schedules";
2121
import SelectedCourses from "containers/SelectedCourses";
2222
import { BauhausSide } from "components/Bauhaus";
23+
import { useSchedulePersistence } from "hooks/useSchedulePersistence"; // Import the custom hook
2324
import Checkout from "./Checkout";
2425
import Course from "./Course";
2526
import Detail from "./Detail";
@@ -39,6 +40,10 @@ function BuildSchedule() {
3940
const isMobile = useSelector((state) => state.appState.isMobile);
4041
const auth = useSelector((state) => state.auth);
4142
const dispatch = useDispatch();
43+
44+
const { saveSchedulesToSessionStorage, restoreSchedulesFromSessionStorage } =
45+
useSchedulePersistence();
46+
4247
const [majorSelected, setMajorSelected] = useState();
4348
const [detailData, setDetailData] = useState(null);
4449
const [courses, setCourses] = useState(null);
@@ -50,47 +55,105 @@ function BuildSchedule() {
5055
const theme = useColorModeValue("light", "dark");
5156
const isInitialMount = useRef(true);
5257

58+
const fetchedMajorId = useRef(null);
59+
const fetchedMajorSelected = useRef(null);
60+
const hasInitialData = useRef(false);
61+
const coursesLoaded = useRef(false);
62+
5363
const fetchCourses = useCallback(
54-
async (majorId, majorSelected) => {
64+
async (majorId, majorSelected, shouldClearSchedule = false) => {
65+
const isSameMajor = fetchedMajorId.current === majorId;
66+
const isSameMajorSelected =
67+
fetchedMajorSelected.current === majorSelected;
68+
69+
if (isSameMajor && isSameMajorSelected && hasInitialData.current) {
70+
return;
71+
}
72+
5573
dispatch(setLoading(true));
5674

5775
try {
5876
const { data } = majorSelected
5977
? await getCoursesByKd(majorSelected.kd_org)
6078
: await getCourses(majorId);
6179

62-
if (!majorSelected) {
80+
if (shouldClearSchedule && coursesLoaded.current) {
81+
saveSchedulesToSessionStorage();
6382
dispatch(clearSchedule());
6483
}
6584

6685
setCourses(data.courses);
6786
setCoursesDetail(data.is_detail);
6887
setLastUpdated(new Date(data.last_update_at));
6988
dispatch(reduxSetCourses(data.courses));
89+
90+
fetchedMajorId.current = majorId;
91+
fetchedMajorSelected.current = majorSelected;
92+
hasInitialData.current = true;
93+
coursesLoaded.current = true;
94+
95+
if (!shouldClearSchedule && data.courses) {
96+
setTimeout(() => {
97+
restoreSchedulesFromSessionStorage();
98+
}, 100);
99+
}
70100
} catch (e) {
71-
/** TODO: handle error */
101+
console.error("Error fetching courses:", e);
72102
}
73103

74104
setTimeout(() => dispatch(setLoading(false)), 1000);
75105
},
76-
[dispatch],
106+
[
107+
dispatch,
108+
saveSchedulesToSessionStorage,
109+
restoreSchedulesFromSessionStorage,
110+
],
77111
);
78112

79113
useEffect(() => {
80-
document.getElementById("input").value = "";
81-
setValue("");
82-
const majorId = auth.majorId;
83-
fetchCourses(majorId, majorSelected);
84-
}, [auth.majorId, majorSelected, dispatch, fetchCourses, setValue]);
114+
if (!hasInitialData.current) {
115+
document.getElementById("input")?.value &&
116+
(document.getElementById("input").value = "");
117+
setValue("");
118+
const majorId = auth.majorId;
119+
const restored = restoreSchedulesFromSessionStorage();
120+
if (!restored) {
121+
fetchCourses(majorId, majorSelected, false);
122+
} else {
123+
fetchCourses(majorId, majorSelected, false);
124+
}
125+
}
126+
}, [auth.majorId, fetchCourses, restoreSchedulesFromSessionStorage]);
127+
128+
useEffect(() => {
129+
if (
130+
hasInitialData.current &&
131+
majorSelected !== fetchedMajorSelected.current
132+
) {
133+
document.getElementById("input")?.value &&
134+
(document.getElementById("input").value = "");
135+
setValue("");
136+
const majorId = auth.majorId;
137+
138+
fetchCourses(majorId, majorSelected, true);
139+
}
140+
}, [majorSelected, auth.majorId, fetchCourses]);
141+
142+
useEffect(() => {
143+
const handleFocus = () => {
144+
if (hasInitialData.current && courses) {
145+
restoreSchedulesFromSessionStorage();
146+
}
147+
};
85148

86-
// const handleChange = (e) => setValueTemporary(e.target.value);
149+
window.addEventListener("focus", handleFocus);
150+
return () => window.removeEventListener("focus", handleFocus);
151+
}, [courses, restoreSchedulesFromSessionStorage]);
87152

88153
let filteredCourse = courses?.filter((c) => {
89154
if (value === "") {
90-
//if value is empty
91155
return c;
92156
} else if (c.name.toLowerCase().includes(value.toLowerCase())) {
93-
//returns filtered array
94157
return c;
95158
} else {
96159
return null;
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { useEffect, useRef, useCallback } from "react";
2+
import { useSelector, useDispatch } from "react-redux";
3+
4+
/**
5+
* Custom hook to prevent schedule reset during component re-renders
6+
* caused by window resize, responsive changes, etc.
7+
*/
8+
export const useSchedulePersistence = () => {
9+
const schedules = useSelector((state) => state.schedules);
10+
const courses = useSelector((state) => state.courses);
11+
const dispatch = useDispatch();
12+
13+
const prevSchedules = useRef([]);
14+
const prevCourses = useRef({});
15+
const hasRestoredSchedules = useRef(false);
16+
17+
useEffect(() => {
18+
if (
19+
schedules.length === 0 &&
20+
prevSchedules.current.length > 0 &&
21+
Object.keys(courses).length > 0 &&
22+
!hasRestoredSchedules.current
23+
) {
24+
prevSchedules.current.forEach((schedule) => {
25+
const key = `${schedule.parentName}-${schedule.name}-${schedule.term}-${schedule.schedule_items[0].room}`;
26+
27+
if (courses[key] !== undefined) {
28+
dispatch({
29+
type: "schedules/addSchedule",
30+
payload: schedule,
31+
});
32+
33+
dispatch({
34+
type: "courses/setCourseSelected",
35+
payload: { key, selected: true },
36+
});
37+
}
38+
});
39+
40+
hasRestoredSchedules.current = true;
41+
42+
setTimeout(() => {
43+
hasRestoredSchedules.current = false;
44+
}, 1000);
45+
}
46+
47+
if (schedules.length > 0) {
48+
prevSchedules.current = [...schedules];
49+
}
50+
51+
if (Object.keys(courses).length > 0) {
52+
prevCourses.current = { ...courses };
53+
}
54+
}, [schedules, courses, dispatch]);
55+
56+
const saveSchedulesToSessionStorage = useCallback(() => {
57+
if (schedules.length > 0) {
58+
try {
59+
sessionStorage.setItem(
60+
"siak_war_schedules_backup",
61+
JSON.stringify({
62+
schedules: schedules,
63+
courses: courses,
64+
timestamp: Date.now(),
65+
}),
66+
);
67+
} catch (e) {
68+
console.warn("Could not save schedules to sessionStorage:", e);
69+
}
70+
}
71+
}, [schedules, courses]);
72+
73+
const restoreSchedulesFromSessionStorage = useCallback(() => {
74+
try {
75+
const backup = sessionStorage.getItem("siak_war_schedules_backup");
76+
if (backup) {
77+
const {
78+
schedules: savedSchedules,
79+
courses: savedCourses,
80+
timestamp,
81+
} = JSON.parse(backup);
82+
83+
if (Date.now() - timestamp < 3600000) {
84+
savedSchedules.forEach((schedule) => {
85+
dispatch({
86+
type: "schedules/addSchedule",
87+
payload: schedule,
88+
});
89+
});
90+
91+
Object.keys(savedCourses).forEach((key) => {
92+
if (savedCourses[key]) {
93+
dispatch({
94+
type: "courses/setCourseSelected",
95+
payload: { key, selected: true },
96+
});
97+
}
98+
});
99+
100+
return true;
101+
}
102+
}
103+
} catch (e) {
104+
console.warn("Could not restore schedules from sessionStorage:", e);
105+
}
106+
return false;
107+
}, [dispatch]);
108+
109+
useEffect(() => {
110+
const interval = setInterval(() => {
111+
if (schedules.length > 0) {
112+
saveSchedulesToSessionStorage();
113+
}
114+
}, 5000);
115+
116+
return () => clearInterval(interval);
117+
}, [schedules, saveSchedulesToSessionStorage]);
118+
119+
useEffect(() => {
120+
const handleBeforeUnload = () => {
121+
saveSchedulesToSessionStorage();
122+
};
123+
124+
window.addEventListener("beforeunload", handleBeforeUnload);
125+
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
126+
}, [saveSchedulesToSessionStorage]);
127+
128+
return {
129+
saveSchedulesToSessionStorage,
130+
restoreSchedulesFromSessionStorage,
131+
hasSchedules: schedules.length > 0,
132+
schedulesCount: schedules.length,
133+
};
134+
};

0 commit comments

Comments
 (0)