From 4faebc0d880731b8133ad53d95c644976618f14c Mon Sep 17 00:00:00 2001 From: Afifah Hadi Date: Wed, 4 Mar 2026 01:15:33 -0800 Subject: [PATCH 01/21] put may 9 and may 10 events on same page but different sections, changed nav button behavior --- app/(pages)/(hackers)/(hub)/schedule/page.tsx | 226 +++++++++++++----- .../Schedule/ScheduleMobileControls.tsx | 11 +- 2 files changed, 168 insertions(+), 69 deletions(-) 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')}
)}
From 05d8e468cce1125d92a106f38c17c75e069e7b39 Mon Sep 17 00:00:00 2001 From: reehals Date: Wed, 25 Feb 2026 18:50:29 -0800 Subject: [PATCH 02/21] Add my code for home page schedule fetching --- .../HomeHacking/ScheduleSneakPeek.tsx | 70 ++++++++++++++++--- .../_hooks/useScheduleSneakPeekData.ts | 58 +++++++++------ 2 files changed, 98 insertions(+), 30 deletions(-) diff --git a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx index 8e1a571f..5223d0d1 100644 --- a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx +++ b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState, useEffect, useMemo } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import CalendarItem from '@pages/(hackers)/_components/Schedule/CalendarItem'; @@ -32,6 +33,43 @@ function SectionLabel({ label }: { label: string }) { ); } +function CountdownLabel({ targetTime }: { targetTime: number }) { + const [timeLeft, setTimeLeft] = useState({ + hours: 0, + minutes: 0, + seconds: 0, + }); + + useEffect(() => { + const calculateTimeLeft = () => { + const difference = targetTime - new Date().getTime(); + if (difference <= 0) { + return { hours: 0, minutes: 0, seconds: 0 }; + } + return { + hours: Math.floor(difference / (1000 * 60 * 60)), + minutes: Math.floor((difference / (1000 * 60)) % 60), + seconds: Math.floor((difference / 1000) % 60), + }; + }; + + setTimeLeft(calculateTimeLeft()); + const timer = setInterval(() => { + setTimeLeft(calculateTimeLeft()); + }, 1000); + + return () => clearInterval(timer); + }, [targetTime]); + + const label = `IN ${timeLeft.hours + .toString() + .padStart(2, '0')}:${timeLeft.minutes + .toString() + .padStart(2, '0')}:${timeLeft.seconds.toString().padStart(2, '0')}`; + + return ; +} + function Panel({ title, liveEvents, @@ -63,6 +101,20 @@ function Panel({ /> )); + const upcomingGroups = useMemo(() => { + const groups: { startTime: number; entries: EventEntry[] }[] = []; + for (const entry of upcomingEvents) { + const startTime = new Date(entry.event.start_time).getTime(); + const existing = groups.find((g) => g.startTime === startTime); + if (existing) { + existing.entries.push(entry); + } else { + groups.push({ startTime, entries: [entry] }); + } + } + return groups.sort((a, b) => a.startTime - b.startTime); + }, [upcomingEvents]); + return (

@@ -81,16 +133,14 @@ function Panel({ )}

- -
- {upcomingEvents.length > 0 ? ( - renderEventItems(upcomingEvents, 'upcoming') - ) : ( -

- No events starting in the next 30 minutes. -

- )} -
+ {upcomingGroups.map((group) => ( +
+ +
+ {renderEventItems(group.entries, `upcoming-${group.startTime}`)} +
+
+ ))}
); } diff --git a/app/(pages)/_hooks/useScheduleSneakPeekData.ts b/app/(pages)/_hooks/useScheduleSneakPeekData.ts index 812fd457..0c36e7ae 100644 --- a/app/(pages)/_hooks/useScheduleSneakPeekData.ts +++ b/app/(pages)/_hooks/useScheduleSneakPeekData.ts @@ -5,10 +5,7 @@ import Event from '@typeDefs/event'; import { useEvents } from '@hooks/useEvents'; import { usePersonalEvents } from '@hooks/usePersonalEvents'; import useActiveUser from '@pages/_hooks/useActiveUser'; -import { - isScheduleEventLive, - startsScheduleEventInNextMs, -} from '@pages/(hackers)/_components/Schedule/scheduleTime'; +import { isScheduleEventLive } from '@pages/(hackers)/_components/Schedule/scheduleTime'; export interface EventEntry { event: Event; @@ -16,8 +13,6 @@ export interface EventEntry { inPersonalSchedule: boolean; } -const THIRTY_MIN_MS = 30 * 60 * 1000; - const toSorted = (events: EventEntry[]): EventEntry[] => [...events].sort( (a, b) => @@ -25,6 +20,20 @@ const toSorted = (events: EventEntry[]): EventEntry[] => new Date(b.event.start_time).getTime() ); +/** Returns only the events starting at the single nearest future start time. */ +const getNextBatchEvents = (entries: EventEntry[], now: Date): EventEntry[] => { + const future = entries.filter( + (e) => new Date(e.event.start_time).getTime() > now.getTime() + ); + if (future.length === 0) return []; + const earliest = Math.min( + ...future.map((e) => new Date(e.event.start_time).getTime()) + ); + return future.filter( + (e) => new Date(e.event.start_time).getTime() === earliest + ); +}; + export function useScheduleSneakPeekData() { const { user } = useActiveUser('/'); const { eventData } = useEvents(user); @@ -63,29 +72,38 @@ export function useScheduleSneakPeekData() { return () => clearInterval(interval); }, []); - const filteredLists = useMemo( - () => ({ + const filteredLists = useMemo(() => { + // GENERAL (and MEALS) events have no add button and must never appear in + // Your Schedule. Filter them out so they always stay in Happening Now. + const schedulablePersonalEntries = personalEventEntries.filter( + (e) => e.event.type !== 'GENERAL' && e.event.type !== 'MEALS' + ); + + // Only exclude events that will actually appear in Your Schedule. + const scheduledIds = new Set( + schedulablePersonalEntries.map((e) => e.event._id) + ); + const happeningNowEntries = allEventEntries.filter( + (e) => !scheduledIds.has(e.event._id) + ); + + return { liveAll: toSorted( - allEventEntries.filter((entry) => isScheduleEventLive(entry.event, now)) - ), - upcomingAll: toSorted( - allEventEntries.filter((entry) => - startsScheduleEventInNextMs(entry.event, THIRTY_MIN_MS, now) + happeningNowEntries.filter((entry) => + isScheduleEventLive(entry.event, now) ) ), + upcomingAll: toSorted(getNextBatchEvents(happeningNowEntries, now)), livePersonal: toSorted( - personalEventEntries.filter((entry) => + schedulablePersonalEntries.filter((entry) => isScheduleEventLive(entry.event, now) ) ), upcomingPersonal: toSorted( - personalEventEntries.filter((entry) => - startsScheduleEventInNextMs(entry.event, THIRTY_MIN_MS, now) - ) + getNextBatchEvents(schedulablePersonalEntries, now) ), - }), - [allEventEntries, personalEventEntries, now] - ); + }; + }, [allEventEntries, personalEventEntries, now]); return { ...filteredLists, From 3f079d96e95e427c51adfc43a36da388a5cb66bc Mon Sep 17 00:00:00 2001 From: reehals Date: Wed, 25 Feb 2026 18:56:39 -0800 Subject: [PATCH 03/21] The first comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The actual fixes I forgot to commit 💀 --- .../(hackers)/_components/2025IndexHero/NextSchedule.tsx | 4 +++- app/(pages)/_hooks/useNextSchedule.ts | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/(pages)/(hackers)/_components/2025IndexHero/NextSchedule.tsx b/app/(pages)/(hackers)/_components/2025IndexHero/NextSchedule.tsx index 6489d42f..87da431d 100644 --- a/app/(pages)/(hackers)/_components/2025IndexHero/NextSchedule.tsx +++ b/app/(pages)/(hackers)/_components/2025IndexHero/NextSchedule.tsx @@ -8,6 +8,7 @@ import { useEvents } from '@hooks/useEvents'; import CalendarItem from '../Schedule/CalendarItem'; import Event from '@typeDefs/event'; import TimeTracker from './TimeTracker'; +import { getScheduleEventEndTime } from '../Schedule/scheduleTime'; import star_icon from '@public/hackers/hero/star.svg'; import styles from './NextSchedule.module.scss'; @@ -47,8 +48,9 @@ export default function NextSchedule() { personalEvents.length > 0 ) { const now = new Date(); + // Include events that haven't ended yet (covers both currently happening and future events) const upcomingEvents = personalEvents.filter( - (event) => new Date(event.start_time) > now + (event) => getScheduleEventEndTime(event).getTime() > now.getTime() ); if (upcomingEvents.length > 0) { diff --git a/app/(pages)/_hooks/useNextSchedule.ts b/app/(pages)/_hooks/useNextSchedule.ts index 22688d13..d1101065 100644 --- a/app/(pages)/_hooks/useNextSchedule.ts +++ b/app/(pages)/_hooks/useNextSchedule.ts @@ -5,6 +5,7 @@ import { usePersonalEvents } from '@hooks/usePersonalEvents'; import Event from '@typeDefs/event'; import useActiveUser from '@pages/_hooks/useActiveUser'; import { useEvents } from '@hooks/useEvents'; +import { getScheduleEventEndTime } from '@pages/(hackers)/_components/Schedule/scheduleTime'; export interface NextEventData { event: Event | null; @@ -45,9 +46,9 @@ export function useNextSchedule() { ) { const now = new Date(); - // Find the next upcoming event (the one with the closest start time in the future) + // Include events that haven't ended yet (covers both currently happening and future events) const upcomingEvents = personalEvents.filter( - (event) => new Date(event.start_time) > now + (event) => getScheduleEventEndTime(event).getTime() > now.getTime() ); if (upcomingEvents.length > 0) { From 3e534aa09edbb7dc0b19ebd425906daf2f936cd0 Mon Sep 17 00:00:00 2001 From: Afifah Hadi Date: Tue, 3 Mar 2026 22:54:56 -0800 Subject: [PATCH 04/21] fixed second sub issue --- app/(pages)/(hackers)/(hub)/schedule/page.tsx | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/app/(pages)/(hackers)/(hub)/schedule/page.tsx b/app/(pages)/(hackers)/(hub)/schedule/page.tsx index b3c6a740..e63e4f28 100644 --- a/app/(pages)/(hackers)/(hub)/schedule/page.tsx +++ b/app/(pages)/(hackers)/(hub)/schedule/page.tsx @@ -247,12 +247,36 @@ 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() - - new Date(b.event.start_time).getTime() - ); + const sortedEvents = [...filteredEvents].sort((a, b) => { + const startA = new Date(a.event.start_time).getTime(); + const startB = new Date(b.event.start_time).getTime(); + + // compare start times + if (startA != startB) { + return startA - startB; + } + + // if events have same start time, get end times + const endA = a.event.end_time + ? new Date(a.event.end_time).getTime() + : null; + const endB = b.event.end_time + ? new Date(b.event.end_time).getTime() + : null; + + // if one event doesn't have an end time, that one should go first + if (endA == null && endB != null) return -1; + if (endA != null && endB == null) return 1; + + // both have end times, the event ending first goes first + if (endA != null && endB != null && endA != endB) { + return endA - endB; + } + + // else if neither events have end times, or end times are equal, + // then those two events are equal and can be scheduled either way + return 0; + }); // Group events by their start time (converted to PDT). const groups = sortedEvents.reduce( From e0b7940b4503597582714eb6a4fabe7826457598 Mon Sep 17 00:00:00 2001 From: Afifah Hadi Date: Wed, 4 Mar 2026 15:18:55 -0800 Subject: [PATCH 05/21] refactored page.tsx and split into reusable components --- app/(pages)/(hackers)/(hub)/schedule/page.tsx | 541 ++---------------- .../_components/Schedule/DayNavButtons.tsx | 37 ++ .../_components/Schedule/DaySection.tsx | 87 +++ .../_components/Schedule/ScheduleControls.tsx | 151 +++++ .../Schedule/ScheduleMobileControls.tsx | 153 ----- .../_components/Schedule/constants.ts | 8 + .../_components/Schedule/groupedEntries.ts | 89 +++ .../Schedule/hooks/useActiveDaySync.ts | 105 ++++ .../Schedule/hooks/useScheduleData.ts | 242 ++++++++ .../(hackers)/_components/Schedule/types.ts | 14 + app/(pages)/_globals/globals.scss | 3 + 11 files changed, 769 insertions(+), 661 deletions(-) create mode 100644 app/(pages)/(hackers)/_components/Schedule/DayNavButtons.tsx create mode 100644 app/(pages)/(hackers)/_components/Schedule/DaySection.tsx create mode 100644 app/(pages)/(hackers)/_components/Schedule/ScheduleControls.tsx delete mode 100644 app/(pages)/(hackers)/_components/Schedule/ScheduleMobileControls.tsx create mode 100644 app/(pages)/(hackers)/_components/Schedule/constants.ts create mode 100644 app/(pages)/(hackers)/_components/Schedule/groupedEntries.ts create mode 100644 app/(pages)/(hackers)/_components/Schedule/hooks/useActiveDaySync.ts create mode 100644 app/(pages)/(hackers)/_components/Schedule/hooks/useScheduleData.ts create mode 100644 app/(pages)/(hackers)/_components/Schedule/types.ts diff --git a/app/(pages)/(hackers)/(hub)/schedule/page.tsx b/app/(pages)/(hackers)/(hub)/schedule/page.tsx index e63e4f28..0d84ff8d 100644 --- a/app/(pages)/(hackers)/(hub)/schedule/page.tsx +++ b/app/(pages)/(hackers)/(hub)/schedule/page.tsx @@ -1,15 +1,9 @@ 'use client'; -import { useState, useEffect, useMemo, useRef } from 'react'; -import CalendarItem from '../../_components/Schedule/CalendarItem'; + import Footer from '@components/Footer/Footer'; import Image from 'next/image'; import headerGrass from '@public/hackers/schedule/header_grass.svg'; -import Event from '@typeDefs/event'; -import { ScheduleFilter } from '@typeDefs/filters'; -import { Button } from '@pages/_globals/components/ui/button'; -import Filters from '@pages/(hackers)/_components/Schedule/Filters'; -import ScheduleMobileControls from '@pages/(hackers)/_components/Schedule/ScheduleMobileControls'; - +import ScheduleControls from '@pages/(hackers)/_components/Schedule/ScheduleControls'; import { Tooltip, TooltipContent, @@ -17,376 +11,14 @@ import { TooltipTrigger, } from '@globals/components/ui/tooltip'; import TooltipCow from '@public/index/schedule/vocal_angel_cow.svg'; -import useActiveUser from '@pages/_hooks/useActiveUser'; -import { usePersonalEvents } from '@hooks/usePersonalEvents'; -import { useEvents } from '@hooks/useEvents'; - -export interface EventDetails { - event: Event; - attendeeCount?: number; - inPersonalSchedule?: boolean; - isRecommended?: boolean; -} - -interface ScheduleData { - [dayKey: string]: EventDetails[]; -} +import DaySection from '@pages/(hackers)/_components/Schedule/DaySection'; +import { DAY_KEYS } from '@pages/(hackers)/_components/Schedule/constants'; +import { useScheduleData } from '@pages/(hackers)/_components/Schedule/hooks/useScheduleData'; export default function Page() { - const { user, loading: userLoading } = useActiveUser('/'); - - // Pass the user to useEvents - const { - eventData, - isLoading: eventsLoading, - error: eventsError, - refreshEvents, - } = useEvents(user); - - const [activeTab, setActiveTab] = useState<'schedule' | 'personal'>( - 'schedule' - ); - const [activeDay, setActiveDay] = useState<'9' | '10'>('9'); - 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); - pendingDayRef.current = day; - ignoreScrollSyncUntilRef.current = Date.now() + 2000; - document - .getElementById(`day-${day}`) - ?.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }; - - const { - personalEvents, - isLoading: personalEventsLoading, - error: personalEventsError, - addToPersonalSchedule, - removeFromPersonalSchedule, - isInPersonalSchedule, - refreshPersonalEvents, - } = usePersonalEvents(user?._id || ''); - - // Function to handle adding to personal schedule with loading state - const handleAddToSchedule = async (eventId: string) => { - const success = await addToPersonalSchedule(eventId); - - if (success) { - // If successful, update both tabs - await refreshPersonalEvents(); - await refreshEvents(); - - // Also update the main schedule data if we're on the schedule tab - if (activeTab === 'schedule') { - // Update the schedule data to reflect the change - if (scheduleData) { - const newScheduleData = { ...scheduleData }; - - // Mark the event as in personal schedule - Object.keys(newScheduleData).forEach((dayKey) => { - newScheduleData[dayKey] = newScheduleData[dayKey].map((item) => { - if (item.event._id === eventId) { - return { ...item, inPersonalSchedule: true }; - } - return item; - }); - }); - - setScheduleData(newScheduleData); - } - } - } - }; - - // Function to handle removing from personal schedule with loading state - const handleRemoveFromSchedule = async (eventId: string) => { - const success = await removeFromPersonalSchedule(eventId); - - if (success) { - // If successful, update both tabs - await refreshPersonalEvents(); - await refreshEvents(); - - // Also update the main schedule data if we're on the schedule tab - if (activeTab === 'schedule') { - // Update the schedule data to reflect the change - if (scheduleData) { - const newScheduleData = { ...scheduleData }; - - // Mark the event as not in personal schedule - Object.keys(newScheduleData).forEach((dayKey) => { - newScheduleData[dayKey] = newScheduleData[dayKey].map((item) => { - if (item.event._id === eventId) { - return { ...item, inPersonalSchedule: false }; - } - return item; - }); - }); - - setScheduleData(newScheduleData); - } - } - } - }; - - // Force refresh events when user data changes - useEffect(() => { - if (user && !userLoading) { - refreshEvents(); - } - }, [user, userLoading, refreshEvents]); - - // Update the existing useEffect - simplify to just set the schedule data without virtual events - useEffect(() => { - if (!eventsLoading && !personalEventsLoading) { - // Group events by day key - "09" or "10". - const groupedByDay = eventData.reduce( - (acc: ScheduleData, eventWithCount) => { - const event = eventWithCount.event; - const dayKey = event.start_time.toLocaleString('en-US', { - timeZone: 'America/Los_Angeles', - day: 'numeric', - }); - if (!acc[dayKey]) { - acc[dayKey] = []; - } - - // Check if this event is in the user's personal schedule - const isPersonal = isInPersonalSchedule(event._id || ''); - - acc[dayKey].push({ - event, - attendeeCount: eventWithCount.attendeeCount, - inPersonalSchedule: isPersonal, - isRecommended: eventWithCount.isRecommended, - }); - return acc; - }, - {} - ); - - setScheduleData(groupedByDay); - } - }, [ - eventData, - personalEvents, - isInPersonalSchedule, - personalEventsLoading, - eventsLoading, - ]); - - useEffect(() => { - if (activeTab === 'personal') { - refreshPersonalEvents(); - } - }, [activeTab, refreshPersonalEvents]); - - // Format personal events data - const personalScheduleData = useMemo(() => { - // Return empty object instead of null to avoid loading state - if (!personalEvents?.length) return {}; - - const groupedByDay = personalEvents.reduce((acc: ScheduleData, event) => { - const dayKey = event.start_time.toLocaleString('en-US', { - timeZone: 'America/Los_Angeles', - day: 'numeric', - }); - if (!acc[dayKey]) { - acc[dayKey] = []; - } - - // Find the attendee count for this event from eventData - const eventWithCount = eventData.find((e) => e.event._id === event._id); - - acc[dayKey].push({ - event, - attendeeCount: eventWithCount?.attendeeCount || 0, - inPersonalSchedule: true, - isRecommended: eventWithCount?.isRecommended || false, - }); - return acc; - }, {}); - - return groupedByDay; - }, [personalEvents, eventData]); - - const dataToUse = - activeTab === 'personal' ? personalScheduleData : scheduleData; - - const getGroupedEntriesForDay = ( - dayKey: '9' | '10', - dataToUse: ScheduleData | null, - activeFilters: ScheduleFilter[] - ): [string, EventDetails[]][] => { - // Filter events for the active day - const eventsForDay = dataToUse?.[dayKey] ?? []; - // Apply filter logic - let filteredEvents = eventsForDay; - - if (activeFilters.length > 0 && !activeFilters.includes('ALL')) { - filteredEvents = eventsForDay.filter((eventDetail) => { - // Special handling for RECOMMENDED filter - if (activeFilters.includes('RECOMMENDED')) { - // If user wants recommended events and this one is recommended, include it - if (eventDetail.isRecommended) { - return true; - } - } - - // Regular type filtering for other filters - return activeFilters.includes(eventDetail.event.type); - }); - } - - // If no events found after filtering, return empty array - if (filteredEvents.length === 0) return []; + const schedule = useScheduleData(); - // Sort the filtered events by start time. - const sortedEvents = [...filteredEvents].sort((a, b) => { - const startA = new Date(a.event.start_time).getTime(); - const startB = new Date(b.event.start_time).getTime(); - - // compare start times - if (startA != startB) { - return startA - startB; - } - - // if events have same start time, get end times - const endA = a.event.end_time - ? new Date(a.event.end_time).getTime() - : null; - const endB = b.event.end_time - ? new Date(b.event.end_time).getTime() - : null; - - // if one event doesn't have an end time, that one should go first - if (endA == null && endB != null) return -1; - if (endA != null && endB == null) return 1; - - // both have end times, the event ending first goes first - if (endA != null && endB != null && endA != endB) { - return endA - endB; - } - - // else if neither events have end times, or end times are equal, - // then those two events are equal and can be scheduled either way - return 0; - }); - - // Group events by their start time (converted to PDT). - const groups = sortedEvents.reduce( - (acc: { [key: string]: EventDetails[] }, ed) => { - const pstDate = new Date( - ed.event.start_time.toLocaleString('en-US', { - timeZone: 'America/Los_Angeles', - }) - ); - const timeKey = pstDate.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: '2-digit', - hour12: true, - }); - if (!acc[timeKey]) { - acc[timeKey] = []; - } - acc[timeKey].push(ed); - return acc; - }, - {} - ); - - // Sort the grouped entries by time. - return Object.entries(groups).sort((a, b) => { - const dummyDay = '01/01/2000'; - const dateA = new Date(`${dummyDay} ${a[0]}`); - const dateB = new Date(`${dummyDay} ${b[0]}`); - return dateA.getTime() - dateB.getTime(); - }); - }; - - // 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') { - setActiveFilters(['ALL']); - return; - } - - const withoutAll = activeFilters.filter((id) => id !== 'ALL'); - - if (withoutAll.includes(label)) { - const nextFilters = withoutAll.filter((id) => id !== label); - setActiveFilters(nextFilters.length > 0 ? nextFilters : ['ALL']); - return; - } - - setActiveFilters([...withoutAll, label]); - }; - - // Loading state only when initially loading data, not when performing add/remove actions (requested by design) - const isInitialLoad = userLoading; - - const isError = personalEventsError || eventsError; - - if (isError) + if (schedule.isError) return (
-
+ +
- -
-
- -
- -
- - -
-
-
- {isInitialLoad ? ( + {schedule.isInitialLoad ? (

loading...

) : ( - (['9', '10'] as const).map((dayKey) => { - const dayEntries = groupedEntriesByDay[dayKey]; - const dayTitle = dayKey === '9' ? 'May 9' : 'May 10'; - - return ( -
-
- {dayTitle} -
-
- - {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).' - )} -
- )} -
- ); - }) + DAY_KEYS.map((dayKey) => ( + schedule.setActiveTab('schedule')} + onAddToSchedule={schedule.handleAddToSchedule} + onRemoveFromSchedule={schedule.handleRemoveFromSchedule} + /> + )) )}
+
diff --git a/app/(pages)/(hackers)/_components/Schedule/DayNavButtons.tsx b/app/(pages)/(hackers)/_components/Schedule/DayNavButtons.tsx new file mode 100644 index 00000000..ac83a878 --- /dev/null +++ b/app/(pages)/(hackers)/_components/Schedule/DayNavButtons.tsx @@ -0,0 +1,37 @@ +import { DAY_KEYS, DAY_LABELS, DayKey } from './constants'; + +interface DayNavButtonsProps { + activeDay: DayKey; + onSelectDay: (day: DayKey) => void; + className?: string; + buttonClassName?: string; +} + +export default function DayNavButtons({ + activeDay, + onSelectDay, + className, + buttonClassName, +}: DayNavButtonsProps) { + return ( +
+ {DAY_KEYS.map((dayKey) => ( + + ))} +
+ ); +} diff --git a/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx b/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx new file mode 100644 index 00000000..572b7bd1 --- /dev/null +++ b/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx @@ -0,0 +1,87 @@ +import { Button } from '@pages/_globals/components/ui/button'; +import CalendarItem from './CalendarItem'; +import { DAY_LABELS, DayKey } from './constants'; +import { GroupedDayEntries } from './types'; + +interface DaySectionProps { + dayKey: DayKey; + entries: GroupedDayEntries; + activeTab: 'schedule' | 'personal'; + onSwitchToScheduleTab: () => void; + onAddToSchedule: (eventId: string) => void; + onRemoveFromSchedule: (eventId: string) => void; +} + +export default function DaySection({ + dayKey, + entries, + activeTab, + onSwitchToScheduleTab, + onAddToSchedule, + onRemoveFromSchedule, +}: DaySectionProps) { + const dayTitle = DAY_LABELS[dayKey].replace(/^MAY/, 'May'); + + return ( +
+
+ {dayTitle} +
+
+ + {entries.length > 0 ? ( + entries.map(([timeKey, events]) => ( +
+
+ {timeKey} +
+
+ {events.map((eventDetail) => ( + + onAddToSchedule(eventDetail.event._id || '') + } + onRemoveFromSchedule={() => + onRemoveFromSchedule(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/ScheduleControls.tsx b/app/(pages)/(hackers)/_components/Schedule/ScheduleControls.tsx new file mode 100644 index 00000000..69b7b03a --- /dev/null +++ b/app/(pages)/(hackers)/_components/Schedule/ScheduleControls.tsx @@ -0,0 +1,151 @@ +import Image from 'next/image'; +import { pageFilters, ScheduleFilter } from '@typeDefs/filters'; +import { useEffect, useState } from 'react'; +import DayNavButtons from './DayNavButtons'; +import Filters from './Filters'; +import { DayKey } from './constants'; + +const MOBILE_FILTER_BG_DEFAULT = '#F3F3FC'; +const MOBILE_FILTER_TEXT_DEFAULT = '#3F3F3F'; +const MOBILE_FILTER_BG_SELECTED = '#3F3F3F'; +const MOBILE_FILTER_TEXT_SELECTED = '#FAFAFF'; + +interface ScheduleControlsProps { + activeDay: DayKey; + changeActiveDay: (day: DayKey) => void; + activeFilters: ScheduleFilter[]; + toggleFilter: (label: ScheduleFilter) => void; + isMobileFilterOpen: boolean; + setIsMobileFilterOpen: ( + value: boolean | ((prev: boolean) => boolean) + ) => void; +} + +export default function ScheduleControls({ + activeDay, + changeActiveDay, + activeFilters, + toggleFilter, + isMobileFilterOpen, + setIsMobileFilterOpen, +}: ScheduleControlsProps) { + const hasSelectedFilters = activeFilters.some((filter) => filter !== 'ALL'); + const selectedFilterDots = activeFilters.filter((filter) => filter !== 'ALL'); + const [isScrolled, setIsScrolled] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 110); + }; + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + return ( + <> +
+
+
+ +
+ + {!isMobileFilterOpen && ( + + )} +
+ + {isMobileFilterOpen && ( +
+ {pageFilters.map((filter) => ( + + ))} +
+ )} +
+ +
+
+ +
+ +
+ +
+
+ + ); +} diff --git a/app/(pages)/(hackers)/_components/Schedule/ScheduleMobileControls.tsx b/app/(pages)/(hackers)/_components/Schedule/ScheduleMobileControls.tsx deleted file mode 100644 index b8a2ce9e..00000000 --- a/app/(pages)/(hackers)/_components/Schedule/ScheduleMobileControls.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import Image from 'next/image'; -import { pageFilters, ScheduleFilter } from '@typeDefs/filters'; -import { useEffect, useState } from 'react'; - -const MOBILE_FILTER_BG_DEFAULT = '#F3F3FC'; -const MOBILE_FILTER_TEXT_DEFAULT = '#3F3F3F'; -const MOBILE_FILTER_BG_SELECTED = '#3F3F3F'; -const MOBILE_FILTER_TEXT_SELECTED = '#FAFAFF'; - -interface ScheduleMobileControlsProps { - activeDay: '9' | '10'; - changeActiveDay: (day: '9' | '10') => void; - activeFilters: ScheduleFilter[]; - toggleFilter: (label: ScheduleFilter) => void; - isMobileFilterOpen: boolean; - setIsMobileFilterOpen: ( - value: boolean | ((prev: boolean) => boolean) - ) => void; -} - -export default function ScheduleMobileControls({ - activeDay, - changeActiveDay, - activeFilters, - toggleFilter, - isMobileFilterOpen, - setIsMobileFilterOpen, -}: ScheduleMobileControlsProps) { - const hasSelectedFilters = activeFilters.some((filter) => filter !== 'ALL'); - const selectedFilterDots = activeFilters.filter((filter) => filter !== 'ALL'); - - const [isScrolled, setIsScrolled] = useState(false); - - // Allows for filter button to disspear when user scroll down - useEffect(() => { - const handleScroll = () => { - // If scrolled more than 110px, hide filter button - setIsScrolled(window.scrollY > 110); - }; - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); - }, []); - - const renderDayButton = (day: '9' | '10', label: string) => ( - - ); - - return ( -
-
-
- -
- - {!isMobileFilterOpen && ( -
- {renderDayButton('9', 'May 9')} - {renderDayButton('10', 'May 10')} -
- )} -
- - {isMobileFilterOpen && ( -
- {pageFilters.map((filter) => ( - - ))} -
- )} -
- ); -} diff --git a/app/(pages)/(hackers)/_components/Schedule/constants.ts b/app/(pages)/(hackers)/_components/Schedule/constants.ts new file mode 100644 index 00000000..0ed45a57 --- /dev/null +++ b/app/(pages)/(hackers)/_components/Schedule/constants.ts @@ -0,0 +1,8 @@ +export const DAY_KEYS = ['9', '10'] as const; + +export type DayKey = (typeof DAY_KEYS)[number]; + +export const DAY_LABELS: Record = { + '9': 'MAY 9', + '10': 'MAY 10', +}; diff --git a/app/(pages)/(hackers)/_components/Schedule/groupedEntries.ts b/app/(pages)/(hackers)/_components/Schedule/groupedEntries.ts new file mode 100644 index 00000000..5fb7b437 --- /dev/null +++ b/app/(pages)/(hackers)/_components/Schedule/groupedEntries.ts @@ -0,0 +1,89 @@ +import { ScheduleFilter } from '@typeDefs/filters'; +import { DAY_KEYS, DayKey } from './constants'; +import { EventDetails, GroupedDayEntries, ScheduleData } from './types'; + +export const getGroupedEntriesForDay = ( + dayKey: DayKey, + dataToUse: ScheduleData | null, + activeFilters: ScheduleFilter[] +): GroupedDayEntries => { + const eventsForDay = dataToUse?.[dayKey] ?? []; + let filteredEvents = eventsForDay; + + if (activeFilters.length > 0 && !activeFilters.includes('ALL')) { + filteredEvents = eventsForDay.filter((eventDetail) => { + if (activeFilters.includes('RECOMMENDED') && eventDetail.isRecommended) { + return true; + } + + return activeFilters.includes(eventDetail.event.type); + }); + } + + if (filteredEvents.length === 0) return []; + + const sortedEvents = [...filteredEvents].sort((a, b) => { + const startA = new Date(a.event.start_time).getTime(); + const startB = new Date(b.event.start_time).getTime(); + + if (startA !== startB) { + return startA - startB; + } + + const endA = a.event.end_time ? new Date(a.event.end_time).getTime() : null; + const endB = b.event.end_time ? new Date(b.event.end_time).getTime() : null; + + if (endA === null && endB !== null) return -1; + if (endA !== null && endB === null) return 1; + + if (endA !== null && endB !== null && endA !== endB) { + return endA - endB; + } + + return 0; + }); + + const groups = sortedEvents.reduce( + (acc: Record, eventDetails) => { + const localizedStart = new Date( + eventDetails.event.start_time.toLocaleString('en-US', { + timeZone: 'America/Los_Angeles', + }) + ); + + const timeKey = localizedStart.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true, + }); + + if (!acc[timeKey]) { + acc[timeKey] = []; + } + + acc[timeKey].push(eventDetails); + return acc; + }, + {} + ); + + return Object.entries(groups).sort((a, b) => { + const dummyDay = '01/01/2000'; + const dateA = new Date(`${dummyDay} ${a[0]}`); + const dateB = new Date(`${dummyDay} ${b[0]}`); + return dateA.getTime() - dateB.getTime(); + }); +}; + +export const buildGroupedEntriesByDay = ( + dataToUse: ScheduleData | null, + activeFilters: ScheduleFilter[] +): Record => { + return DAY_KEYS.reduce( + (acc, dayKey) => { + acc[dayKey] = getGroupedEntriesForDay(dayKey, dataToUse, activeFilters); + return acc; + }, + {} as Record + ); +}; diff --git a/app/(pages)/(hackers)/_components/Schedule/hooks/useActiveDaySync.ts b/app/(pages)/(hackers)/_components/Schedule/hooks/useActiveDaySync.ts new file mode 100644 index 00000000..9c15e995 --- /dev/null +++ b/app/(pages)/(hackers)/_components/Schedule/hooks/useActiveDaySync.ts @@ -0,0 +1,105 @@ +'use client'; + +import { useCallback, useEffect, useRef } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; +import { DAY_KEYS, DayKey } from '../constants'; + +interface UseActiveDaySyncOptions { + activeDay: DayKey; + setActiveDay: Dispatch>; + dayKeys?: readonly DayKey[]; + anchorRatio?: number; + clickLockMs?: number; + syncSignal?: unknown; +} + +export function useActiveDaySync({ + activeDay, + setActiveDay, + dayKeys = DAY_KEYS, + anchorRatio = 0.45, + clickLockMs = 900, + syncSignal, +}: UseActiveDaySyncOptions) { + const ignoreScrollSyncUntilRef = useRef(0); + const pendingDayRef = useRef(null); + + const updateActiveDayFromScroll = useCallback(() => { + const anchor = window.innerHeight * anchorRatio; + const pendingDay = pendingDayRef.current; + + if (pendingDay) { + const pendingSection = document.getElementById(`day-${pendingDay}`); + + if (pendingSection) { + const pendingRect = pendingSection.getBoundingClientRect(); + const pendingReached = + pendingRect.top <= anchor + 8 && pendingRect.bottom > anchor; + + if (pendingReached) { + setActiveDay((prev) => (prev === pendingDay ? prev : pendingDay)); + pendingDayRef.current = null; + ignoreScrollSyncUntilRef.current = 0; + return; + } + } + + if (Date.now() < ignoreScrollSyncUntilRef.current) { + setActiveDay((prev) => (prev === pendingDay ? prev : pendingDay)); + return; + } + + pendingDayRef.current = null; + } + + const daySections = dayKeys + .map((day) => { + const section = document.getElementById(`day-${day}`); + return section ? { day, rect: section.getBoundingClientRect() } : null; + }) + .filter( + (section): section is { day: DayKey; rect: DOMRect } => section !== null + ); + + if (daySections.length === 0) return; + + let nextActiveDay = daySections[0].day; + for (const section of daySections) { + if (section.rect.top <= anchor) { + nextActiveDay = section.day; + } + } + + setActiveDay((prev) => (prev === nextActiveDay ? prev : nextActiveDay)); + }, [anchorRatio, dayKeys, setActiveDay]); + + useEffect(() => { + updateActiveDayFromScroll(); + window.addEventListener('scroll', updateActiveDayFromScroll, { + passive: true, + }); + + return () => { + window.removeEventListener('scroll', updateActiveDayFromScroll); + }; + }, [updateActiveDayFromScroll]); + + useEffect(() => { + updateActiveDayFromScroll(); + }, [activeDay, syncSignal, updateActiveDayFromScroll]); + + const changeActiveDay = useCallback( + (day: DayKey) => { + pendingDayRef.current = day; + ignoreScrollSyncUntilRef.current = Date.now() + clickLockMs; + setActiveDay((prev) => (prev === day ? prev : day)); + + document + .getElementById(`day-${day}`) + ?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }, + [clickLockMs, setActiveDay] + ); + + return { changeActiveDay }; +} diff --git a/app/(pages)/(hackers)/_components/Schedule/hooks/useScheduleData.ts b/app/(pages)/(hackers)/_components/Schedule/hooks/useScheduleData.ts new file mode 100644 index 00000000..cb1b9982 --- /dev/null +++ b/app/(pages)/(hackers)/_components/Schedule/hooks/useScheduleData.ts @@ -0,0 +1,242 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; +import useActiveUser from '@pages/_hooks/useActiveUser'; +import { useEvents } from '@hooks/useEvents'; +import { usePersonalEvents } from '@hooks/usePersonalEvents'; +import { ScheduleFilter } from '@typeDefs/filters'; +import { DAY_KEYS, DayKey } from '../constants'; +import { buildGroupedEntriesByDay } from '../groupedEntries'; +import { ScheduleData } from '../types'; +import { useActiveDaySync } from './useActiveDaySync'; + +interface UseScheduleDataResult { + activeTab: 'schedule' | 'personal'; + setActiveTab: Dispatch>; + activeDay: DayKey; + setActiveDay: Dispatch>; + activeFilters: ScheduleFilter[]; + toggleFilter: (label: ScheduleFilter) => void; + isMobileFilterOpen: boolean; + setIsMobileFilterOpen: ( + value: boolean | ((prev: boolean) => boolean) + ) => void; + groupedEntriesByDay: ReturnType; + handleAddToSchedule: (eventId: string) => Promise; + handleRemoveFromSchedule: (eventId: string) => Promise; + isInitialLoad: boolean; + isError: boolean; + changeActiveDay: (day: DayKey) => void; +} + +const getDayKeyInPacific = (date: Date) => + date.toLocaleString('en-US', { + timeZone: 'America/Los_Angeles', + day: 'numeric', + }); + +export function useScheduleData(): UseScheduleDataResult { + const { user, loading: userLoading } = useActiveUser('/'); + + const { + eventData, + isLoading: eventsLoading, + error: eventsError, + refreshEvents, + } = useEvents(user); + + const { + personalEvents, + isLoading: personalEventsLoading, + error: personalEventsError, + addToPersonalSchedule, + removeFromPersonalSchedule, + isInPersonalSchedule, + refreshPersonalEvents, + } = usePersonalEvents(user?._id || ''); + + const [activeTab, setActiveTab] = useState<'schedule' | 'personal'>( + 'schedule' + ); + const [activeDay, setActiveDay] = useState('9'); + const [activeFilters, setActiveFilters] = useState(['ALL']); + const [isMobileFilterOpen, setIsMobileFilterOpen] = useState(false); + const [scheduleData, setScheduleData] = useState(null); + + const patchScheduleMembership = ( + eventId: string, + inPersonalSchedule: boolean + ) => { + setScheduleData((prev) => { + if (!prev) return prev; + + const next = { ...prev }; + Object.keys(next).forEach((dayKey) => { + next[dayKey] = next[dayKey].map((item) => { + if (item.event._id === eventId) { + return { ...item, inPersonalSchedule }; + } + return item; + }); + }); + + return next; + }); + }; + + const handleAddToSchedule = async (eventId: string) => { + const success = await addToPersonalSchedule(eventId); + + if (success) { + await refreshPersonalEvents(); + await refreshEvents(); + + if (activeTab === 'schedule') { + patchScheduleMembership(eventId, true); + } + } + }; + + const handleRemoveFromSchedule = async (eventId: string) => { + const success = await removeFromPersonalSchedule(eventId); + + if (success) { + await refreshPersonalEvents(); + await refreshEvents(); + + if (activeTab === 'schedule') { + patchScheduleMembership(eventId, false); + } + } + }; + + useEffect(() => { + if (user && !userLoading) { + refreshEvents(); + } + }, [user, userLoading, refreshEvents]); + + useEffect(() => { + if (!eventsLoading && !personalEventsLoading) { + const groupedByDay = eventData.reduce( + (acc: ScheduleData, eventWithCount) => { + const event = eventWithCount.event; + const dayKey = getDayKeyInPacific(event.start_time); + + if (!acc[dayKey]) { + acc[dayKey] = []; + } + + const isPersonal = isInPersonalSchedule(event._id || ''); + + acc[dayKey].push({ + event, + attendeeCount: eventWithCount.attendeeCount, + inPersonalSchedule: isPersonal, + isRecommended: eventWithCount.isRecommended, + }); + return acc; + }, + {} + ); + + setScheduleData(groupedByDay); + } + }, [ + eventData, + personalEvents, + isInPersonalSchedule, + personalEventsLoading, + eventsLoading, + ]); + + useEffect(() => { + if (activeTab === 'personal') { + refreshPersonalEvents(); + } + }, [activeTab, refreshPersonalEvents]); + + const personalScheduleData = useMemo(() => { + if (!personalEvents?.length) return {}; + + const groupedByDay = personalEvents.reduce((acc: ScheduleData, event) => { + const dayKey = getDayKeyInPacific(event.start_time); + if (!acc[dayKey]) { + acc[dayKey] = []; + } + + const eventWithCount = eventData.find((e) => e.event._id === event._id); + + acc[dayKey].push({ + event, + attendeeCount: eventWithCount?.attendeeCount || 0, + inPersonalSchedule: true, + isRecommended: eventWithCount?.isRecommended || false, + }); + return acc; + }, {}); + + return groupedByDay; + }, [personalEvents, eventData]); + + const dataToUse = + activeTab === 'personal' ? personalScheduleData : scheduleData; + + const groupedEntriesByDay = useMemo( + () => buildGroupedEntriesByDay(dataToUse, activeFilters), + [dataToUse, activeFilters] + ); + + const syncSignal = useMemo( + () => + `${activeTab}:${activeFilters.join(',')}:${DAY_KEYS.map( + (dayKey) => groupedEntriesByDay[dayKey].length + ).join(',')}`, + [activeTab, activeFilters, groupedEntriesByDay] + ); + + const { changeActiveDay } = useActiveDaySync({ + activeDay, + setActiveDay, + dayKeys: DAY_KEYS, + syncSignal, + }); + + const toggleFilter = (label: ScheduleFilter) => { + if (label === 'ALL') { + setActiveFilters(['ALL']); + return; + } + + const withoutAll = activeFilters.filter((id) => id !== 'ALL'); + + if (withoutAll.includes(label)) { + const nextFilters = withoutAll.filter((id) => id !== label); + setActiveFilters(nextFilters.length > 0 ? nextFilters : ['ALL']); + return; + } + + setActiveFilters([...withoutAll, label]); + }; + + const isInitialLoad = userLoading; + const isError = Boolean(personalEventsError || eventsError); + + return { + activeTab, + setActiveTab, + activeDay, + setActiveDay, + activeFilters, + toggleFilter, + isMobileFilterOpen, + setIsMobileFilterOpen, + groupedEntriesByDay, + handleAddToSchedule, + handleRemoveFromSchedule, + isInitialLoad, + isError, + changeActiveDay, + }; +} diff --git a/app/(pages)/(hackers)/_components/Schedule/types.ts b/app/(pages)/(hackers)/_components/Schedule/types.ts new file mode 100644 index 00000000..23d62e05 --- /dev/null +++ b/app/(pages)/(hackers)/_components/Schedule/types.ts @@ -0,0 +1,14 @@ +import Event from '@typeDefs/event'; + +export interface EventDetails { + event: Event; + attendeeCount?: number; + inPersonalSchedule?: boolean; + isRecommended?: boolean; +} + +export interface ScheduleData { + [dayKey: string]: EventDetails[]; +} + +export type GroupedDayEntries = [string, EventDetails[]][]; diff --git a/app/(pages)/_globals/globals.scss b/app/(pages)/_globals/globals.scss index 9c5e1ce5..41fbc74a 100644 --- a/app/(pages)/_globals/globals.scss +++ b/app/(pages)/_globals/globals.scss @@ -108,6 +108,9 @@ box-sizing: border-box; padding: 0; margin: 0; +} + +body { font-family: var(--font-jakarta); } From 48bb9909d5a436a65874f8458fab45a07f9f891c Mon Sep 17 00:00:00 2001 From: Afifah Hadi Date: Wed, 4 Mar 2026 19:59:38 -0800 Subject: [PATCH 06/21] added day nav button animation --- .../_components/Schedule/DayNavButtons.tsx | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/app/(pages)/(hackers)/_components/Schedule/DayNavButtons.tsx b/app/(pages)/(hackers)/_components/Schedule/DayNavButtons.tsx index ac83a878..30e82fb1 100644 --- a/app/(pages)/(hackers)/_components/Schedule/DayNavButtons.tsx +++ b/app/(pages)/(hackers)/_components/Schedule/DayNavButtons.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { DAY_KEYS, DAY_LABELS, DayKey } from './constants'; interface DayNavButtonsProps { @@ -13,23 +14,36 @@ export default function DayNavButtons({ className, buttonClassName, }: DayNavButtonsProps) { + const [hoveredDay, setHoveredDay] = useState(null); + const previewDay = + hoveredDay && hoveredDay !== activeDay ? hoveredDay : activeDay; + return (
{DAY_KEYS.map((dayKey) => ( ))}
From 335b9ea63334836ec144321a215f31e7bf95e218 Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Thu, 5 Mar 2026 18:43:32 -0800 Subject: [PATCH 07/21] moved hooks --- app/(pages)/(hackers)/(hub)/schedule/page.tsx | 2 +- .../Schedule/hooks => _hooks}/useActiveDaySync.ts | 2 +- .../Schedule/hooks => _hooks}/useScheduleData.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename app/(pages)/{(hackers)/_components/Schedule/hooks => _hooks}/useActiveDaySync.ts (97%) rename app/(pages)/{(hackers)/_components/Schedule/hooks => _hooks}/useScheduleData.ts (96%) diff --git a/app/(pages)/(hackers)/(hub)/schedule/page.tsx b/app/(pages)/(hackers)/(hub)/schedule/page.tsx index 0d84ff8d..9d6a2981 100644 --- a/app/(pages)/(hackers)/(hub)/schedule/page.tsx +++ b/app/(pages)/(hackers)/(hub)/schedule/page.tsx @@ -13,7 +13,7 @@ import { import TooltipCow from '@public/index/schedule/vocal_angel_cow.svg'; import DaySection from '@pages/(hackers)/_components/Schedule/DaySection'; import { DAY_KEYS } from '@pages/(hackers)/_components/Schedule/constants'; -import { useScheduleData } from '@pages/(hackers)/_components/Schedule/hooks/useScheduleData'; +import { useScheduleData } from '@pages/_hooks/useScheduleData'; export default function Page() { const schedule = useScheduleData(); diff --git a/app/(pages)/(hackers)/_components/Schedule/hooks/useActiveDaySync.ts b/app/(pages)/_hooks/useActiveDaySync.ts similarity index 97% rename from app/(pages)/(hackers)/_components/Schedule/hooks/useActiveDaySync.ts rename to app/(pages)/_hooks/useActiveDaySync.ts index 9c15e995..b52769b8 100644 --- a/app/(pages)/(hackers)/_components/Schedule/hooks/useActiveDaySync.ts +++ b/app/(pages)/_hooks/useActiveDaySync.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef } from 'react'; import type { Dispatch, SetStateAction } from 'react'; -import { DAY_KEYS, DayKey } from '../constants'; +import { DAY_KEYS, DayKey } from '../(hackers)/_components/Schedule/constants'; interface UseActiveDaySyncOptions { activeDay: DayKey; diff --git a/app/(pages)/(hackers)/_components/Schedule/hooks/useScheduleData.ts b/app/(pages)/_hooks/useScheduleData.ts similarity index 96% rename from app/(pages)/(hackers)/_components/Schedule/hooks/useScheduleData.ts rename to app/(pages)/_hooks/useScheduleData.ts index cb1b9982..0bb364d9 100644 --- a/app/(pages)/(hackers)/_components/Schedule/hooks/useScheduleData.ts +++ b/app/(pages)/_hooks/useScheduleData.ts @@ -6,9 +6,9 @@ import useActiveUser from '@pages/_hooks/useActiveUser'; import { useEvents } from '@hooks/useEvents'; import { usePersonalEvents } from '@hooks/usePersonalEvents'; import { ScheduleFilter } from '@typeDefs/filters'; -import { DAY_KEYS, DayKey } from '../constants'; -import { buildGroupedEntriesByDay } from '../groupedEntries'; -import { ScheduleData } from '../types'; +import { DAY_KEYS, DayKey } from '../(hackers)/_components/Schedule/constants'; +import { buildGroupedEntriesByDay } from '../(hackers)/_components/Schedule/groupedEntries'; +import { ScheduleData } from '../(hackers)/_components/Schedule/types'; import { useActiveDaySync } from './useActiveDaySync'; interface UseScheduleDataResult { From cbe82ea1266b10885f26382849a674b1422bf4cb Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Tue, 10 Mar 2026 19:29:29 -0700 Subject: [PATCH 08/21] comment out images temp --- .../(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx index 5703481e..65577b36 100644 --- a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx +++ b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx @@ -9,9 +9,9 @@ import { useScheduleSneakPeekData, } from '../../../_hooks/useScheduleSneakPeekData'; -import sleeping_cow from '@public/hackers/hero/sleeping_cow.svg'; +// import sleeping_cow from '@public/hackers/hero/sleeping_cow.svg'; import duckbunny from '@public/hackers/hero/scheduleSneakPeek/duck+bunny.svg'; -import duckfrog from '@public/hackers/hero/scheduleSneakPeek/duck+frog.svg'; +// import duckfrog from '@public/hackers/hero/scheduleSneakPeek/duck+frog.svg'; import cucumber_cow from '@public/hackers/hero/scheduleSneakPeek/cucumber_cow.svg'; interface ScheduleSneakPeekProps { From d98b173f82f49975d0a3a6d8cb5d480edf6e8a3d Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Mon, 23 Mar 2026 09:11:58 -0700 Subject: [PATCH 09/21] moved type file --- .../2025IndexHero/NextSchedule.tsx | 116 ------------------ .../_components/Schedule/DaySection.tsx | 2 +- .../_components/Schedule/groupedEntries.ts | 6 +- app/(pages)/_hooks/useScheduleData.ts | 2 +- .../Schedule/types.ts => _types/schedule.ts} | 0 5 files changed, 7 insertions(+), 119 deletions(-) delete mode 100644 app/(pages)/(hackers)/_components/2025IndexHero/NextSchedule.tsx rename app/{(pages)/(hackers)/_components/Schedule/types.ts => _types/schedule.ts} (100%) diff --git a/app/(pages)/(hackers)/_components/2025IndexHero/NextSchedule.tsx b/app/(pages)/(hackers)/_components/2025IndexHero/NextSchedule.tsx deleted file mode 100644 index 87da431d..00000000 --- a/app/(pages)/(hackers)/_components/2025IndexHero/NextSchedule.tsx +++ /dev/null @@ -1,116 +0,0 @@ -'use client'; - -import Image from 'next/image'; -import { useEffect, useState } from 'react'; -import { usePersonalEvents } from '@hooks/usePersonalEvents'; -import useActiveUser from '@pages/_hooks/useActiveUser'; -import { useEvents } from '@hooks/useEvents'; -import CalendarItem from '../Schedule/CalendarItem'; -import Event from '@typeDefs/event'; -import TimeTracker from './TimeTracker'; -import { getScheduleEventEndTime } from '../Schedule/scheduleTime'; -import star_icon from '@public/hackers/hero/star.svg'; - -import styles from './NextSchedule.module.scss'; - -export default function NextSchedule() { - const [nextEventData, setNextEventData] = useState<{ - event: Event | null; - attendeeCount: number; - inPersonalSchedule: boolean; - }>({ - event: null, - attendeeCount: 0, - inPersonalSchedule: false, - }); - - const { user } = useActiveUser('/'); - const { personalEvents, isLoading, refreshPersonalEvents } = - usePersonalEvents(user?._id || ''); - const { - eventData, - isLoading: eventsLoading, - refreshEvents, - } = useEvents(user); - - useEffect(() => { - if (user?._id) { - refreshPersonalEvents(); - refreshEvents(); - } - }, [user?._id, refreshPersonalEvents, refreshEvents]); - - useEffect(() => { - if ( - !isLoading && - !eventsLoading && - personalEvents && - personalEvents.length > 0 - ) { - const now = new Date(); - // Include events that haven't ended yet (covers both currently happening and future events) - const upcomingEvents = personalEvents.filter( - (event) => getScheduleEventEndTime(event).getTime() > now.getTime() - ); - - if (upcomingEvents.length > 0) { - const sortedEvents = [...upcomingEvents].sort( - (a, b) => - new Date(a.start_time).getTime() - new Date(b.start_time).getTime() - ); - - const nextEvent = sortedEvents[0]; - const eventWithCount = eventData.find( - (e) => e.event._id === nextEvent._id - ); - - setNextEventData({ - event: nextEvent as Event, - attendeeCount: eventWithCount?.attendeeCount || 0, - inPersonalSchedule: true, - }); - } else { - setNextEventData({ - event: null, - attendeeCount: 0, - inPersonalSchedule: false, - }); - } - } - }, [personalEvents, isLoading, eventsLoading, eventData]); - - const { event, attendeeCount, inPersonalSchedule } = nextEventData; - const nextEventTime = event?.start_time.getTime() || undefined; - - return ( -
-
-

NEXT ON YOUR SCHEDULE

- star icon - {event && ( -
- -
- )} -
- {event && ( - - )} -
- ); -} diff --git a/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx b/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx index 572b7bd1..1c25246a 100644 --- a/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx +++ b/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx @@ -1,7 +1,7 @@ import { Button } from '@pages/_globals/components/ui/button'; import CalendarItem from './CalendarItem'; import { DAY_LABELS, DayKey } from './constants'; -import { GroupedDayEntries } from './types'; +import { GroupedDayEntries } from '@typeDefs/schedule'; interface DaySectionProps { dayKey: DayKey; diff --git a/app/(pages)/(hackers)/_components/Schedule/groupedEntries.ts b/app/(pages)/(hackers)/_components/Schedule/groupedEntries.ts index 5fb7b437..beeb83b5 100644 --- a/app/(pages)/(hackers)/_components/Schedule/groupedEntries.ts +++ b/app/(pages)/(hackers)/_components/Schedule/groupedEntries.ts @@ -1,6 +1,10 @@ import { ScheduleFilter } from '@typeDefs/filters'; import { DAY_KEYS, DayKey } from './constants'; -import { EventDetails, GroupedDayEntries, ScheduleData } from './types'; +import { + EventDetails, + GroupedDayEntries, + ScheduleData, +} from '@typeDefs/schedule'; export const getGroupedEntriesForDay = ( dayKey: DayKey, diff --git a/app/(pages)/_hooks/useScheduleData.ts b/app/(pages)/_hooks/useScheduleData.ts index 0bb364d9..f2696335 100644 --- a/app/(pages)/_hooks/useScheduleData.ts +++ b/app/(pages)/_hooks/useScheduleData.ts @@ -8,7 +8,7 @@ import { usePersonalEvents } from '@hooks/usePersonalEvents'; import { ScheduleFilter } from '@typeDefs/filters'; import { DAY_KEYS, DayKey } from '../(hackers)/_components/Schedule/constants'; import { buildGroupedEntriesByDay } from '../(hackers)/_components/Schedule/groupedEntries'; -import { ScheduleData } from '../(hackers)/_components/Schedule/types'; +import { ScheduleData } from '@typeDefs/schedule'; import { useActiveDaySync } from './useActiveDaySync'; interface UseScheduleDataResult { diff --git a/app/(pages)/(hackers)/_components/Schedule/types.ts b/app/_types/schedule.ts similarity index 100% rename from app/(pages)/(hackers)/_components/Schedule/types.ts rename to app/_types/schedule.ts From f558cde1a1d0cb2fc3d2e92df5aa30f9891d8801 Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Mon, 23 Mar 2026 15:20:40 -0700 Subject: [PATCH 10/21] update ui to figma reqests --- app/(pages)/(hackers)/(hub)/schedule/page.tsx | 6 +-- .../_components/Schedule/CalendarItem.tsx | 24 +++++------ .../_components/Schedule/DayNavButtons.tsx | 2 +- .../_components/Schedule/DaySection.tsx | 41 ++++++++++++++++--- .../_components/Schedule/ScheduleControls.tsx | 6 ++- .../Schedule/scheduleEventStyles.ts | 6 +-- public/hackers/schedule/location.svg | 2 +- 7 files changed, 60 insertions(+), 27 deletions(-) diff --git a/app/(pages)/(hackers)/(hub)/schedule/page.tsx b/app/(pages)/(hackers)/(hub)/schedule/page.tsx index 27d35cf0..0ecae9f9 100644 --- a/app/(pages)/(hackers)/(hub)/schedule/page.tsx +++ b/app/(pages)/(hackers)/(hub)/schedule/page.tsx @@ -41,7 +41,7 @@ export default function Page() { />
-
+
@@ -101,7 +101,7 @@ export default function Page() { setIsMobileFilterOpen={schedule.setIsMobileFilterOpen} /> -
+
{schedule.isInitialLoad ? (

loading...

@@ -121,8 +121,6 @@ export default function Page() { )}
- -
); diff --git a/app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx b/app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx index 327d80b3..c80508d5 100644 --- a/app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx +++ b/app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx @@ -58,7 +58,7 @@ export function CalendarItem({ }} >
-

+

{name}

- + {timeDisplay} {displayType === 'MEALS' && ' (Subject to change)'} @@ -88,23 +88,23 @@ export function CalendarItem({ height={13.44} className="mr-1" /> - + {location}
)}
{tags && tags.length > 0 && ( -
+
{tags.map((tag) => (
{tag.toUpperCase()} @@ -116,8 +116,8 @@ export function CalendarItem({ {displayType !== 'GENERAL' && displayType !== 'MEALS' && (
{host && ( - - {host} + + {host.toUpperCase()} )}
@@ -140,13 +140,13 @@ export function CalendarItem({ `} > -
+
attendee icon
- + {`${attendeeCount ?? ''} Hacker${ attendeeCount && attendeeCount < 2 ? ' is' : 's are' - } attending this event`} + } attending`}
)} diff --git a/app/(pages)/(hackers)/_components/Schedule/DayNavButtons.tsx b/app/(pages)/(hackers)/_components/Schedule/DayNavButtons.tsx index 30e82fb1..51c127c3 100644 --- a/app/(pages)/(hackers)/_components/Schedule/DayNavButtons.tsx +++ b/app/(pages)/(hackers)/_components/Schedule/DayNavButtons.tsx @@ -34,7 +34,7 @@ export default function DayNavButtons({ } ${buttonClassName ?? ''}`} > -
- {dayTitle} +
+
+ {dayTitle}{' '} + {dayTitle === 'May 9' ? ( + Saturday + ) : ( + Sunday + )} +
+
+ + {dayTag} + +
-
+
{entries.length > 0 ? ( entries.map(([timeKey, events]) => ( @@ -38,7 +69,7 @@ export default function DaySection({ key={`${dayKey}-${timeKey}`} className="relative mb-[24px] last:mb-0" > -
+
{timeKey}
diff --git a/app/(pages)/(hackers)/_components/Schedule/ScheduleControls.tsx b/app/(pages)/(hackers)/_components/Schedule/ScheduleControls.tsx index 7095e721..f64e5554 100644 --- a/app/(pages)/(hackers)/_components/Schedule/ScheduleControls.tsx +++ b/app/(pages)/(hackers)/_components/Schedule/ScheduleControls.tsx @@ -99,7 +99,11 @@ export default function ScheduleControls({ )} diff --git a/app/(pages)/(hackers)/_components/Schedule/scheduleEventStyles.ts b/app/(pages)/(hackers)/_components/Schedule/scheduleEventStyles.ts index 716b9aa9..6106dc63 100644 --- a/app/(pages)/(hackers)/_components/Schedule/scheduleEventStyles.ts +++ b/app/(pages)/(hackers)/_components/Schedule/scheduleEventStyles.ts @@ -14,19 +14,19 @@ export const SCHEDULE_EVENT_STYLES: Record = { ACTIVITIES: { bgColor: '#FFE2D5', textColor: '#52230C', - addButtonColor: '#FFD5C2', + addButtonColor: '#FFD5C2', // integrated for calendar add buttons (not related to ACTIVITES) }, WORKSHOPS: { bgColor: '#E9FBBA', textColor: '#1A3819', - addButtonColor: '#D1F76E', + addButtonColor: '#D1F76E', // integrated for calendar add buttons (not related to WORKSHOPS) }, MEALS: { bgColor: '#FFE7B2', textColor: '#572700', }, RECOMMENDED: { - bgColor: '#CCFFFE', + bgColor: '#C0AAE2', textColor: '#003D3D', }, }; diff --git a/public/hackers/schedule/location.svg b/public/hackers/schedule/location.svg index 5cf81525..e0d0ccc5 100644 --- a/public/hackers/schedule/location.svg +++ b/public/hackers/schedule/location.svg @@ -1,3 +1,3 @@ - + From e82bce0d82d34ad92b85c4ead571a5cb1583ce18 Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Mon, 23 Mar 2026 15:34:31 -0700 Subject: [PATCH 11/21] address fixes --- .../HomeHacking/ScheduleSneakPeek.tsx | 85 +++++++++---------- app/(pages)/_hooks/useScheduleData.ts | 20 +++-- 2 files changed, 51 insertions(+), 54 deletions(-) diff --git a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx index 0521d011..9c7a3a56 100644 --- a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx +++ b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx @@ -9,9 +9,9 @@ import { useScheduleSneakPeekData, } from '../../../_hooks/useScheduleSneakPeekData'; -// import sleeping_cow from '@public/hackers/hero/sleeping_cow.svg'; +import sleeping_cow from '@public/hackers/hero/sleeping_cow.svg'; import duckbunny from '@public/hackers/scheduleSneakPeek/duck+bunny.svg'; -// import duckfrog from '@public/hackers/scheduleSneakPeek/duck+frog.svg'; +import duckfrog from '@public/hackers/scheduleSneakPeek/duck+frog.svg'; import cucumber_cow from '@public/hackers/scheduleSneakPeek/cucumber_cow.svg'; interface ScheduleSneakPeekProps { @@ -163,52 +163,45 @@ function Panel({
)}
- - {/* -
- {upcomingEvents.length > 0 ? ( - renderEventItems(upcomingEvents, 'upcoming') - ) : ( -
- { -

- {title === 'Your schedule' - ? 'No upcoming events on your schedule' - : 'No upcoming events'} -

-

- {title === 'Your schedule' - ? 'This is where you’ll see upcoming events. Seems like there’s nothing coming up! Take a look to see if there’s anything you want to check out.' - : 'This is where you’ll see upcoming events. Seems like there’s nothing coming up!'} -

- - {title == 'Your schedule' ? ( - - ) : null} - -
- )} -
*/} - {upcomingGroups.map((group) => ( -
- -
- {renderEventItems(group.entries, `upcoming-${group.startTime}`)} + {upcomingGroups.length > 0 ? ( + upcomingGroups.map((group) => ( +
+ +
+ {renderEventItems(group.entries, `upcoming-${group.startTime}`)} +
+ )) + ) : ( +
+ { +

+ {title === 'Your schedule' + ? 'No upcoming events on your schedule' + : 'No upcoming events'} +

+

+ {title === 'Your schedule' + ? 'This is where you’ll see upcoming events. Seems like there’s nothing coming up! Take a look to see if there’s anything you want to check out.' + : 'This is where you’ll see upcoming events. Seems like there’s nothing coming up!'} +

+ + {title == 'Your schedule' ? ( + + ) : null} +
- ))} + )}
); } diff --git a/app/(pages)/_hooks/useScheduleData.ts b/app/(pages)/_hooks/useScheduleData.ts index f2696335..1066d6b5 100644 --- a/app/(pages)/_hooks/useScheduleData.ts +++ b/app/(pages)/_hooks/useScheduleData.ts @@ -188,13 +188,17 @@ export function useScheduleData(): UseScheduleDataResult { [dataToUse, activeFilters] ); - const syncSignal = useMemo( - () => - `${activeTab}:${activeFilters.join(',')}:${DAY_KEYS.map( - (dayKey) => groupedEntriesByDay[dayKey].length - ).join(',')}`, - [activeTab, activeFilters, groupedEntriesByDay] - ); + const syncSignal = useMemo(() => { + const contentHash = DAY_KEYS.map((dayKey) => { + const dayGroups = groupedEntriesByDay[dayKey] || []; + const totalEvents = dayGroups.reduce( + (sum, group) => sum + group.entries.length, + 0 + ); + return `${dayGroups.length}-${totalEvents}`; + }).join(','); + return `${activeTab}:${activeFilters.join(',')}:${contentHash}`; + }, [activeTab, activeFilters, groupedEntriesByDay]); const { changeActiveDay } = useActiveDaySync({ activeDay, @@ -220,7 +224,7 @@ export function useScheduleData(): UseScheduleDataResult { setActiveFilters([...withoutAll, label]); }; - const isInitialLoad = userLoading; + const isInitialLoad = userLoading; // only show loading state for inital rendering bc eventsLoading/personalEventsLoading causes non ui-friendly refresh const isError = Boolean(personalEventsError || eventsError); return { From 42707f79e3fa3db5f79db3f072b0a979d9b17807 Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Mon, 23 Mar 2026 15:42:51 -0700 Subject: [PATCH 12/21] typo and remove border --- app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx | 1 - .../(hackers)/_components/Schedule/scheduleEventStyles.ts | 2 +- app/(pages)/_hooks/useScheduleData.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx b/app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx index c80508d5..14fdecfa 100644 --- a/app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx +++ b/app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx @@ -99,7 +99,6 @@ export function CalendarItem({ {tags.map((tag) => (
= { ACTIVITIES: { bgColor: '#FFE2D5', textColor: '#52230C', - addButtonColor: '#FFD5C2', // integrated for calendar add buttons (not related to ACTIVITES) + addButtonColor: '#FFD5C2', // integrated for calendar add buttons (not related to ACTIVITIES) }, WORKSHOPS: { bgColor: '#E9FBBA', diff --git a/app/(pages)/_hooks/useScheduleData.ts b/app/(pages)/_hooks/useScheduleData.ts index 1066d6b5..49c3aeb0 100644 --- a/app/(pages)/_hooks/useScheduleData.ts +++ b/app/(pages)/_hooks/useScheduleData.ts @@ -224,7 +224,7 @@ export function useScheduleData(): UseScheduleDataResult { setActiveFilters([...withoutAll, label]); }; - const isInitialLoad = userLoading; // only show loading state for inital rendering bc eventsLoading/personalEventsLoading causes non ui-friendly refresh + const isInitialLoad = userLoading; // only show loading state for initial rendering bc eventsLoading/personalEventsLoading causes non ui-friendly refresh const isError = Boolean(personalEventsError || eventsError); return { From 3a946f678c598f7b55c2484da8b64af4a9531a4c Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Mon, 23 Mar 2026 16:12:56 -0700 Subject: [PATCH 13/21] add shared now hook and fix fixes --- .../HomeHacking/ScheduleSneakPeek.tsx | 59 ++++++++----------- .../_components/Schedule/DaySection.tsx | 8 +-- .../_components/Schedule/ScheduleControls.tsx | 3 +- app/(pages)/_hooks/useScheduleData.ts | 8 ++- app/(pages)/_hooks/useScheduleSharedNow.ts | 32 ++++++++++ 5 files changed, 67 insertions(+), 43 deletions(-) create mode 100644 app/(pages)/_hooks/useScheduleSharedNow.ts diff --git a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx index 9c7a3a56..27a9af66 100644 --- a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx +++ b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useMemo } from 'react'; +import { useMemo } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import CalendarItem from '@pages/(hackers)/_components/Schedule/CalendarItem'; @@ -8,6 +8,7 @@ import { EventEntry, useScheduleSneakPeekData, } from '../../../_hooks/useScheduleSneakPeekData'; +import { useSharedNow } from '@pages/_hooks/useScheduleSharedNow'; import sleeping_cow from '@public/hackers/hero/sleeping_cow.svg'; import duckbunny from '@public/hackers/scheduleSneakPeek/duck+bunny.svg'; @@ -39,38 +40,21 @@ function SectionLabel({ label }: { label: string }) { } function CountdownLabel({ targetTime }: { targetTime: number }) { - const [timeLeft, setTimeLeft] = useState({ - hours: 0, - minutes: 0, - seconds: 0, - }); - - useEffect(() => { - const calculateTimeLeft = () => { - const difference = targetTime - new Date().getTime(); - if (difference <= 0) { - return { hours: 0, minutes: 0, seconds: 0 }; - } - return { - hours: Math.floor(difference / (1000 * 60 * 60)), - minutes: Math.floor((difference / (1000 * 60)) % 60), - seconds: Math.floor((difference / 1000) % 60), - }; + const now = useSharedNow(); + + const timeLeft = useMemo(() => { + const diff = targetTime - now; + if (diff <= 0) return { h: 0, m: 0, s: 0 }; + return { + h: Math.floor(diff / 3600000), + m: Math.floor((diff / 60000) % 60), + s: Math.floor((diff / 1000) % 60), }; + }, [targetTime, now]); - setTimeLeft(calculateTimeLeft()); - const timer = setInterval(() => { - setTimeLeft(calculateTimeLeft()); - }, 1000); - - return () => clearInterval(timer); - }, [targetTime]); - - const label = `IN ${timeLeft.hours - .toString() - .padStart(2, '0')}:${timeLeft.minutes + const label = `IN ${timeLeft.h.toString().padStart(2, '0')}:${timeLeft.m .toString() - .padStart(2, '0')}:${timeLeft.seconds.toString().padStart(2, '0')}`; + .padStart(2, '0')}:${timeLeft.s.toString().padStart(2, '0')}`; return ; } @@ -107,17 +91,22 @@ function Panel({ )); const upcomingGroups = useMemo(() => { - const groups: { startTime: number; entries: EventEntry[] }[] = []; + const groupsMap = new Map(); for (const entry of upcomingEvents) { const startTime = new Date(entry.event.start_time).getTime(); - const existing = groups.find((g) => g.startTime === startTime); + const existing = groupsMap.get(startTime); if (existing) { - existing.entries.push(entry); + existing.push(entry); } else { - groups.push({ startTime, entries: [entry] }); + groupsMap.set(startTime, [entry]); } } - return groups.sort((a, b) => a.startTime - b.startTime); + return Array.from(groupsMap.entries()) + .map(([startTime, entries]) => ({ + startTime, + entries, + })) + .sort((a, b) => a.startTime - b.startTime); }, [upcomingEvents]); return ( diff --git a/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx b/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx index 7f01506e..7469bda9 100644 --- a/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx +++ b/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx @@ -49,11 +49,9 @@ export default function DaySection({
{dayTitle}{' '} - {dayTitle === 'May 9' ? ( - Saturday - ) : ( - Sunday - )} + + {targetDate.toLocaleDateString('en-US', { weekday: 'long' })} +
diff --git a/app/(pages)/(hackers)/_components/Schedule/ScheduleControls.tsx b/app/(pages)/(hackers)/_components/Schedule/ScheduleControls.tsx index f64e5554..95425a1d 100644 --- a/app/(pages)/(hackers)/_components/Schedule/ScheduleControls.tsx +++ b/app/(pages)/(hackers)/_components/Schedule/ScheduleControls.tsx @@ -37,7 +37,8 @@ export default function ScheduleControls({ const handleScroll = () => { setIsScrolled(window.scrollY > 110); }; - window.addEventListener('scroll', handleScroll); + const scrollOptions: AddEventListenerOptions = { passive: true }; + window.addEventListener('scroll', handleScroll, scrollOptions); return () => window.removeEventListener('scroll', handleScroll); }, []); diff --git a/app/(pages)/_hooks/useScheduleData.ts b/app/(pages)/_hooks/useScheduleData.ts index 49c3aeb0..d16e6935 100644 --- a/app/(pages)/_hooks/useScheduleData.ts +++ b/app/(pages)/_hooks/useScheduleData.ts @@ -192,7 +192,7 @@ export function useScheduleData(): UseScheduleDataResult { const contentHash = DAY_KEYS.map((dayKey) => { const dayGroups = groupedEntriesByDay[dayKey] || []; const totalEvents = dayGroups.reduce( - (sum, group) => sum + group.entries.length, + (sum, group) => sum + (group[1]?.length || 0), 0 ); return `${dayGroups.length}-${totalEvents}`; @@ -224,7 +224,11 @@ export function useScheduleData(): UseScheduleDataResult { setActiveFilters([...withoutAll, label]); }; - const isInitialLoad = userLoading; // only show loading state for initial rendering bc eventsLoading/personalEventsLoading causes non ui-friendly refresh + const isInitialLoad = + userLoading || + (activeTab === 'schedule' && + scheduleData === null && + (eventsLoading || personalEventsLoading)); // only show loading state for initial rendering; avoid non ui-friendly refresh after data has loaded const isError = Boolean(personalEventsError || eventsError); return { diff --git a/app/(pages)/_hooks/useScheduleSharedNow.ts b/app/(pages)/_hooks/useScheduleSharedNow.ts new file mode 100644 index 00000000..1d78696d --- /dev/null +++ b/app/(pages)/_hooks/useScheduleSharedNow.ts @@ -0,0 +1,32 @@ +'use client'; +import { useState, useEffect } from 'react'; + +let sharedNow = Date.now(); +let sharedNowInterval: ReturnType | null = null; +const subscribers = new Set<(now: number) => void>(); + +export function useSharedNow(): number { + const [now, setNow] = useState(sharedNow); + + useEffect(() => { + const callback = (latest: number) => setNow(latest); + subscribers.add(callback); + + if (sharedNowInterval === null) { + sharedNowInterval = setInterval(() => { + sharedNow = Date.now(); + subscribers.forEach((cb) => cb(sharedNow)); + }, 1000); + } + + return () => { + subscribers.delete(callback); + if (subscribers.size === 0 && sharedNowInterval) { + clearInterval(sharedNowInterval); + sharedNowInterval = null; + } + }; + }, []); + + return now; +} From 0c6655634cf8b6f687a49a946bd6cc9aae66946b Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Mon, 23 Mar 2026 16:27:20 -0700 Subject: [PATCH 14/21] update now time caculation to pst --- .../_components/Schedule/DaySection.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx b/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx index 7469bda9..da0ab943 100644 --- a/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx +++ b/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx @@ -21,15 +21,24 @@ export default function DaySection({ onRemoveFromSchedule, }: DaySectionProps) { const dayTitle = DAY_LABELS[dayKey].replace(/^MAY/, 'May'); - const now = new Date(); - const currentYear = now.getFullYear(); + const pacificNow = new Date( + new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles' }) + ); + const today = new Date( + pacificNow.getFullYear(), + pacificNow.getMonth(), + pacificNow.getDate() + ); const targetMonthIndex = 4; // May - - const today = new Date(currentYear, now.getMonth(), now.getDate()); - const targetDate = new Date(currentYear, targetMonthIndex, Number(dayKey)); + const targetDate = new Date( + pacificNow.getFullYear(), + targetMonthIndex, + Number(dayKey) + ); const msPerDay = 24 * 60 * 60 * 1000; const diff = Math.round((targetDate.getTime() - today.getTime()) / msPerDay); + const dayTag = diff === 0 ? 'Today' From a20dfac3d88788be2014fcc6e4bbdca69ba881b8 Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Mon, 23 Mar 2026 16:29:47 -0700 Subject: [PATCH 15/21] ts fix --- .../(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx index 27a9af66..7de068ce 100644 --- a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx +++ b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx @@ -143,7 +143,7 @@ function Panel({ href="/schedule" className="hover:brightness-[97%] hover:saturate-[140%]" > - {title == 'Your schedule' ? ( + {title === 'Your schedule' ? ( From a5aa451d287a5b185083d844f674e16b851aaa52 Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Mon, 23 Mar 2026 16:35:40 -0700 Subject: [PATCH 16/21] margin fix --- app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx b/app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx index 14fdecfa..3ca6b585 100644 --- a/app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx +++ b/app/(pages)/(hackers)/_components/Schedule/CalendarItem.tsx @@ -71,7 +71,7 @@ export function CalendarItem({ }`} >
-

+

{name}

From 3f635441ef3bd386fee9a5c0f9df0f88747db77c Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Mon, 23 Mar 2026 16:42:57 -0700 Subject: [PATCH 17/21] remove dedundant hook call --- .../_components/HomeHacking/ScheduleSneakPeek.tsx | 2 +- app/(pages)/_hooks/useScheduleData.ts | 2 -- app/(pages)/_hooks/useScheduleSneakPeekData.ts | 14 +++++--------- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx index 7de068ce..da45f00f 100644 --- a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx +++ b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx @@ -183,7 +183,7 @@ function Panel({ href="/schedule" className="hover:brightness-[97%] hover:saturate-[140%]" > - {title == 'Your schedule' ? ( + {title === 'Your schedule' ? ( diff --git a/app/(pages)/_hooks/useScheduleData.ts b/app/(pages)/_hooks/useScheduleData.ts index d16e6935..232595cc 100644 --- a/app/(pages)/_hooks/useScheduleData.ts +++ b/app/(pages)/_hooks/useScheduleData.ts @@ -89,7 +89,6 @@ export function useScheduleData(): UseScheduleDataResult { const success = await addToPersonalSchedule(eventId); if (success) { - await refreshPersonalEvents(); await refreshEvents(); if (activeTab === 'schedule') { @@ -102,7 +101,6 @@ export function useScheduleData(): UseScheduleDataResult { const success = await removeFromPersonalSchedule(eventId); if (success) { - await refreshPersonalEvents(); await refreshEvents(); if (activeTab === 'schedule') { diff --git a/app/(pages)/_hooks/useScheduleSneakPeekData.ts b/app/(pages)/_hooks/useScheduleSneakPeekData.ts index 0c36e7ae..53afb1a3 100644 --- a/app/(pages)/_hooks/useScheduleSneakPeekData.ts +++ b/app/(pages)/_hooks/useScheduleSneakPeekData.ts @@ -1,11 +1,12 @@ 'use client'; -import { useMemo, useState, useEffect } from 'react'; +import { useMemo } from 'react'; import Event from '@typeDefs/event'; import { useEvents } from '@hooks/useEvents'; import { usePersonalEvents } from '@hooks/usePersonalEvents'; import useActiveUser from '@pages/_hooks/useActiveUser'; import { isScheduleEventLive } from '@pages/(hackers)/_components/Schedule/scheduleTime'; +import { useSharedNow } from './useScheduleSharedNow'; export interface EventEntry { event: Event; @@ -63,14 +64,9 @@ export function useScheduleSneakPeekData() { }); }, [personalEvents, eventData]); - const [now, setNow] = useState(new Date()); - - useEffect(() => { - const interval = setInterval(() => { - setNow(new Date()); - }, 60000); // Update every 60 seconds - return () => clearInterval(interval); - }, []); + // Update "now" every second + const nowMs = useSharedNow(); + const now = useMemo(() => new Date(nowMs), [nowMs]); const filteredLists = useMemo(() => { // GENERAL (and MEALS) events have no add button and must never appear in From 2a1c7ea79f5d021b35861f8551b8e5fb28ae6e25 Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Mon, 23 Mar 2026 16:47:29 -0700 Subject: [PATCH 18/21] added more margin --- .../(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx index da45f00f..e13c6322 100644 --- a/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx +++ b/app/(pages)/(hackers)/_components/HomeHacking/ScheduleSneakPeek.tsx @@ -228,7 +228,7 @@ export default function ScheduleSneakPeek({
-
+
Date: Mon, 23 Mar 2026 16:54:18 -0700 Subject: [PATCH 19/21] save event seconds and populate events --- .../_components/Schedule/groupedEntries.ts | 3 ++- .../_hooks/useScheduleSneakPeekData.ts | 20 ++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/(pages)/(hackers)/_components/Schedule/groupedEntries.ts b/app/(pages)/(hackers)/_components/Schedule/groupedEntries.ts index beeb83b5..088ad196 100644 --- a/app/(pages)/(hackers)/_components/Schedule/groupedEntries.ts +++ b/app/(pages)/(hackers)/_components/Schedule/groupedEntries.ts @@ -40,7 +40,8 @@ export const getGroupedEntriesForDay = ( if (endA === null && endB !== null) return -1; if (endA !== null && endB === null) return 1; - if (endA !== null && endB !== null && endA !== endB) { + // Make sure events without end time populate first (ex: check in events) + if (endA !== null && endB !== null) { return endA - endB; } diff --git a/app/(pages)/_hooks/useScheduleSneakPeekData.ts b/app/(pages)/_hooks/useScheduleSneakPeekData.ts index 53afb1a3..eedfe74f 100644 --- a/app/(pages)/_hooks/useScheduleSneakPeekData.ts +++ b/app/(pages)/_hooks/useScheduleSneakPeekData.ts @@ -23,15 +23,17 @@ const toSorted = (events: EventEntry[]): EventEntry[] => /** Returns only the events starting at the single nearest future start time. */ const getNextBatchEvents = (entries: EventEntry[], now: Date): EventEntry[] => { - const future = entries.filter( - (e) => new Date(e.event.start_time).getTime() > now.getTime() - ); - if (future.length === 0) return []; - const earliest = Math.min( - ...future.map((e) => new Date(e.event.start_time).getTime()) - ); - return future.filter( - (e) => new Date(e.event.start_time).getTime() === earliest + const nowMs = now.getTime(); + let earliestMs = Number.MAX_SAFE_INTEGER; + for (const e of entries) { + const startMs = new Date(e.event.start_time).getTime(); + if (startMs > nowMs && startMs < earliestMs) { + earliestMs = startMs; + } + } + if (earliestMs === Number.MAX_SAFE_INTEGER) return []; + return entries.filter( + (e) => new Date(e.event.start_time).getTime() === earliestMs ); }; From f2ba101aeaec128d8a5c615e08ba29c3339579b0 Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Mon, 23 Mar 2026 17:07:54 -0700 Subject: [PATCH 20/21] revert loading condition --- app/(pages)/_hooks/useScheduleData.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/(pages)/_hooks/useScheduleData.ts b/app/(pages)/_hooks/useScheduleData.ts index 232595cc..2bd398d0 100644 --- a/app/(pages)/_hooks/useScheduleData.ts +++ b/app/(pages)/_hooks/useScheduleData.ts @@ -222,11 +222,7 @@ export function useScheduleData(): UseScheduleDataResult { setActiveFilters([...withoutAll, label]); }; - const isInitialLoad = - userLoading || - (activeTab === 'schedule' && - scheduleData === null && - (eventsLoading || personalEventsLoading)); // only show loading state for initial rendering; avoid non ui-friendly refresh after data has loaded + const isInitialLoad = userLoading; // only show loading state for initial rendering; avoid non ui-friendly refresh after data has loaded const isError = Boolean(personalEventsError || eventsError); return { From fc1754abc8bcc3d3dc9b6e25cfcae9e7b292bdcd Mon Sep 17 00:00:00 2001 From: michelleyeoh Date: Mon, 23 Mar 2026 17:20:50 -0700 Subject: [PATCH 21/21] update pst weekend and hard coded 9 --- app/(pages)/(hackers)/_components/Schedule/DaySection.tsx | 5 ++++- app/(pages)/_hooks/useScheduleData.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx b/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx index da0ab943..c505cafd 100644 --- a/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx +++ b/app/(pages)/(hackers)/_components/Schedule/DaySection.tsx @@ -59,7 +59,10 @@ export default function DaySection({
{dayTitle}{' '} - {targetDate.toLocaleDateString('en-US', { weekday: 'long' })} + {targetDate.toLocaleDateString('en-US', { + weekday: 'long', + timeZone: 'America/Los_Angeles', + })}
diff --git a/app/(pages)/_hooks/useScheduleData.ts b/app/(pages)/_hooks/useScheduleData.ts index 2bd398d0..7f2e7244 100644 --- a/app/(pages)/_hooks/useScheduleData.ts +++ b/app/(pages)/_hooks/useScheduleData.ts @@ -59,7 +59,7 @@ export function useScheduleData(): UseScheduleDataResult { const [activeTab, setActiveTab] = useState<'schedule' | 'personal'>( 'schedule' ); - const [activeDay, setActiveDay] = useState('9'); + const [activeDay, setActiveDay] = useState(DAY_KEYS[0]); const [activeFilters, setActiveFilters] = useState(['ALL']); const [isMobileFilterOpen, setIsMobileFilterOpen] = useState(false); const [scheduleData, setScheduleData] = useState(null);