Skip to content

Commit

Permalink
Merge pull request #101 from SJSUCSClub/94-feature-compare-page
Browse files Browse the repository at this point in the history
[Feature] compare page
  • Loading branch information
chrehall68 authored Sep 30, 2024
2 parents 575c53b + 9a93074 commit f3aae87
Show file tree
Hide file tree
Showing 12 changed files with 328 additions and 38 deletions.
97 changes: 64 additions & 33 deletions app/(main)/compare/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Card, LinkBtn } from '@/components/atoms';
import { Btn, Card, LinkBtn } from '@/components/atoms';
import { CompareItem, FilterGroup, SearchBar } from '@/components/molecules';
import { BarChart } from '@/components/organisms';
import {
Expand All @@ -8,6 +8,7 @@ import {
} from '@/types';
import fetcher from '@/utils/fetcher';
import roundToTenth from '@/utils/round-to-tenth';
import { EllipsisVerticalIcon, XMarkIcon } from '@heroicons/react/24/solid';

export default async function Page({
searchParams,
Expand Down Expand Up @@ -108,12 +109,34 @@ export default async function Page({

return (
<main>
<section className="mx-auto flex w-full max-w-content-width items-stretch gap-md px-md">
<div className="w-[250px] max-lg:hidden">
<div className="sticky top-0 flex max-h-[100dvh] min-h-[50dvh] w-full flex-col gap-sm overflow-y-auto pb-lg pt-lg">
<p className="pb-md">
Search and select professors and/or courses to compare
</p>
<div className="mx-auto flex w-full max-w-content-width px-md py-lg">
<p className="flex-1">Grade/Rating Analysis</p>
<Btn
popoverTarget="filters"
variant="tertiary"
className="rounded-sm p-0 lg:hidden"
>
<EllipsisVerticalIcon width={24} height={24} />
</Btn>
</div>
<section className="mx-auto flex w-full max-w-content-width items-stretch px-md">
<div className="lg:w-[250px] lg:pr-md">
<div
id="filters"
popover="auto"
className="top-0 max-h-[100dvh] min-h-[50dvh] w-full overflow-y-auto bg-page pb-lg pt-lg max-lg:h-[100dvh] max-lg:px-md lg:sticky lg:flex lg:flex-col lg:gap-sm"
>
<div className="flex pb-md">
<p className="flex-1">Filters</p>
<Btn
popoverTarget="filters"
variant="tertiary"
className="rounded-sm p-0 lg:hidden"
>
<XMarkIcon width={24} height={24} />
</Btn>
</div>

<div className="pb-lg pr-md">
<SearchBar param="compareQuery" shouldResetPageOnChange={false} />
</div>
Expand Down Expand Up @@ -149,41 +172,49 @@ export default async function Page({
</div>
</div>
<div className="flex min-w-0 flex-1 flex-col items-stretch gap-md pb-lg pt-lg">
<div className="flex gap-sm overflow-x-auto">
{professorStats.map((professor) => (
<CompareItem
key={professor.id}
link={`/professors/${professor.id}`}
review={professor.review}
takeAgainPercent={professor.takeAgainPercent}
avgGrade={professor.avgGrade}
totalReviews={professor.totalReviews}
id={professor.id}
/>
))}
{courseStats.map((course) => (
<CompareItem
key={course.id}
link={`/courses/${course.id}`}
review={course.review}
takeAgainPercent={course.takeAgainPercent}
avgGrade={course.avgGrade}
totalReviews={course.totalReviews}
id={course.id}
/>
))}
</div>
{professorStats.length + courseStats.length ? (
<div className="flex gap-sm overflow-x-auto">
{professorStats.map((professor) => (
<CompareItem
key={professor.id}
link={`/professors/${professor.id}`}
review={professor.review}
takeAgainPercent={professor.takeAgainPercent}
avgGrade={professor.avgGrade}
totalReviews={professor.totalReviews}
id={professor.id}
/>
))}
{courseStats.map((course) => (
<CompareItem
key={course.id}
link={`/courses/${course.id}`}
review={course.review}
takeAgainPercent={course.takeAgainPercent}
avgGrade={course.avgGrade}
totalReviews={course.totalReviews}
id={course.id}
/>
))}
</div>
) : (
<p className="w-full p-lg text-center italic text-neutral">
No items selected.
<br />
Search and select professors/courses to compare.
</p>
)}
<Card className="p-lg max-lg:w-full lg:flex-1">
<p className="pb-sm font-bold">Rating Distribution</p>
<BarChart
series={[
...(professorStats.map((professor) => ({
name: professor.id,
data: professor.ratingDistribution,
data: professor.ratingDistribution.reverse(),
})) ?? []),
...(courseStats.map((course) => ({
name: course.id,
data: course.ratingDistribution,
data: course.ratingDistribution.reverse(),
})) ?? []),
]}
categories={[5, 4, 3, 2, 1]}
Expand Down
7 changes: 6 additions & 1 deletion app/(main)/courses/[id]/@statistics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,12 @@ export default async function Page({
<Card className="p-lg max-lg:w-full lg:flex-1">
<p className="pb-sm font-bold">Rating Distribution</p>
<BarChart
series={[{ name: 'Rating Distribution', data: reviewDistribution }]}
series={[
{
name: 'Rating Distribution',
data: reviewDistribution.reverse(),
},
]}
categories={[5, 4, 3, 2, 1]}
/>
</Card>
Expand Down
2 changes: 1 addition & 1 deletion app/(main)/professors/[id]/@statistics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export default async function Page({
series={[
{
name: 'Rating Distribution',
data: reviewDistribution,
data: reviewDistribution.reverse(),
},
]}
categories={[5, 4, 3, 2, 1]}
Expand Down
153 changes: 153 additions & 0 deletions components/molecules/client/compare-search-bar/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
'use client';

import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';

import { Card } from '@/components/atoms';
import { ParamSelect, SearchBar } from '@/components/molecules';
import { CoursesSearchResponse, ProfessorsSearchResponse } from '@/types';
import SWRConfigProvider from '@/wrappers/swr-config';
import Link from 'next/link';

type Error = { message: string };

const useCoursesSearchResults = (currentQuery: string) => {
const apiQueryParams = new URLSearchParams();
apiQueryParams.set('query', currentQuery);
apiQueryParams.set('limit', '3');
const { data, error, isLoading } = useSWR<CoursesSearchResponse, Error>(
`/django/core/courses/search?${apiQueryParams.toString()}`,
);
return { data, error, isLoading };
};

const useProfessorsSearchResults = (currentQuery: string) => {
const apiQueryParams = new URLSearchParams();
apiQueryParams.set('query', currentQuery);
apiQueryParams.set('limit', '3');

const { data, error, isLoading } = useSWR<ProfessorsSearchResponse, Error>(
`/django/core/professors/search?${apiQueryParams.toString()}`,
);
return { data, error, isLoading };
};

interface StatusMessageProps {
isLoading: boolean;
error: Error | undefined;
data: ({ total_results: number } & unknown) | undefined;
}

const StatusMessage: React.FC<StatusMessageProps> = ({
isLoading,
error,
data,
}) => {
if (isLoading) {
return <li className="mx-auto my-8 w-fit">Loading...</li>;
}
if (error) {
return (
<li className="mx-auto my-8 w-fit text-important">
Error: {error.message}
</li>
);
}
if (!data || data.total_results === 0) {
return <li className="mx-auto my-8 w-fit">No results found</li>;
}
return null;
};

const CourseSearchResults: React.FC = () => {
const searchParams = useSearchParams();
const currentOption = searchParams.get('compareOption') ?? 'professors';
const currentQuery = searchParams.get('compareQuery') ?? '';
const { data, error, isLoading } = useCoursesSearchResults(currentQuery);
return (
<menu>
<StatusMessage isLoading={isLoading} error={error} data={data} />
{data && data.total_results > 0
? data.items.map((course, i) => (
<li key={i} className="border-b-2 border-border last:border-b-0">
<Link
href={`/courses/${course.department}-${course.course_number}?navOption=${currentOption}`}
className="flex flex-col px-lg py-md animation hover:bg-[rgb(var(--color-primary)/0.15)] focus:bg-[rgb(var(--color-primary)/0.15)]"
>
<span className="overflow-ellipsis text-small-lg text-neutral">
{course.department} {course.course_number}
</span>
<span className="overflow-ellipsis text-p font-bold">
{course.name}
</span>
</Link>
</li>
))
: null}
</menu>
);
};

const ProfessorSearchResults: React.FC = () => {
const searchParams = useSearchParams();
const currentOption = searchParams.get('compareOption') ?? 'professors';
const currentQuery = searchParams.get('compareQuery') ?? '';
const { data, error, isLoading } = useProfessorsSearchResults(currentQuery);
return (
<menu>
<StatusMessage isLoading={isLoading} error={error} data={data} />
{data && data.total_results > 0
? data.items.map((professor, i) => (
<li key={i} className="border-b-2 border-border last:border-b-0">
<Link
href={`/professors/${professor.email.split('@')[0]}?navOption=${currentOption}`}
className="flex flex-col px-lg py-md animation hover:bg-[rgb(var(--color-primary)/0.15)] focus:bg-[rgb(var(--color-primary)/0.15)]"
>
<span className="overflow-ellipsis text-small-lg text-neutral">
{professor.email}
</span>
<span className="overflow-ellipsis text-p font-bold">
{professor.name}
</span>
</Link>
</li>
))
: null}
</menu>
);
};

export const CompareSearchBar: React.FC = () => {
const searchParams = useSearchParams();
const currentOption = searchParams.get('compareOption') ?? 'professors';
const currentQuery = searchParams.get('compareQuery') ?? '';
return (
<div className="relative flex whitespace-nowrap">
<SearchBar
param="compareQuery"
shouldResetPageOnChange={false}
className="peer flex-1 [&>input]:!rounded-r-none"
/>
<ParamSelect
param="compareOption"
shouldResetPageOnChange={false}
className="rounded-l-none border-border bg-background"
value={currentOption}
>
<option value="professors">Professors</option>
<option value="courses">Courses</option>
</ParamSelect>
{currentQuery ? (
<Card className="absolute left-0 top-[50px] z-50 hidden w-[1000px] max-w-[80dvw] shadow-paper hover:block peer-focus-within:block peer-has-[:placeholder-shown]:hidden">
<SWRConfigProvider>
{currentOption === 'professors' ? (
<ProfessorSearchResults />
) : currentOption === 'courses' ? (
<CourseSearchResults />
) : null}
</SWRConfigProvider>
</Card>
) : null}
</div>
);
};
2 changes: 2 additions & 0 deletions components/molecules/client/compare-search-bar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { CompareSearchBar as default } from './component';
export * from './component';
1 change: 1 addition & 0 deletions components/molecules/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './profile-btn';
export * from './nav-search-bar';
export * from './pagination-bar';
export * from './color-mode-picker';
export * from './compare-search-bar';
5 changes: 3 additions & 2 deletions components/molecules/client/profile-btn/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { LinkBtn } from '@/components/atoms';
import { cn } from '@/utils/cn';
import { useSession } from '@/wrappers/session-provider';
import { UserIcon } from '@heroicons/react/20/solid';
import { UserCircleIcon } from '@heroicons/react/24/solid';

interface Props
extends Omit<
Expand All @@ -20,8 +20,9 @@ export const ProfileBtn: React.FC<Props> = ({ className, ...props }) => {
{...props}
href="/profile"
variant="tertiary"
aria-label="Profile"
>
<UserIcon width={20} height={20} /> Profile
<UserCircleIcon width={24} height={24} />{' '}
</LinkBtn>
);
}
Expand Down
Loading

0 comments on commit f3aae87

Please sign in to comment.