@@ -5,6 +5,8 @@ import { useConvexAuth, usePaginatedQuery, useQuery } from "convex/react";
55import { CalendarIcon , ListIcon } from "lucide-react" ;
66import { useEffect , useRef , useState } from "react" ;
77import { useNextTerm , useNextYear } from "@/components/AppConfigProvider" ;
8+ import { Label } from "@/components/ui/label" ;
9+ import { Switch } from "@/components/ui/switch" ;
810import ViewSelector from "@/components/ViewSelector" ;
911import { useSearchParam } from "@/hooks/use-search-param" ;
1012import { CourseSelector } from "@/modules/course-selection" ;
@@ -14,6 +16,7 @@ import type {
1416 CourseOfferingWithCourse ,
1517} from "@/modules/course-selection/types" ;
1618import {
19+ type Class ,
1720 getUserClassesByTerm ,
1821 ScheduleCalendar ,
1922} from "@/modules/schedule-calendar/schedule-calendar" ;
@@ -26,9 +29,50 @@ const RegisterPage = () => {
2629 const [ hoveredCourse , setHoveredCourse ] = useState < CourseOffering | null > (
2730 null ,
2831 ) ;
32+ const [ selectedCourse , setSelectedCourse ] = useState < Class | null > ( null ) ;
2933 const [ mobileView , setMobileView ] = useState < "selector" | "calendar" > (
3034 "selector" ,
3135 ) ;
36+ const [ previousMobileView , setPreviousMobileView ] = useState <
37+ "selector" | "calendar"
38+ > ( "selector" ) ;
39+ const [ isMobile , setIsMobile ] = useState ( false ) ;
40+
41+ // TODO: save the state to cookie
42+ const [ showAlternatives , setShowAlternatives ] = useState ( true ) ;
43+
44+ useEffect ( ( ) => {
45+ const checkMobile = ( ) => {
46+ setIsMobile ( window . innerWidth < 768 ) ;
47+ } ;
48+
49+ checkMobile ( ) ;
50+ window . addEventListener ( "resize" , checkMobile ) ;
51+ return ( ) => window . removeEventListener ( "resize" , checkMobile ) ;
52+ } , [ ] ) ;
53+
54+ useEffect ( ( ) => {
55+ if ( selectedCourse && isMobile && mobileView === "calendar" ) {
56+ setPreviousMobileView ( "calendar" ) ;
57+ setMobileView ( "selector" ) ;
58+ }
59+ } , [ selectedCourse , isMobile , mobileView ] ) ;
60+
61+ const handleCourseSelect = ( course : Class | null ) => {
62+ if ( ! course && isMobile && previousMobileView === "calendar" ) {
63+ // When closing detail panel on mobile, return to calendar view
64+ setMobileView ( "calendar" ) ;
65+ }
66+ setSelectedCourse ( course ) ;
67+ } ;
68+
69+ // clear selected course when switching tabs
70+ const handleMobileViewChange = ( view : "selector" | "calendar" ) => {
71+ setMobileView ( view ) ;
72+ if ( view === "calendar" && selectedCourse ) {
73+ setSelectedCourse ( null ) ;
74+ }
75+ } ;
3276
3377 // Search param state with debouncing and URL sync
3478 const { searchValue, setSearchValue, debouncedSearchValue } = useSearchParam ( {
@@ -68,7 +112,16 @@ const RegisterPage = () => {
68112 }
69113 } , [ results , debouncedSearchValue , status ] ) ;
70114
71- const classes = getUserClassesByTerm ( allClasses , currentYear , currentTerm ) ;
115+ const allClassesForTerm = getUserClassesByTerm (
116+ allClasses ,
117+ currentYear ,
118+ currentTerm ,
119+ ) ;
120+
121+ // Filter out alternatives if toggle is off
122+ const classes = showAlternatives
123+ ? allClassesForTerm
124+ : allClassesForTerm ?. filter ( ( c ) => ! c . alternativeOf ) ;
72125
73126 const isSearching =
74127 status === "LoadingFirstPage" &&
@@ -84,13 +137,32 @@ const RegisterPage = () => {
84137 return < CourseSelectorSkeleton /> ;
85138 }
86139
140+ const AltToggle = ( ) => (
141+ < >
142+ < Switch
143+ id = "alt-switcher"
144+ className = "order-1 h-4 w-6 after:absolute after:inset-0 [&_span]:size-3 data-[state=checked]:[&_span]:translate-x-2 data-[state=checked]:[&_span]:rtl:-translate-x-2"
145+ checked = { showAlternatives }
146+ onCheckedChange = { setShowAlternatives }
147+ />
148+ < div className = "grid grow gap-2" >
149+ < Label htmlFor = "alt-switcher" > Show alternative courses</ Label >
150+ < p className = "text-xs text-muted-foreground" >
151+ You can set one course as alternative for another.
152+ </ p >
153+ </ div >
154+ </ >
155+ ) ;
156+
87157 return (
88158 < div className = "flex flex-col gap-4 w-full" >
89159 { /* Mobile toggle buttons */ }
90160 < div className = "md:hidden shrink-0 p-2" >
91161 < ViewSelector
92162 value = { mobileView }
93- onValueChange = { setMobileView }
163+ onValueChange = { ( val ) =>
164+ handleMobileViewChange ( val as "selector" | "calendar" )
165+ }
94166 tabs = { [
95167 { value : "selector" , label : "Courses" , icon : ListIcon } ,
96168 { value : "calendar" , label : "Schedule" , icon : CalendarIcon } ,
@@ -109,31 +181,53 @@ const RegisterPage = () => {
109181 loadMore = { loadMore }
110182 status = { status }
111183 isSearching = { isSearching }
184+ selectedCourse = { selectedCourse }
185+ onCourseSelect = { handleCourseSelect }
112186 selectedClassNumbers = { selectedClassNumbers }
113187 />
114188 ) : (
115- < div className = "h-full" >
116- < ScheduleCalendar classes = { classes } hoveredCourse = { hoveredCourse } />
189+ < div className = "h-full flex flex-col space-y-2" >
190+ < div className = "md:hidden relative flex w-full items-start gap-2 rounded-md border border-input p-4 shadow-xs outline-none has-data-[state=checked]:border-primary/50" >
191+ < AltToggle />
192+ </ div >
193+ < ScheduleCalendar
194+ classes = { classes }
195+ hoveredCourse = { hoveredCourse }
196+ selectedCourse = { selectedCourse }
197+ onCourseSelect = { handleCourseSelect }
198+ />
117199 </ div >
118200 ) }
119201 </ div >
120202
121203 { /* Desktop view */ }
122204 < div className = "hidden md:flex gap-4 flex-1 min-h-0" >
123- < CourseSelector
124- courseOfferingsWithCourses = { displayedResults }
125- onHover = { setHoveredCourse }
126- onSearchChange = { setSearchValue }
127- searchQuery = { searchValue }
128- loadMore = { loadMore }
129- status = { status }
130- isSearching = { isSearching }
131- selectedClassNumbers = { selectedClassNumbers }
132- />
205+ < div className = "flex flex-col space-y-4" >
206+ < div className = "relative flex w-full items-start gap-2 rounded-md border border-input p-4 shadow-xs outline-none has-data-[state=checked]:border-primary/50" >
207+ < AltToggle />
208+ </ div >
209+ < CourseSelector
210+ courseOfferingsWithCourses = { displayedResults }
211+ onHover = { setHoveredCourse }
212+ onSearchChange = { setSearchValue }
213+ searchQuery = { searchValue }
214+ loadMore = { loadMore }
215+ status = { status }
216+ isSearching = { isSearching }
217+ selectedCourse = { selectedCourse }
218+ onCourseSelect = { handleCourseSelect }
219+ selectedClassNumbers = { selectedClassNumbers }
220+ />
221+ </ div >
133222
134223 < div className = "flex-1 min-w-0" >
135224 < div className = "sticky top-0" >
136- < ScheduleCalendar classes = { classes } hoveredCourse = { hoveredCourse } />
225+ < ScheduleCalendar
226+ classes = { classes }
227+ hoveredCourse = { hoveredCourse }
228+ selectedCourse = { selectedCourse }
229+ onCourseSelect = { handleCourseSelect }
230+ />
137231 </ div >
138232 </ div >
139233 </ div >
0 commit comments