diff --git a/app/(pages)/(hackers)/(hub)/schedule/page.tsx b/app/(pages)/(hackers)/(hub)/schedule/page.tsx index fe5de9b1..b3c6a740 100644 --- a/app/(pages)/(hackers)/(hub)/schedule/page.tsx +++ b/app/(pages)/(hackers)/(hub)/schedule/page.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect, useMemo, useRef } from 'react'; import CalendarItem from '../../_components/Schedule/CalendarItem'; import Footer from '@components/Footer/Footer'; import Image from 'next/image'; @@ -50,10 +50,16 @@ export default function Page() { const [activeFilters, setActiveFilters] = useState(['ALL']); const [isMobileFilterOpen, setIsMobileFilterOpen] = useState(false); const [scheduleData, setScheduleData] = useState(null); + const ignoreScrollSyncUntilRef = useRef(0); + const pendingDayRef = useRef<'9' | '10' | null>(null); const changeActiveDay = (day: '9' | '10') => { setActiveDay(day); - window.scrollTo({ top: 0, behavior: 'smooth' }); + pendingDayRef.current = day; + ignoreScrollSyncUntilRef.current = Date.now() + 2000; + document + .getElementById(`day-${day}`) + ?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }; const { @@ -212,13 +218,13 @@ export default function Page() { const dataToUse = activeTab === 'personal' ? personalScheduleData : scheduleData; - // Update the filtering logic to handle recommended events correctly - const sortedGroupedEntries = useMemo(() => { - if (!dataToUse) return []; - + const getGroupedEntriesForDay = ( + dayKey: '9' | '10', + dataToUse: ScheduleData | null, + activeFilters: ScheduleFilter[] + ): [string, EventDetails[]][] => { // Filter events for the active day - const eventsForDay = dataToUse[activeDay] || []; - + const eventsForDay = dataToUse?.[dayKey] ?? []; // Apply filter logic let filteredEvents = eventsForDay; @@ -241,6 +247,7 @@ export default function Page() { if (filteredEvents.length === 0) return []; // Sort the filtered events by start time. + // TODO: UPDATE THIS WITH MY CODE FROM MY OTHER TICKET const sortedEvents = [...filteredEvents].sort( (a, b) => new Date(a.event.start_time).getTime() - @@ -276,7 +283,62 @@ export default function Page() { const dateB = new Date(`${dummyDay} ${b[0]}`); return dateA.getTime() - dateB.getTime(); }); - }, [dataToUse, activeDay, activeFilters]); + }; + + // useMemo cache the result of an expensive calculation between re-renders, + // so it only runs when its dependencies change + const groupedEntriesByDay = useMemo(() => { + return { + '9': getGroupedEntriesForDay('9', dataToUse, activeFilters), + '10': getGroupedEntriesForDay('10', dataToUse, activeFilters), + }; + }, [dataToUse, activeFilters]); + + useEffect(() => { + const updateActiveDayFromScroll = () => { + if (Date.now() < ignoreScrollSyncUntilRef.current) { + if (pendingDayRef.current) { + setActiveDay(pendingDayRef.current); + } + return; + } + + pendingDayRef.current = null; + + const daySections = (['9', '10'] as const) + .map((day) => { + const section = document.getElementById(`day-${day}`); + return section + ? { day, rect: section.getBoundingClientRect() } + : null; + }) + .filter( + (section): section is { day: '9' | '10'; rect: DOMRect } => + section !== null + ); + + if (daySections.length === 0) return; + + // Flip active day when a section title reaches ~45% down viewport. + const anchor = window.innerHeight * 0.45; + let nextActiveDay: '9' | '10' = daySections[0].day; + for (const section of daySections) { + if (section.rect.top <= anchor) { + nextActiveDay = section.day; + } + } + + setActiveDay(nextActiveDay); + }; + + updateActiveDayFromScroll(); + window.addEventListener('scroll', updateActiveDayFromScroll, { + passive: true, + }); + return () => { + window.removeEventListener('scroll', updateActiveDayFromScroll); + }; + }, [activeTab, activeFilters, groupedEntriesByDay]); const toggleFilter = (label: ScheduleFilter) => { if (label === 'ALL') { @@ -311,15 +373,18 @@ export default function Page() { ); return ( -
-
+
+
header-grass
-
+
@@ -391,89 +456,120 @@ export default function Page() {
-
+
{isInitialLoad ? (

loading...

- ) : sortedGroupedEntries.length > 0 ? ( - sortedGroupedEntries.map(([timeKey, events]) => ( -
-
- {timeKey} -
-
- {events.map((eventDetail) => ( - - handleAddToSchedule(eventDetail.event._id || '') - } - onRemoveFromSchedule={() => - handleRemoveFromSchedule(eventDetail.event._id || '') - } - /> - ))} -
-
- )) ) : ( - isInitialLoad && ( -
- {activeTab === 'personal' ? ( -
-

- No events in your personal schedule yet. -

- + (['9', '10'] as const).map((dayKey) => { + const dayEntries = groupedEntriesByDay[dayKey]; + const dayTitle = dayKey === '9' ? 'May 9' : 'May 10'; + + return ( +
+
+ {dayTitle}
- ) : ( - 'No events found for this day and filter(s).' - )} -
- ) +
+ + {dayEntries.length > 0 ? ( + dayEntries.map(([timeKey, events]) => ( +
+
+ {timeKey} +
+
+ {events.map((eventDetail) => ( + + handleAddToSchedule(eventDetail.event._id || '') + } + onRemoveFromSchedule={() => + handleRemoveFromSchedule( + eventDetail.event._id || '' + ) + } + /> + ))} +
+
+ )) + ) : ( +
+ {activeTab === 'personal' ? ( +
+

+ No events in your personal schedule yet. +

+ +
+ ) : ( + 'No events found for this day and filter(s).' + )} +
+ )} + + ); + }) )}
diff --git a/app/(pages)/(hackers)/_components/Schedule/ScheduleMobileControls.tsx b/app/(pages)/(hackers)/_components/Schedule/ScheduleMobileControls.tsx index 33f27240..b8a2ce9e 100644 --- a/app/(pages)/(hackers)/_components/Schedule/ScheduleMobileControls.tsx +++ b/app/(pages)/(hackers)/_components/Schedule/ScheduleMobileControls.tsx @@ -45,16 +45,19 @@ export default function ScheduleMobileControls({ ); @@ -113,8 +116,8 @@ export default function ScheduleMobileControls({ {!isMobileFilterOpen && (
- {renderDayButton('9', 'MAY 9')} - {renderDayButton('10', 'MAY 10')} + {renderDayButton('9', 'May 9')} + {renderDayButton('10', 'May 10')}
)}