Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New Courses View #288

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions apps/api/src/courses/course.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,9 +520,32 @@ export class CourseService {
};
}

// TODO: Needs to be refactored
async getTeacherCourses(authorId: UUIDType): Promise<AllCoursesForTeacherResponse> {
return this.db
.select(this.getSelectField())
.select({
id: courses.id,
description: sql<string>`${courses.description}`,
title: courses.title,
imageUrl: courses.imageUrl,
authorId: sql<string>`${courses.authorId}`,
author: sql<string>`CONCAT(${users.firstName} || ' ' || ${users.lastName})`,
authorEmail: sql<string>`${users.email}`,
category: sql<string>`${categories.title}`,
enrolled: sql<boolean>`CASE WHEN ${studentCourses.studentId} IS NOT NULL THEN true ELSE false END`,
enrolledParticipantCount: sql<number>`0`,
courseLessonCount: courses.lessonsCount,
completedLessonCount: sql<number>`0`,
priceInCents: courses.priceInCents,
currency: courses.currency,
hasFreeLessons: sql<boolean>`
EXISTS (
SELECT 1
FROM ${courseLessons}
WHERE ${courseLessons.courseId} = ${courses.id}
AND ${courseLessons.isFree} = true
)`,
})
.from(courses)
.leftJoin(studentCourses, eq(studentCourses.courseId, courses.id))
.leftJoin(categories, eq(courses.categoryId, categories.id))
Expand All @@ -545,8 +568,6 @@ export class CourseService {
users.email,
studentCourses.studentId,
categories.title,
coursesSummaryStats.freePurchasedCount,
coursesSummaryStats.paidPurchasedCount,
)
.orderBy(
sql<boolean>`CASE WHEN ${studentCourses.studentId} IS NULL THEN TRUE ELSE FALSE END`,
Expand Down
22 changes: 22 additions & 0 deletions apps/api/src/swagger/api-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2302,6 +2302,26 @@
"data": {
"type": "object",
"properties": {
"firstName": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"lastName": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"id": {
"format": "uuid",
"type": "string"
Expand Down Expand Up @@ -2348,6 +2368,8 @@
}
},
"required": [
"firstName",
"lastName",
"id",
"description",
"contactEmail",
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/users/schemas/user.schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Type, type Static } from "@sinclair/typebox";
import { type Static, Type } from "@sinclair/typebox";

import { commonUserSchema } from "src/common/schemas/common-user.schema";

export const allUsersSchema = Type.Array(commonUserSchema);
export const userDetailsSchema = Type.Object({
firstName: Type.Union([Type.String(), Type.Null()]),
lastName: Type.Union([Type.String(), Type.Null()]),
id: Type.String({ format: "uuid" }),
description: Type.Union([Type.String(), Type.Null()]),
contactEmail: Type.Union([Type.String(), Type.Null()]),
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,16 @@ export class UsersService {
public async getUserDetails(userId: string): Promise<UserDetails> {
const [userBio]: UserDetails[] = await this.db
.select({
firstName: users.firstName,
lastName: users.lastName,
id: userDetails.id,
description: userDetails.description,
contactEmail: userDetails.contactEmail,
contactPhone: userDetails.contactPhoneNumber,
jobTitle: userDetails.jobTitle,
})
.from(userDetails)
.leftJoin(users, eq(userDetails.userId, users.id))
.where(eq(userDetails.userId, userId));

if (!userBio) {
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/api/queries/useTeacherCourses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ApiClient } from "../api-client";

import type { GetTeacherCoursesResponse } from "../generated-api";

export const teacherCourses = (authorId: string) => {
export const teacherCoursesOptions = (authorId: string) => {
return {
queryKey: ["teacher-courses", authorId],
queryFn: async () => {
Expand All @@ -17,5 +17,5 @@ export const teacherCourses = (authorId: string) => {
};

export function useTeacherCourses(authorId: string) {
return useQuery(teacherCourses(authorId));
return useQuery(teacherCoursesOptions(authorId));
}
6 changes: 2 additions & 4 deletions apps/web/app/assets/svgs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ export { default as ArrowUp } from "./arrow-up.svg?react";
export { default as ArrowDown } from "./arrow-down.svg?react";
export { default as DragAndDropIcon } from "./drag-and-drop.svg?react";
export { default as Info } from "./info.svg?react";
export { default as Text } from "./text.svg?react";
export { default as Presentation } from "./presentation.svg?react";
export { default as Video } from "./video.svg?react";
export { default as Quiz } from "./quiz.svg?react";
export { default as Warning } from "./warning.svg?react";
export { default as Admin } from "./admin.svg?react";

export * from "./lesson-types";
4 changes: 4 additions & 0 deletions apps/web/app/assets/svgs/lesson-types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { default as Text } from "./text.svg?react";
export { default as Presentation } from "./presentation.svg?react";
export { default as Video } from "./video.svg?react";
export { default as Quiz } from "./quiz.svg?react";
3 changes: 3 additions & 0 deletions apps/web/app/assets/svgs/lesson-types/presentation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/web/app/assets/svgs/lesson-types/quiz.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/web/app/assets/svgs/lesson-types/text.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/web/app/assets/svgs/lesson-types/video.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions apps/web/app/assets/svgs/presentation.svg

This file was deleted.

3 changes: 0 additions & 3 deletions apps/web/app/assets/svgs/quiz.svg

This file was deleted.

3 changes: 0 additions & 3 deletions apps/web/app/assets/svgs/text.svg

This file was deleted.

3 changes: 0 additions & 3 deletions apps/web/app/assets/svgs/video.svg

This file was deleted.

44 changes: 44 additions & 0 deletions apps/web/app/components/Badges/ProgressBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Badge } from "~/components/ui/badge";

import type { IconName } from "~/types/shared";

type ProgressBadgeProps = {
progress: "completed" | "inProgress" | "notStarted";
className?: string;
};

type ProgressConfig = {
[key in "completed" | "inProgress" | "notStarted"]: {
variant: "successFilled" | "inProgressFilled" | "notStartedFilled";
icon: IconName;
label: string;
};
};

export const ProgressBadge = ({ progress, className }: ProgressBadgeProps) => {
const progressConfig: ProgressConfig = {
completed: {
variant: "successFilled",
icon: "InputRoundedMarkerSuccess",
label: "Completed",
},
inProgress: {
variant: "inProgressFilled",
icon: "InProgress",
label: "In Progress",
},
notStarted: {
variant: "notStartedFilled",
icon: "NotStartedRounded",
label: "Not Started",
},
};

const { variant, icon, label } = progressConfig[progress];

return (
<Badge variant={variant} icon={icon} {...(Boolean(className) && { className })}>
{label}
</Badge>
);
};
1 change: 1 addition & 0 deletions apps/web/app/components/CardBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const badgeVariants = cva(
default: "text-neutral-900",
primary: "text-primary-950",
secondary: "text-secondary-700",
secondaryFilled: "text-secondary-700 bg-secondary-50",
successOutlined: "text-success-800",
successFilled: "text-white bg-success-600",
},
Expand Down
46 changes: 42 additions & 4 deletions apps/web/app/components/PageWrapper/PageWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,53 @@
import {
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "~/components/ui/breadcrumb";
import { cn } from "~/lib/utils";

import type { HTMLAttributes } from "react";
import type { HTMLAttributes, ReactNode } from "react";

type PageWrapperProps = HTMLAttributes<HTMLDivElement> & {
breadcrumbs?: { title: string; href: string }[];
children: ReactNode;
className?: string;
};

export const PageWrapper = ({ className, ...props }: PageWrapperProps) => {
type Breadcrumb = { title: string; href: string };

type BreadcrumbsProps = {
breadcrumbs?: Breadcrumb[];
};

export const Breadcrumbs = ({ breadcrumbs = [] }: BreadcrumbsProps) => {
if (!breadcrumbs.length) return null;

return (
<BreadcrumbList>
{breadcrumbs.map(({ href, title }, index) => (
<BreadcrumbItem key={index}>
<BreadcrumbLink href={href}>{title}</BreadcrumbLink>
{index < breadcrumbs.length - 1 && <BreadcrumbSeparator />}
</BreadcrumbItem>
))}
</BreadcrumbList>
);
};

export const PageWrapper = ({ className, breadcrumbs, children, ...props }: PageWrapperProps) => {
const hasBreadcrumbs = Boolean(breadcrumbs);

const classes = cn(
"h-auto w-full pt-6 px-4 pb-4 md:px-6 md:pb-6 2xl:pt-12 2xl:px-8 2xl:pb-8",
"w-full pt-6 px-4 pb-4 md:px-6 md:pb-6 3xl:pt-12 3xl:px-8 3xl:pb-8",
{ "pt-8 md:pt-6 3xl:pb-2": hasBreadcrumbs },
className,
);
return <div className={classes} {...props} />;

return (
<div className={classes} {...props}>
{breadcrumbs && <Breadcrumbs breadcrumbs={breadcrumbs} />}
{children}
</div>
);
};
52 changes: 52 additions & 0 deletions apps/web/app/components/ui/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import * as AccordionPrimitive from "@radix-ui/react-accordion";
import * as React from "react";

import { cn } from "~/lib/utils";

const Accordion = AccordionPrimitive.Root;

const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn(className)} {...props} />
));
AccordionItem.displayName = "AccordionItem";

const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between font-medium transition-all",
className,
)}
{...props}
>
{children}
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;

const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn(className)}>{children}</div>
</AccordionPrimitive.Content>
));

AccordionContent.displayName = AccordionPrimitive.Content.displayName;

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
Loading
Loading