Skip to content

Commit

Permalink
Improved Course Selection UI with better scrolling and loading behavio
Browse files Browse the repository at this point in the history
  • Loading branch information
ImJustChew committed Oct 25, 2023
1 parent 76622f7 commit 1b00fd8
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 102 deletions.
211 changes: 110 additions & 101 deletions src/app/[lang]/courses/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import CourseListItem from "@/components/Courses/CourseListItem";
import InputControl from "@/components/FormComponents/InputControl";
import supabase, { CourseDefinition } from "@/config/supabase";
import { Button, Divider, Drawer, IconButton, LinearProgress, Stack } from "@mui/joy";
import { Button, CircularProgress, Divider, Drawer, IconButton, LinearProgress, Stack } from "@mui/joy";
import { NextPage } from "next";
import { useEffect, useState, Fragment, useRef, use, useMemo } from "react";
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Filter, X } from "react-feather";
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Filter, Search, X } from "react-feather";
import { useForm } from "react-hook-form";
import { useDebouncedCallback } from "use-debounce";
import { useMediaQuery } from 'usehooks-ts';
Expand Down Expand Up @@ -107,81 +107,80 @@ const CoursePage: NextPage = () => {
})
}

const searchQuery = (filters: RefineControlFormTypes, index: number = 0) => {
const searchQuery = async (filters: RefineControlFormTypes, index: number = 0) => {
console.log("current filters", filters);
scrollRef.current?.scrollTo(0, 0);
(async () => {
setLoading(true);
//Query for courses
try {
let temp = supabase
.from('courses')
.select('*', { count: 'exact' })
if (filters.textSearch) {
temp = temp
.textSearch('multilang_search', `'${filters.textSearch.split(' ').join("' & '")}'`)
}
if (filters.level.length)
temp = temp
.or(filters.level.map(level => `and(course.gte.${level}000,course.lte.${level}999)`).join(','))
if (filters.language.length)
temp = temp
.or(filters.language.map(lang => `language.eq.${lang}`).join(','))
if (filters.department.length)
temp = temp
.in('department', filters.department.map(({ code }) => code))
if (filters.className)
temp = temp
.or(`compulsory_for.cs.{"${filters.className}"},elective_for.cs.{"${filters.className}"}`);
if (filters.others.includes('xclass'))
temp = temp
.textSearch(`備註`, `'X-Class'`)
if (filters.others.includes('extra_selection'))
temp = temp
.eq('no_extra_selection', false)
if (filters.firstSpecialization || filters.secondSpecialization) {
temp = temp
.or(`first_specialization.cs.{"${filters.firstSpecialization ?? ""}"},second_specialization.cs.{"${filters.secondSpecialization ?? ""}"}`)
}
if (filters.venues.length) {
temp = temp
.containedBy('venues', filters.venues)
}
if (filters.disciplines.length) {
temp = temp
.containedBy('cross_discipline', filters.disciplines)
}
if (filters.gecDimensions.length) {
temp = temp
.in('ge_type', filters.gecDimensions) //TODO: should consider changing name to gec_type
}
if (filters.geTarget.length) {
temp = temp
.in('ge_target', filters.geTarget)
}
if (filters.timeslots.length) {
console.log(filters.timeslots)
setLoading(true);
//Query for courses
try {
let temp = supabase
.from('courses')
.select('*', { count: 'exact' })
if (filters.textSearch) {
temp = temp
.textSearch('multilang_search', `'${filters.textSearch.split(' ').join("' & '")}'`)
}
if (filters.level.length)
temp = temp
.or(filters.level.map(level => `and(course.gte.${level}000,course.lte.${level}999)`).join(','))
if (filters.language.length)
temp = temp
.or(filters.language.map(lang => `language.eq.${lang}`).join(','))
if (filters.department.length)
temp = temp
.in('department', filters.department.map(({ code }) => code))
if (filters.className)
temp = temp
.or(`compulsory_for.cs.{"${filters.className}"},elective_for.cs.{"${filters.className}"}`);
if (filters.others.includes('xclass'))
temp = temp
.textSearch(`備註`, `'X-Class'`)
if (filters.others.includes('extra_selection'))
temp = temp
.containedBy('time_slots', filters.timeslots)
// .overlaps('time_slots', filters.timeslots) //Overlap works if only one of their timeslots is selected
}

let { data: courses, error, count } = await temp.order('raw_id', { ascending: true }).range(index, index + 29)
// move scroll to top
setTotalCount(count ?? 0)
if (error) console.error(error);
else {
console.log(courses)
setCourses(courses!);
setHeadIndex(index);
}
.eq('no_extra_selection', false)
if (filters.firstSpecialization || filters.secondSpecialization) {
temp = temp
.or(`first_specialization.cs.{"${filters.firstSpecialization ?? ""}"},second_specialization.cs.{"${filters.secondSpecialization ?? ""}"}`)
}
if (filters.venues.length) {
temp = temp
.containedBy('venues', filters.venues)
}
catch (e) {
console.error(e);
if (filters.disciplines.length) {
temp = temp
.containedBy('cross_discipline', filters.disciplines)
}
setLoading(false);
})()
if (filters.gecDimensions.length) {
temp = temp
.in('ge_type', filters.gecDimensions) //TODO: should consider changing name to gec_type
}
if (filters.geTarget.length) {
temp = temp
.in('ge_target', filters.geTarget)
}
if (filters.timeslots.length) {
console.log(filters.timeslots)
temp = temp
.containedBy('time_slots', filters.timeslots)
// .overlaps('time_slots', filters.timeslots) //Overlap works if only one of their timeslots is selected
}

let { data: courses, error, count } = await temp.order('raw_id', { ascending: true }).range(index, index + 29)
// move scroll to top
setTotalCount(count ?? 0)
if (error) console.error(error);
else {
console.log(courses)
setCourses(courses!);
setHeadIndex(index);
}
}
catch (e) {
console.error(e);
}
setLoading(false);
}

const searchQueryFunc = useDebouncedCallback(searchQuery, 1000);

const handleClear = () => {
Expand All @@ -190,6 +189,7 @@ const CoursePage: NextPage = () => {

//Trigger Search When Filters Change
useEffect(() => {
setLoading(true);
searchQueryFunc(filters);
//Save filters to URL
//But we have to handle geTarget and department differently, special cases where have nested objects
Expand All @@ -202,14 +202,19 @@ const CoursePage: NextPage = () => {
}, [JSON.stringify(filters)])


return (
<div className="grid grid-cols-1 md:grid-cols-[auto_320px] w-full h-full overflow-hidden">
<div className="flex flex-col w-full h-full overflow-hidden space-y-5 px-2 pt-2">
return (<>
{/* <div className="grid grid-cols-1 md:grid-cols-[auto_320px] w-full h-full overflow-hidden"> */}
<div className="flex flex-col w-full h-screen overflow-auto space-y-5 px-2 pt-2 md:pr-[320px] no-scrollbar scroll-smooth" ref={scrollRef}>
<InputControl
control={control}
name="textSearch"
placeholder={dict.course.list.search_placeholder}
variant="soft"
size="lg"
sx={{ position: 'sticky', top: 0, zIndex: 10 }}
startDecorator={
loading? <CircularProgress size="sm"/>: <Search/>
}
endDecorator={
<Fragment>
{filters.textSearch.length > 0 && <IconButton onClick={() => setValue('textSearch', "")}>
Expand All @@ -226,33 +231,36 @@ const CoursePage: NextPage = () => {
</Fragment>
}
/>
<div className="flex flex-col w-full h-full overflow-auto space-y-5 pb-8 scroll-smooth" ref={scrollRef}>
<div className="flex flex-row justify-between px-3 py-1 border-b">
<h6 className="text-gray-600">{dict.course.list.courses}</h6>
<h6 className="text-gray-600">{dict.course.list.found}: {totalCount} {dict.course.list.courses}</h6>
<div className="relative">
{/* loading covers all with white cover */}
{loading && <div className="absolute inset-0 bg-white/60 dark:bg-neutral-900/60 z-10"></div>}
<div className="flex flex-col w-full h-full space-y-5 pb-8">
<div className="flex flex-row justify-between px-3 py-1 border-b">
<h6 className="text-gray-600">{dict.course.list.courses}</h6>
<h6 className="text-gray-600">{dict.course.list.found}: {totalCount} {dict.course.list.courses}</h6>
</div>
{courses.map((course, index) => (
<CourseListItem key={index} course={course} />
))}
<Stack
direction="row"
justifyContent="center"
>
{currentPage != 1 && <IconButton onClick={() => searchQuery(filters, 0)}>
<ChevronsLeft />
</IconButton>}
{currentPage != 1 && <IconButton onClick={() => searchQuery(filters, headIndex - 30)}>
<ChevronLeft />
</IconButton>}
{renderPagination()}
{currentPage != totalPage && <IconButton onClick={() => searchQuery(filters, headIndex + 30)}>
<ChevronRight />
</IconButton>}
{currentPage != totalPage && <IconButton onClick={() => searchQuery(filters, (totalPage - 1) * 30)}>
<ChevronsRight />
</IconButton>}
</Stack>
</div>
{loading && <div className="w-full"><LinearProgress /> </div>}
{courses.map((course, index) => (
<CourseListItem key={index} course={course} />
))}
<Stack
direction="row"
justifyContent="center"
>
{currentPage != 1 && <IconButton onClick={() => searchQuery(filters, 0)}>
<ChevronsLeft />
</IconButton>}
{currentPage != 1 && <IconButton onClick={() => searchQuery(filters, headIndex - 30)}>
<ChevronLeft />
</IconButton>}
{renderPagination()}
{currentPage != totalPage && <IconButton onClick={() => searchQuery(filters, headIndex + 30)}>
<ChevronRight />
</IconButton>}
{currentPage != totalPage && <IconButton onClick={() => searchQuery(filters, (totalPage - 1) * 30)}>
<ChevronsRight />
</IconButton>}
</Stack>
</div>
</div>
{!isMobile && <RefineControls control={control} onClear={handleClear} />}
Expand All @@ -273,7 +281,8 @@ const CoursePage: NextPage = () => {
>
<RefineControls control={control} onClear={handleClear}/>
</Drawer>}
</div>
{/* </div> */}
</>

)
}
Expand Down
15 changes: 15 additions & 0 deletions src/app/[lang]/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,18 @@

body {
}

@layer utilities {
@variants responsive {
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}

/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
}
2 changes: 1 addition & 1 deletion src/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function RootLayout({
<body className={`${inter.className} grid grid-cols-1 grid-rows-[64px_40px_calc(100vh-108px)] md:grid-cols-[12rem_auto] md:grid-rows-[64px_calc(100vh-64px)_12rem] bg-white dark:bg-neutral-900 dark:text-white`}>
<Header/>
<SideNav/>
<main className='overflow-auto'>
<main className='overflow-auto h-full w-full'>
{children}
<Analytics />
</main>
Expand Down

0 comments on commit 1b00fd8

Please sign in to comment.