Skip to content

Commit

Permalink
feat: Add courses page
Browse files Browse the repository at this point in the history
This page will display the best courses available
  • Loading branch information
hampfh committed Dec 23, 2024
1 parent 57d4e82 commit a0d3546
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 23 deletions.
51 changes: 51 additions & 0 deletions apps/api/src/routes/courses/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,55 @@ export const courseRoute = new Elysia({
}),
tags: ["Courses"],
},
)
.get(
"/",
async ({ query: { ordering, count, page } }) => {
const courses = await prisma.course.findMany({
take: count ?? 20,
skip: (page ?? 0) * (count ?? 20),
orderBy: {
cachedRating: ordering === "ascending" ? "asc" : "desc",
},
where: {
cachedReviewCount: {
gt: 0,
},
},
select: {
courseCode: true,
},
});

return (
await Promise.all(courses.map((course) => getCourse(course.courseCode)))
).filter((x) => x != null);
},
{
query: t.Object({
page: t.Optional(
t.Number({
minimum: 0,
}),
),
count: t.Optional(
t.Number({
minimum: 1,
maximum: 100,
}),
),
ordering: t.Enum(
{
ascending: "ascending",
descending: "descending",
},
{
default: "descending",
title: "Ordering",
description:
"Descending will pick the best-rated courses, ascending the worst rated",
},
),
}),
},
);
26 changes: 26 additions & 0 deletions apps/web/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import { Route as rootRoute } from './routes/__root'
import { Route as IndexImport } from './routes/index'
import { Route as CoursesIndexImport } from './routes/courses/index'
import { Route as CoursesCourseIdIndexImport } from './routes/courses/$courseId/index'
import { Route as AccountsAccountIdIndexImport } from './routes/accounts/$accountId/index'
import { Route as loginSignupIndexImport } from './routes/(login)/signup/index'
Expand All @@ -25,6 +26,12 @@ const IndexRoute = IndexImport.update({
getParentRoute: () => rootRoute,
} as any)

const CoursesIndexRoute = CoursesIndexImport.update({
id: '/courses/',
path: '/courses/',
getParentRoute: () => rootRoute,
} as any)

