Skip to content

Commit 54c7f4b

Browse files
authored
Merge pull request #273 from NIAEFEUP/fix/mobile-professor-dropdown-responsive
feat: mobile professor dropdown menu is responsive
2 parents 5f794c6 + 3ff68be commit 54c7f4b

File tree

3 files changed

+316
-193
lines changed

3 files changed

+316
-193
lines changed

src/components/planner/sidebar/CoursesController.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Button } from '../../ui/button'
99

1010

1111
const CoursesController = () => {
12-
const { pickedCourses,setUcsModalOpen } = useContext(CourseContext);
12+
const { pickedCourses, setUcsModalOpen } = useContext(CourseContext);
1313

1414
const noCoursesPicked = pickedCourses.length === 0;
1515
return (

src/components/planner/sidebar/CoursesController/ClassSelector.tsx

+27-192
Original file line numberDiff line numberDiff line change
@@ -9,72 +9,31 @@ import { teacherIdsFromCourseInfo, uniqueTeachersFromCourseInfo, getAllPickedSlo
99
import { Button } from '../../../ui/button'
1010
import ProfessorItem from './ProfessorItem'
1111
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from '../../../ui/dropdown-menu'
12+
import ClassSelectorDropdownController from './ClassSelectorDropdownController'
1213

1314
type Props = {
1415
course: CourseInfo
1516
}
1617

17-
const buildTeacherFilters = (teachers, filteredTeachers) => {
18-
if (!filteredTeachers) return [];
19-
20-
return teachers.map((teacher) => {
21-
return {
22-
...teacher,
23-
isFiltered: filteredTeachers.includes(teacher.id)
24-
}
25-
})
26-
}
27-
2818
//TODO: Check this code, not too good
2919
const ClassSelector = ({ course }: Props) => {
3020
const classSelectorTriggerRef = useRef(null)
3121
const classSelectorContentRef = useRef(null)
3222
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
3323

3424
const { multipleOptions, setMultipleOptions, selectedOption } = useContext(MultipleOptionsContext)
35-
const { pickedCourses, choosingNewCourse } = useContext(CourseContext)
3625

3726
const [selectedClassId, setSelectedClassId] = useState<number | null>(null);
3827

3928
const courseOption: CourseOption = multipleOptions[selectedOption].course_options.find((opt) => opt.course_id === course.id)
4029
if (courseOption)
4130
courseOption.filteredTeachers = [...teacherIdsFromCourseInfo(course)];
4231

43-
/**
44-
* This is used to retrieve the teachers from a course and to populate the filter of the teachers
45-
* which is the dropdown menu that appears by clicking on "Professores" on the class selector dropdown
46-
*/
47-
const teachers = useMemo(() => {
48-
if (!course.classes) return []
49-
50-
return uniqueTeachersFromCourseInfo(course);
51-
}, [course.classes])
52-
53-
// This is used to store the ids of the teachers so it is easy to verify if a teacher is filtered or not
54-
const [filteredTeachers, setFilteredTeachers] = useState(teacherIdsFromCourseInfo(course));
55-
56-
useEffect(() => {
57-
setFilteredTeachers(teacherIdsFromCourseInfo(course));
58-
}, [pickedCourses])
59-
60-
// This is used as an object with the teacher properties in order for us to being able
61-
// to show teacher information on the filter dropdown menu
62-
const [teacherFilters, setTeacherFilters] = useState(() => {
63-
return buildTeacherFilters(teachers, filteredTeachers);
64-
});
65-
6632
const [locked, setLocked] = useState(courseOption?.locked)
6733

6834
const [preview, setPreview] = useState<number | null>(null)
6935
const [display, setDisplay] = useState(courseOption?.picked_class_id)
7036

71-
const deleteOption = () => {
72-
const multipleOptionsEntry = multipleOptions[selectedOption].course_options.find((option) => option.picked_class_id === selectedClassId);
73-
multipleOptionsEntry.picked_class_id = null;
74-
setSelectedClassId(null);
75-
setMultipleOptions([...multipleOptions]);
76-
}
77-
7837
useEffect(() => {
7938
const course_options = multipleOptions[selectedOption].course_options;
8039
const option = course_options.filter((option) => option.course_id === course.id && option.picked_class_id !== null)
@@ -88,21 +47,6 @@ const ClassSelector = ({ course }: Props) => {
8847
setDisplay(option[0].picked_class_id);
8948
}, [selectedOption, multipleOptions, course.id]);
9049

91-
useEffect(() => {
92-
setTeacherFilters(() => {
93-
return buildTeacherFilters(teachers, filteredTeachers);
94-
});
95-
}, [filteredTeachers])
96-
97-
/**
98-
* This useEffect is used to make the dropdown content width match the trigger width
99-
*/
100-
useEffect(() => {
101-
if (classSelectorTriggerRef.current && classSelectorContentRef.current) {
102-
classSelectorContentRef.current.style.width = `${classSelectorTriggerRef.current.offsetWidth}px`
103-
}
104-
}, [isDropdownOpen])
105-
10650
const toggleLocker = () => {
10751
const newMultipleOptions = [...multipleOptions];
10852
const courseOptions = newMultipleOptions[selectedOption].course_options.map(opt => {
@@ -120,44 +64,6 @@ const ClassSelector = ({ course }: Props) => {
12064
setLocked(courseOption?.locked)
12165
}, [selectedOption]);
12266

123-
//(thePeras): Classes options should be a new state
124-
/**
125-
* Return the classes options filtered by the selected teachers
126-
* Classes with at least one of its teachers selected will be returned
127-
*/
128-
const getOptions = (): Array<ClassInfo> => {
129-
return course.classes?.filter((c) => {
130-
return c.slots.some((slot) => slot.professors.filter((prof) => filteredTeachers?.includes(prof.id)).length > 0)
131-
})
132-
}
133-
134-
useEffect(() => {
135-
setFilteredTeachers(courseOption?.filteredTeachers);
136-
}, [choosingNewCourse])
137-
138-
// Checks if any of the selected classes have time conflicts with the classInfo
139-
// This is used to display a warning icon in each class of the dropdown in case of conflicts
140-
const timesCollideWithSelected = (classInfo: ClassInfo) => {
141-
const pickedSlots = getAllPickedSlots(pickedCourses, multipleOptions[selectedOption])
142-
return pickedSlots.some((slot) => classInfo.slots.some((currentSlot) => schedulesConflict(slot, currentSlot)))
143-
}
144-
145-
// Puts inside the preview the actual selected class so we can then restore it later after the user stops
146-
// previewing
147-
const showPreview = (classInfo: ClassInfo) => {
148-
const newMultipleOptions = [...multipleOptions];
149-
const newCourseOptions: CourseOption[] = newMultipleOptions[selectedOption].course_options.map((c: CourseOption) => {
150-
if (c.course_id === course.id) {
151-
setPreview(classInfo.id)
152-
c.picked_class_id = classInfo.id
153-
}
154-
155-
return c;
156-
});
157-
158-
newMultipleOptions[selectedOption].course_options = newCourseOptions;
159-
setMultipleOptions(newMultipleOptions)
160-
}
16167

16268
// Restores into multiple options the picked_class_id prior to when the user started previewing
16369
const removePreview = () => {
@@ -177,24 +83,8 @@ const ClassSelector = ({ course }: Props) => {
17783
setPreview(null);
17884
}
17985

180-
function toggleTeacher(id: number) {
181-
if (filteredTeachers.includes(id)) {
182-
setFilteredTeachers(filteredTeachers.filter((t) => t !== id))
183-
} else {
184-
setFilteredTeachers([...filteredTeachers, id])
185-
}
186-
}
187-
188-
function toggleAllTeachers(teachers: ProfessorInfo[]) {
189-
if (filteredTeachers.length > 0) {
190-
setFilteredTeachers([])
191-
} else {
192-
setFilteredTeachers(teachers.flatMap((t) => t.id))
193-
}
194-
}
195-
19686
return (
197-
<div className="relative text-sm" key={`course-option-${course.acronym}`}>
87+
<div className="text-sm" key={`course-option-${course.acronym}`}>
19888
{/* Header */}
19989
<p className="mb-0.5 flex text-xs">
20090
<strong>{course.acronym}</strong>
@@ -209,87 +99,32 @@ const ClassSelector = ({ course }: Props) => {
20999
removePreview();
210100
}
211101
}}>
212-
<DropdownMenuTrigger asChild disabled={courseOption?.locked} ref={classSelectorTriggerRef}>
213-
<Button
214-
variant="outline"
215-
size="sm"
216-
className="w-full justify-between truncate bg-lightish text-xs font-normal tracking-tighter hover:bg-primary/75 hover:text-white dark:bg-darkish"
102+
<div className="w-full">
103+
<DropdownMenuTrigger asChild disabled={courseOption?.locked} ref={classSelectorTriggerRef}>
104+
<Button
105+
variant="outline"
106+
size="sm"
107+
className="w-full justify-between truncate bg-lightish text-xs font-normal tracking-tighter hover:bg-primary/75 hover:text-white dark:bg-darkish"
108+
>
109+
<span className={`${selectedClassId === null ? "opacity-50" : ""}`}>{getClassDisplayText(course, selectedClassId)}</span>
110+
{!courseOption?.locked && <ChevronUpDownIcon className="text-blackish h-6 w-6 dark:text-lightish" />}
111+
</Button>
112+
</DropdownMenuTrigger>
113+
<DropdownMenuContent
114+
className="bg-lightish text-darkish dark:bg-darkish dark:text-lightish"
115+
ref={classSelectorContentRef}
217116
>
218-
<span className={`${selectedClassId === null ? "opacity-50" : ""}`}>{getClassDisplayText(course, selectedClassId)}</span>
219-
{!courseOption?.locked && <ChevronUpDownIcon className="text-blackish h-6 w-6 dark:text-lightish" />}
220-
</Button>
221-
</DropdownMenuTrigger>
222-
<DropdownMenuContent
223-
className="bg-lightish text-darkish dark:bg-darkish dark:text-lightish"
224-
ref={classSelectorContentRef}
225-
>
226-
{course.classes === null ? (
227-
<p className="w-100 select-none p-2 text-center">A carregar as aulas...</p>
228-
) : (
229-
<>
230-
<DropdownMenuGroup>
231-
<DropdownMenuSub>
232-
<DropdownMenuSubTrigger>
233-
<User className="mr-2 h-4 w-4" />
234-
<span>Professores</span>
235-
</DropdownMenuSubTrigger>
236-
<DropdownMenuPortal>
237-
<DropdownMenuSubContent className="w-80 bg-lightish text-darkish dark:bg-darkish dark:text-lightish">
238-
<DropdownMenuItem
239-
onClick={(e) => {
240-
e.preventDefault()
241-
toggleAllTeachers(teachers)
242-
}}
243-
>
244-
<span className="block truncate dark:text-white">
245-
{filteredTeachers?.length > 0 ? 'Apagar todos' : 'Selecionar Todos'}
246-
</span>
247-
</DropdownMenuItem>
248-
<DropdownMenuSeparator />
249-
{teacherFilters.map((option) => {
250-
return (
251-
<ProfessorItem
252-
key={`${course.acronym}-teacher-${option.acronym}`}
253-
professorInformation={option}
254-
filtered={option.isFiltered}
255-
onSelect={(e) => {
256-
e.preventDefault()
257-
toggleTeacher(option.id)
258-
}}
259-
/>
260-
)
261-
})}
262-
</DropdownMenuSubContent>
263-
</DropdownMenuPortal>
264-
</DropdownMenuSub>
265-
</DropdownMenuGroup>
266-
<DropdownMenuSeparator />
267-
<DropdownMenuGroup className="max-h-96 overflow-y-auto">
268-
<DropdownMenuItem onSelect={() => deleteOption()}>
269-
<span className="text-sm tracking-tighter">Remover Seleção</span>
270-
</DropdownMenuItem>
271-
{course.classes &&
272-
getOptions().map((classInfo) => (
273-
<ClassItem
274-
key={`schedule-${classInfo.name}`}
275-
course_id={course.id}
276-
classInfo={classInfo}
277-
displayed={display === classInfo.id}
278-
checked={selectedOption === classInfo.id}
279-
preview={preview}
280-
conflict={timesCollideWithSelected(classInfo)}
281-
onSelect={() => {
282-
setSelectedClassId(classInfo.id)
283-
setPreview(null)
284-
}}
285-
onMouseEnter={() => showPreview(classInfo)}
286-
onMouseLeave={() => removePreview()}
287-
/>
288-
))}
289-
</DropdownMenuGroup>
290-
</>
291-
)}
292-
</DropdownMenuContent>
117+
<ClassSelectorDropdownController
118+
course={course}
119+
selectedClassIdHook={[selectedClassId, setSelectedClassId]}
120+
previewHook={[preview, setPreview]}
121+
display={display}
122+
removePreview={removePreview}
123+
contentRef={classSelectorContentRef}
124+
triggerRef={classSelectorTriggerRef}
125+
/>
126+
</DropdownMenuContent>
127+
</div>
293128
</DropdownMenu>
294129

295130
{/* Lock Button */}

0 commit comments

Comments
 (0)