Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 36 additions & 15 deletions src/app/(protected)/tracking/TrackingClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export default function TrackingClient() {
const [activeCourseKey, setActiveCourseKey] = useState<string | null>(null);
const [showPinnedCourse, setShowPinnedCourse] = useState(false);
const courseHeaderRefs = useRef<Record<string, HTMLDivElement | null>>({});
const lastActiveCourseKeyRef = useRef<string | null>(null);
const lastShowPinnedCourseRef = useRef(false);

// Per-course record limits (for performance with 100+ records)
const [expandedCourses, setExpandedCourses] = useState<Set<string>>(new Set());
Expand Down Expand Up @@ -189,13 +191,14 @@ export default function TrackingClient() {
const items = groupedAllData[activeCourseKey];
if (!items?.length) return null;

const courseKey = activeCourseKey;
const displayCourseName =
attendanceData?.courses?.[items[0].course]?.name ||
coursesData?.courses?.[items[0].course]?.name ||
activeCourseKey;
attendanceData?.courses?.[courseKey]?.name ||
coursesData?.courses?.[courseKey]?.name ||
courseKey;
const courseCode = (
attendanceData?.courses?.[items[0].course]?.code ??
coursesData?.courses?.[items[0].course]?.code ??
attendanceData?.courses?.[courseKey]?.code ??
coursesData?.courses?.[courseKey]?.code ??
""
).toUpperCase();

Expand Down Expand Up @@ -318,22 +321,40 @@ export default function TrackingClient() {
}
}

if (lastCrossedHeader) {
setActiveCourseKey(lastCrossedHeader);
setShowPinnedCourse(true);
} else {
setActiveCourseKey(null);
setShowPinnedCourse(false);
const newKey = lastCrossedHeader;
const newShow = lastCrossedHeader !== null;

// Only call setState when the value actually changed to avoid unnecessary
// React re-renders on every scroll/resize event.
if (newKey !== lastActiveCourseKeyRef.current) {
lastActiveCourseKeyRef.current = newKey;
setActiveCourseKey(newKey);
}
if (newShow !== lastShowPinnedCourseRef.current) {
lastShowPinnedCourseRef.current = newShow;
setShowPinnedCourse(newShow);
}
};

// Throttle via requestAnimationFrame so at most one DOM read + setState
// pair runs per frame during rapid scroll/resize bursts.
let rafId: ReturnType<typeof requestAnimationFrame> | null = null;
const scheduleUpdate = () => {
if (rafId !== null) return;
rafId = requestAnimationFrame(() => {
rafId = null;
updateActiveCourse();
});
};

updateActiveCourse();
window.addEventListener("scroll", updateActiveCourse, { passive: true });
window.addEventListener("resize", updateActiveCourse);
window.addEventListener("scroll", scheduleUpdate, { passive: true });
window.addEventListener("resize", scheduleUpdate);

return () => {
window.removeEventListener("scroll", updateActiveCourse);
window.removeEventListener("resize", updateActiveCourse);
window.removeEventListener("scroll", scheduleUpdate);
window.removeEventListener("resize", scheduleUpdate);
if (rafId !== null) cancelAnimationFrame(rafId);
};
}, [currentCourseKeys]);

Expand Down
19 changes: 16 additions & 3 deletions src/hooks/useBackToExit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,24 @@ export function useBackToExit(): void {
toast.dismiss(toastIdRef.current);
}

toastIdRef.current = toast("Press back again to exit", {
// Capture the id in a closure so that if this toast is dismissed before
// its callbacks fire (e.g. during an exit animation after a rapid
// dismiss+create cycle), the stale callback won't clear state that
// already belongs to the newer, active toast.
let newToastId: ReturnType<typeof toast> | null = null;
const handleClear = () => {
if (toastIdRef.current === newToastId) {
clearState();
}
};

newToastId = toast("Press back again to exit", {
duration: THRESHOLD_MS,
onDismiss: clearState,
onAutoClose: clearState,
onDismiss: handleClear,
onAutoClose: handleClear,
});

toastIdRef.current = newToastId;
};

const handlePopState = (event: PopStateEvent) => {
Expand Down