const CoursesCourseIdIndexRoute = CoursesCourseIdIndexImport.update({
id: '/courses/$courseId/',
path: '/courses/$courseId/',
Expand Down Expand Up @@ -60,6 +67,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexImport
parentRoute: typeof rootRoute
}
'/courses/': {
id: '/courses/'
path: '/courses'
fullPath: '/courses'
preLoaderRoute: typeof CoursesIndexImport
parentRoute: typeof rootRoute
}
'/(login)/signin/': {
id: '/(login)/signin/'
path: '/signin'
Expand Down Expand Up @@ -95,6 +109,7 @@ declare module '@tanstack/react-router' {

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/courses': typeof CoursesIndexRoute
'/signin': typeof loginSigninIndexRoute
'/signup': typeof loginSignupIndexRoute
'/accounts/$accountId': typeof AccountsAccountIdIndexRoute
Expand All @@ -103,6 +118,7 @@ export interface FileRoutesByFullPath {

export interface FileRoutesByTo {
'/': typeof IndexRoute
'/courses': typeof CoursesIndexRoute
'/signin': typeof loginSigninIndexRoute
'/signup': typeof loginSignupIndexRoute
'/accounts/$accountId': typeof AccountsAccountIdIndexRoute
Expand All @@ -112,6 +128,7 @@ export interface FileRoutesByTo {
export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/courses/': typeof CoursesIndexRoute
'/(login)/signin/': typeof loginSigninIndexRoute
'/(login)/signup/': typeof loginSignupIndexRoute
'/accounts/$accountId/': typeof AccountsAccountIdIndexRoute
Expand All @@ -122,20 +139,23 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/courses'
| '/signin'
| '/signup'
| '/accounts/$accountId'
| '/courses/$courseId'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/courses'
| '/signin'
| '/signup'
| '/accounts/$accountId'
| '/courses/$courseId'
id:
| '__root__'
| '/'
| '/courses/'
| '/(login)/signin/'
| '/(login)/signup/'
| '/accounts/$accountId/'
Expand All @@ -145,6 +165,7 @@ export interface FileRouteTypes {

export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
CoursesIndexRoute: typeof CoursesIndexRoute
loginSigninIndexRoute: typeof loginSigninIndexRoute
loginSignupIndexRoute: typeof loginSignupIndexRoute
AccountsAccountIdIndexRoute: typeof AccountsAccountIdIndexRoute
Expand All @@ -153,6 +174,7 @@ export interface RootRouteChildren {

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
CoursesIndexRoute: CoursesIndexRoute,
loginSigninIndexRoute: loginSigninIndexRoute,
loginSignupIndexRoute: loginSignupIndexRoute,
AccountsAccountIdIndexRoute: AccountsAccountIdIndexRoute,
Expand All @@ -170,6 +192,7 @@ export const routeTree = rootRoute
"filePath": "__root.tsx",
"children": [
"/",
"/courses/",
"/(login)/signin/",
"/(login)/signup/",
"/accounts/$accountId/",
Expand All @@ -179,6 +202,9 @@ export const routeTree = rootRoute
"/": {
"filePath": "index.tsx"
},
"/courses/": {
"filePath": "courses/index.tsx"
},
"/(login)/signin/": {
"filePath": "(login)/signin/index.tsx"
},
Expand Down
25 changes: 25 additions & 0 deletions apps/web/src/routes/-components/course-large-result.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CourseDto } from "$api/routes/courses/course-dto";
import { Badge } from "@/components/ui/badge";
import { Rating } from "@/routes/-components/rating";
import { cn } from "@/utilities/shadcn-utils";
import { GraduationCapIcon } from "lucide-react";

export function CourseLargeResult({ course }: { course: CourseDto }) {
return (
<div className="group flex cursor-pointer justify-between gap-4 rounded-lg p-4 hover:bg-zinc-200/60 active:opacity-70">
<div className="flex items-center gap-4">
<GraduationCapIcon className="shrink-0" />
<Badge className="h-min shrink-0 text-nowrap">{course.code}</Badge>
<p
className={cn("shrink group-hover:opacity-80", {
"text-sm": course.title.length > 40,
})}
>
{course.title}
</p>
</div>

<Rating rating={course.rating} reviewCount={course.reviewCount} />
</div>
);
}
6 changes: 5 additions & 1 deletion apps/web/src/routes/-components/rating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ export function Rating({
variant?: "small" | "default";
}) {
if (rating == null && variant === "default")
return <Badge variant="outline">Not rated</Badge>;
return (
<Badge variant="outline" className="text-nowrap">
Not rated
</Badge>
);

if (variant === "small") {
return (
Expand Down
31 changes: 9 additions & 22 deletions apps/web/src/routes/-components/search-box.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import NumberTicker from "@/components/ui/number-ticker";
import { Skeleton } from "@/components/ui/skeleton";
Expand All @@ -8,17 +9,14 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { CourseLargeResult } from "@/routes/-components/course-large-result";
import { Rating } from "@/routes/-components/rating";
import { api } from "@/utilities/http-client";
import { QueryKey } from "@/utilities/query-key";
import { cn } from "@/utilities/shadcn-utils";
import { useQuery } from "@tanstack/react-query";
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
import {
GraduationCapIcon,
LanguagesIcon,
LoaderCircleIcon,
} from "lucide-react";
import { ArrowRightIcon, LanguagesIcon, LoaderCircleIcon } from "lucide-react";

export function SearchBox() {
const navigate = useNavigate({
Expand Down Expand Up @@ -73,6 +71,11 @@ export function SearchBox() {
Available in English
</p>
</Toggle>
<Link to="/courses">
<Button className="flex gap-2" variant="outline" size="sm">
Most loved courses <ArrowRightIcon />
</Button>
</Link>
</div>
<Input
placeholder="Search for courses"
Expand Down Expand Up @@ -131,23 +134,7 @@ export function SearchBox() {
courseId: course.code,
}}
>
<div className="group flex cursor-pointer justify-between gap-4 rounded-lg p-4 hover:bg-zinc-200/60 active:opacity-70">
<div className="flex items-center gap-4">
<GraduationCapIcon className="shrink-0" />
<Badge className="h-min shrink-0 text-nowrap">
{course.code}
</Badge>
<p
className={cn("shrink group-hover:opacity-80", {
"text-sm": course.title.length > 40,
})}
>
{course.title}
</p>
</div>

<Rating rating={course.rating} reviewCount={course.reviewCount} />
</div>
<CourseLargeResult course={course} />
</Link>
))}
<div className="flex justify-center">
Expand Down
41 changes: 41 additions & 0 deletions apps/web/src/routes/courses/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { BackToSearchBar } from "@/routes/-components/back-to-search-bar";
import { CourseLargeResult } from "@/routes/-components/course-large-result";
import { api } from "@/utilities/http-client";
import { QueryKey } from "@/utilities/query-key";
import { useQuery } from "@tanstack/react-query";
import { createFileRoute, Link } from "@tanstack/react-router";

export const Route = createFileRoute("/courses/")({
component: CoursesPage,
});

function CoursesPage() {
const { data: courses } = useQuery({
queryKey: [QueryKey.Courses],
queryFn: async () => {
const response = await api.courses.index.get({
query: {
ordering: "descending",
},
});
return response.data;
},
});
return (
<div className="mx-auto flex max-w-[600px] flex-col">
<h1 className="py-5 text-center text-3xl">Courses (best)</h1>
<div className="flex flex-col">
{courses?.map((course) => (
<Link
key={course.id}
to="/courses/$courseId"
params={{ courseId: course.code }}
>
<CourseLargeResult course={course} />
</Link>
))}
</div>
<BackToSearchBar />
</div>
);
}
1 change: 1 addition & 0 deletions apps/web/src/utilities/query-key.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum QueryKey {
Search = "SEARCH",
Course = "COURSE",
Courses = "COURSES",
Session = "SESSION",
Review = "REVIEW",
ReviewSingle = "REVIEW_SINGLE",
Expand Down

0 comments on commit a0d3546

Please sign in to comment.