Skip to content

Commit

Permalink
added compare page
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmadgaz committed Sep 30, 2024
1 parent 575c53b commit 6e399dd
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 31 deletions.
93 changes: 62 additions & 31 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,30 +172,38 @@ 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
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';
91 changes: 91 additions & 0 deletions components/molecules/compare-item/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Card, LinkBtn, Stars } from '@/components/atoms';
import { cn } from '@/utils/cn';
import getEvaluation from '@/utils/get-evaluation';
import {
ArrowPathIcon,
ClipboardDocumentListIcon,
} from '@heroicons/react/16/solid';

interface Props {
link: string;
id: string;
review?: number;
totalReviews: number;
avgGrade?: string | null;
takeAgainPercent?: number;
}

export const CompareItem: React.FC<Props> = (props) => (
<LinkBtn
variant="tertiary"
className="w-fit rounded-md p-0 text-inherit"
href={props.link}
>
<Card className="flex w-fit flex-col gap-sm rounded-md p-md">
<div>
<p className="font-bold">{props.id}</p>
<div className="flex items-center gap-sm">
{props.review ? (
<p className="text-small-lg">{props.review}</p>
) : (
<p className="text-small-lg text-neutral">-</p>
)}

<div className="w-[100px]">
<Stars rating={props.review ?? 0} />
</div>
<p className="text-small-sm text-neutral">
{props.totalReviews} Reviews
</p>
</div>
</div>
<div className="flex flex-col gap-xs">
<div className="flex items-center gap-xs text-neutral">
<ClipboardDocumentListIcon width={16} height={16} />
<p className="text-small-sm">
Average Grade:{' '}
{props.avgGrade ? (
<span
className={cn({
'text-good':
getEvaluation(props.avgGrade, 'grade') === 'good',
'text-ok': getEvaluation(props.avgGrade, 'grade') === 'ok',
'text-bad': getEvaluation(props.avgGrade, 'grade') === 'bad',
})}
>
{props.avgGrade}
</span>
) : (
<span className="text-neutral">-</span>
)}
</p>
</div>
<div className="flex items-center gap-xs text-neutral">
<ArrowPathIcon width={16} height={16} />
<p className="text-small-sm">
Would Take Again:{' '}
{props.takeAgainPercent ? (
<span
className={cn({
'text-good':
getEvaluation(props.takeAgainPercent, 'percentage') ===
'good',
'text-ok':
getEvaluation(props.takeAgainPercent, 'percentage') ===
'ok',
'text-bad':
getEvaluation(props.takeAgainPercent, 'percentage') ===
'bad',
})}
>
{props.takeAgainPercent}%
</span>
) : (
<span className="text-neutral">-</span>
)}
</p>
</div>
</div>
</Card>
</LinkBtn>
);
2 changes: 2 additions & 0 deletions components/molecules/compare-item/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {CompareItem as default} from './component';
export * from './component';
1 change: 1 addition & 0 deletions components/molecules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './filter-group';
export * from './param-select';
export * from './search-bar';
export * from './client';
export * from './compare-item';
3 changes: 3 additions & 0 deletions components/organisms/client/navbar/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ export const Navbar: React.FC = () => {
>
Professors
</LinkBtn>
<LinkBtn onClick={toggleMenu} href="/compare" variant="tertiary">
Compare
</LinkBtn>
<SessionWrapper>
<ProfileBtn />
</SessionWrapper>
Expand Down

0 comments on commit 6e399dd

Please sign in to comment.