diff --git a/apps/api/src/courses/course.controller.ts b/apps/api/src/courses/course.controller.ts index a746f2880..b194f9f82 100644 --- a/apps/api/src/courses/course.controller.ts +++ b/apps/api/src/courses/course.controller.ts @@ -28,7 +28,7 @@ import { Roles } from "src/common/decorators/roles.decorator"; import { CurrentUser } from "src/common/decorators/user.decorator"; import { RolesGuard } from "src/common/guards/roles.guard"; import { CourseService } from "src/courses/course.service"; -import { allCoursesSchema } from "src/courses/schemas/course.schema"; +import { allCoursesForTeacherSchema } from "src/courses/schemas/course.schema"; import { SortCourseFieldsOptions, CourseEnrollmentScope, @@ -174,7 +174,7 @@ export class CourseController { }, { type: "query", name: "excludeCourseId", schema: UUIDSchema }, ], - response: baseResponse(allCoursesSchema), + response: baseResponse(allCoursesForTeacherSchema), }) async getTeacherCourses( @Query("authorId") authorId: UUIDType, diff --git a/apps/api/src/courses/course.service.ts b/apps/api/src/courses/course.service.ts index 151cb15ea..aabecfb01 100644 --- a/apps/api/src/courses/course.service.ts +++ b/apps/api/src/courses/course.service.ts @@ -331,10 +331,9 @@ export class CourseService { const dataWithS3SignedUrls = await Promise.all( data.map(async (item) => { - if (!item.thumbnailUrl) return item; - try { const signedUrl = await this.fileService.getFileUrl(item.thumbnailUrl); + return { ...item, thumbnailUrl: signedUrl }; } catch (error) { console.error(`Failed to get signed URL for ${item.thumbnailUrl}:`, error); @@ -380,8 +379,11 @@ export class CourseService { }) .from(courses) .leftJoin(categories, eq(courses.categoryId, categories.id)) - .leftJoin(studentCourses, eq(courses.id, studentCourses.courseId)) - .where(and(eq(courses.id, id), eq(studentCourses.studentId, userId))); + .leftJoin( + studentCourses, + and(eq(courses.id, studentCourses.courseId), eq(studentCourses.studentId, userId)), + ) + .where(eq(courses.id, id)); if (!course) throw new NotFoundException("Course not found"); @@ -1042,7 +1044,6 @@ export class CourseService { excludeCourseId?: UUIDType, ) { const conditions = []; - if (authorId) { conditions.push(eq(courses.authorId, authorId)); } diff --git a/apps/api/src/file/file.service.ts b/apps/api/src/file/file.service.ts index acd125486..d45cedf5f 100644 --- a/apps/api/src/file/file.service.ts +++ b/apps/api/src/file/file.service.ts @@ -14,6 +14,7 @@ export class FileService { ) {} async getFileUrl(fileKey: string): Promise { + if (!fileKey) return "https://app.lms.localhost/app/assets/placeholders/card-placeholder.jpg"; if (fileKey.startsWith("https://")) return fileKey; try { diff --git a/apps/api/src/swagger/api-schema.json b/apps/api/src/swagger/api-schema.json index 054bb7861..fe018cedf 100644 --- a/apps/api/src/swagger/api-schema.json +++ b/apps/api/src/swagger/api-schema.json @@ -3607,6 +3607,12 @@ }, "hasFreeChapters": { "type": "boolean" + }, + "completedChapterCount": { + "type": "number" + }, + "enrolled": { + "type": "boolean" } }, "required": [ @@ -3614,12 +3620,15 @@ "title", "thumbnailUrl", "description", + "authorId", "author", + "authorEmail", "category", "courseChapterCount", "enrolledParticipantCount", "priceInCents", - "currency" + "currency", + "completedChapterCount" ] } } diff --git a/apps/api/src/user/user.controller.ts b/apps/api/src/user/user.controller.ts index 9c533454a..9cce66dfb 100644 --- a/apps/api/src/user/user.controller.ts +++ b/apps/api/src/user/user.controller.ts @@ -106,8 +106,11 @@ export class UserController { request: [{ type: "query", name: "userId", schema: UUIDSchema, required: true }], response: baseResponse(userDetailsSchema), }) - async getUserDetails(@Query("userId") userId: UUIDType): Promise> { - const userDetails = await this.usersService.getUserDetails(userId); + async getUserDetails( + @Query("userId") userId: UUIDType, + @CurrentUser("role") role: UserRole, + ): Promise> { + const userDetails = await this.usersService.getUserDetails(userId, role); return new BaseResponse(userDetails); } diff --git a/apps/api/src/user/user.service.ts b/apps/api/src/user/user.service.ts index 380c929e5..018568f76 100644 --- a/apps/api/src/user/user.service.ts +++ b/apps/api/src/user/user.service.ts @@ -90,7 +90,7 @@ export class UserService { return user; } - public async getUserDetails(userId: UUIDType): Promise { + public async getUserDetails(userId: UUIDType, userRole: UserRole): Promise { const [userBio]: UserDetails[] = await this.db .select({ firstName: users.firstName, @@ -105,8 +105,31 @@ export class UserService { .leftJoin(users, eq(userDetails.userId, users.id)) .where(eq(userDetails.userId, userId)); - if (!userBio) { - throw new NotFoundException("User details not found"); + if (!userBio && (USER_ROLES.TEACHER === userRole || USER_ROLES.ADMIN === userRole)) { + // TODO: quick + // throw new NotFoundException("User details not found"); + const [user] = await this.db + .select({ + id: users.id, + email: users.email, + firstName: users.firstName, + lastName: users.lastName, + }) + .from(users) + .where(eq(users.id, userId)); + + const [userBio] = await this.db + .insert(userDetails) + .values({ userId, contactEmail: user.email }) + .returning({ + id: userDetails.id, + description: userDetails.description, + contactEmail: userDetails.contactEmail, + contactPhone: userDetails.contactPhoneNumber, + jobTitle: userDetails.jobTitle, + }); + + return { firstName: user.firstName, lastName: user.lastName, ...userBio }; } return userBio; diff --git a/apps/web/app/api/generated-api.ts b/apps/web/app/api/generated-api.ts index 348e48830..ae26bdeca 100644 --- a/apps/web/app/api/generated-api.ts +++ b/apps/web/app/api/generated-api.ts @@ -331,8 +331,6 @@ export interface GetAllCoursesResponse { authorEmail?: string; category: string; courseChapterCount: number; - completedChapterCount: number; - enrolled?: boolean; enrolledParticipantCount: number; priceInCents: number; currency: string; @@ -361,14 +359,14 @@ export interface GetStudentCoursesResponse { authorEmail?: string; category: string; courseChapterCount: number; - completedChapterCount: number; - enrolled?: boolean; enrolledParticipantCount: number; priceInCents: number; currency: string; isPublished?: boolean; createdAt?: string; hasFreeChapters?: boolean; + completedChapterCount: number; + enrolled?: boolean; }[]; pagination: { totalItems: number; @@ -391,14 +389,14 @@ export interface GetAvailableCoursesResponse { authorEmail?: string; category: string; courseChapterCount: number; - completedChapterCount: number; - enrolled?: boolean; enrolledParticipantCount: number; priceInCents: number; currency: string; isPublished?: boolean; createdAt?: string; hasFreeChapters?: boolean; + completedChapterCount: number; + enrolled?: boolean; }[]; pagination: { totalItems: number; @@ -416,19 +414,19 @@ export interface GetTeacherCoursesResponse { thumbnailUrl: string | null; description: string; /** @format uuid */ - authorId?: string; + authorId: string; author: string; - authorEmail?: string; + authorEmail: string; category: string; courseChapterCount: number; - completedChapterCount: number; - enrolled?: boolean; enrolledParticipantCount: number; priceInCents: number; currency: string; isPublished?: boolean; createdAt?: string; hasFreeChapters?: boolean; + completedChapterCount: number; + enrolled?: boolean; }[]; } @@ -451,12 +449,12 @@ export interface GetCourseResponse { title: string; type: "text" | "presentation" | "video" | "quiz"; displayOrder: number; - status: "completed" | "in_progress" | "not_started"; + status: "not_started" | "in_progress" | "completed"; quizQuestionCount: number | null; isExternal?: boolean; }[]; completedLessonCount?: number; - chapterProgress?: "completed" | "in_progress" | "not_started"; + chapterProgress?: "not_started" | "in_progress" | "completed"; isFreemium?: boolean; enrolled?: boolean; isSubmitted?: boolean; @@ -539,7 +537,7 @@ export interface GetBetaCourseByIdResponse { updatedAt?: string; }[]; completedLessonCount?: number; - chapterProgress?: "completed" | "in_progress" | "not_started"; + chapterProgress?: "not_started" | "in_progress" | "completed"; isFreemium?: boolean; enrolled?: boolean; isSubmitted?: boolean; @@ -703,12 +701,12 @@ export interface GetChapterWithLessonResponse { title: string; type: "text" | "presentation" | "video" | "quiz"; displayOrder: number; - status: "completed" | "in_progress" | "not_started"; + status: "not_started" | "in_progress" | "completed"; quizQuestionCount: number | null; isExternal?: boolean; }[]; completedLessonCount?: number; - chapterProgress?: "completed" | "in_progress" | "not_started"; + chapterProgress?: "not_started" | "in_progress" | "completed"; isFreemium?: boolean; enrolled?: boolean; isSubmitted?: boolean; @@ -765,7 +763,7 @@ export type BetaCreateChapterBody = { }[]; updatedAt?: string; }[]; - chapterProgress?: "completed" | "in_progress" | "not_started"; + chapterProgress?: "not_started" | "in_progress" | "completed"; isFreemium?: boolean; enrolled?: boolean; isSubmitted?: boolean; @@ -831,7 +829,7 @@ export type UpdateChapterBody = { }[]; updatedAt?: string; }[]; - chapterProgress?: "completed" | "in_progress" | "not_started"; + chapterProgress?: "not_started" | "in_progress" | "completed"; isFreemium?: boolean; enrolled?: boolean; isSubmitted?: boolean; @@ -1604,7 +1602,7 @@ export class API extends HttpClient ({ }); return response.data; }, - select: (data: GetAllCoursesResponse) => data.data, + select: (data: GetAvailableCoursesResponse) => data.data, }); export function useAvailableCourses(searchParams?: CourseParams) { diff --git a/apps/web/app/api/queries/useStudentCourses.ts b/apps/web/app/api/queries/useStudentCourses.ts index b46e54002..c75b62842 100644 --- a/apps/web/app/api/queries/useStudentCourses.ts +++ b/apps/web/app/api/queries/useStudentCourses.ts @@ -2,7 +2,7 @@ import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; import { ApiClient } from "../api-client"; -import type { GetAllCoursesResponse } from "../generated-api"; +import type { GetStudentCoursesResponse } from "../generated-api"; import type { SortOption } from "~/types/sorting"; type CourseParams = { @@ -25,7 +25,7 @@ export const studentCoursesQueryOptions = (searchParams?: CourseParams) => ({ }); return response.data; }, - select: (data: GetAllCoursesResponse) => data.data, + select: (data: GetStudentCoursesResponse) => data.data, }); export function useStudentCourses(searchParams?: CourseParams) { diff --git a/apps/web/app/api/queries/useUsers.ts b/apps/web/app/api/queries/useUsers.ts index e051fbf49..4e0de2ed2 100644 --- a/apps/web/app/api/queries/useUsers.ts +++ b/apps/web/app/api/queries/useUsers.ts @@ -3,10 +3,11 @@ import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; import { ApiClient } from "../api-client"; import type { GetUsersResponse } from "../generated-api"; +import type { UserRole } from "~/config/userRoles"; type UsersParams = { keyword?: string; - role?: string; + role?: UserRole; archived?: boolean; sort?: string; }; diff --git a/apps/web/app/modules/Admin/EditCourse/CourseLessons/NewLesson/QuizLessonForm/QuizLessonForm.tsx b/apps/web/app/modules/Admin/EditCourse/CourseLessons/NewLesson/QuizLessonForm/QuizLessonForm.tsx index 5b79c9173..bfd38de0d 100644 --- a/apps/web/app/modules/Admin/EditCourse/CourseLessons/NewLesson/QuizLessonForm/QuizLessonForm.tsx +++ b/apps/web/app/modules/Admin/EditCourse/CourseLessons/NewLesson/QuizLessonForm/QuizLessonForm.tsx @@ -171,7 +171,9 @@ const QuizLessonForm = ({ } if (!noOptionsRequiredTypes.includes(type)) { - return [{ sortableId: crypto.randomUUID(), optionText: "", isCorrect: false, displayOrder: 1 }]; + return [ + { sortableId: crypto.randomUUID(), optionText: "", isCorrect: false, displayOrder: 1 }, + ]; } return []; diff --git a/apps/web/app/modules/Admin/EditCourse/CourseLessons/NewLesson/QuizLessonForm/components/AnswerSelectQuestion.tsx b/apps/web/app/modules/Admin/EditCourse/CourseLessons/NewLesson/QuizLessonForm/components/AnswerSelectQuestion.tsx index 348fbe087..be60606fa 100644 --- a/apps/web/app/modules/Admin/EditCourse/CourseLessons/NewLesson/QuizLessonForm/components/AnswerSelectQuestion.tsx +++ b/apps/web/app/modules/Admin/EditCourse/CourseLessons/NewLesson/QuizLessonForm/components/AnswerSelectQuestion.tsx @@ -116,9 +116,9 @@ const AnswerSelectQuestion = ({ form, questionIndex }: AnswerSelectQuestionProps return (
-
+
- + handleOptionChange(index, "isCorrect", true)} - className="w-4 h-4 cursor-pointer" + className="h-4 w-4 cursor-pointer" /> ) : (
@@ -157,7 +157,7 @@ const AnswerSelectQuestion = ({ form, questionIndex }: AnswerSelectQuestionProps onClick={() => handleOptionChange(index, "isCorrect", !item.isCorrect) } - className="ml-2 body-sm align-middle text-neutral-950 cursor-pointer" + className="body-sm ml-2 cursor-pointer align-middle text-neutral-950" > {t("adminCourseView.curriculum.lesson.other.correct")} @@ -167,7 +167,7 @@ const AnswerSelectQuestion = ({ form, questionIndex }: AnswerSelectQuestionProps
handleRemoveOption(index)} />
@@ -175,7 +175,7 @@ const AnswerSelectQuestion = ({ form, questionIndex }: AnswerSelectQuestionProps {t("common.button.delete")} diff --git a/apps/web/app/modules/Admin/EditCourse/CourseLessons/NewLesson/QuizLessonForm/components/ScaleQuestion.tsx b/apps/web/app/modules/Admin/EditCourse/CourseLessons/NewLesson/QuizLessonForm/components/ScaleQuestion.tsx index 15bd82dbe..de69ebf47 100644 --- a/apps/web/app/modules/Admin/EditCourse/CourseLessons/NewLesson/QuizLessonForm/components/ScaleQuestion.tsx +++ b/apps/web/app/modules/Admin/EditCourse/CourseLessons/NewLesson/QuizLessonForm/components/ScaleQuestion.tsx @@ -103,7 +103,7 @@ const ScaleQuestion = ({ form, questionIndex }: ScaleQuestionProps) => { className="grid grid-cols-1" renderItem={(item, index: number) => ( -
+
diff --git a/apps/web/app/modules/Admin/Users/Users.page.tsx b/apps/web/app/modules/Admin/Users/Users.page.tsx index 84028ef81..f851379c7 100644 --- a/apps/web/app/modules/Admin/Users/Users.page.tsx +++ b/apps/web/app/modules/Admin/Users/Users.page.tsx @@ -38,6 +38,7 @@ import { } from "~/modules/common/SearchFilter/SearchFilter"; import type { GetUsersResponse } from "~/api/generated-api"; +import type { UserRole } from "~/config/userRoles"; type TUser = GetUsersResponse["data"][number]; @@ -50,7 +51,7 @@ const Users = () => { const navigate = useNavigate(); const [searchParams, setSearchParams] = React.useState<{ keyword?: string; - role?: string; + role?: UserRole; archived?: boolean; status?: string; }>({}); diff --git a/apps/web/app/modules/Courses/Courses.page.tsx b/apps/web/app/modules/Courses/Courses.page.tsx index f23a9464a..c23c4793b 100644 --- a/apps/web/app/modules/Courses/Courses.page.tsx +++ b/apps/web/app/modules/Courses/Courses.page.tsx @@ -10,13 +10,12 @@ import { } from "~/api/queries"; import { useAvailableCourses } from "~/api/queries/useAvailableCourses"; import { categoriesQueryOptions, useCategoriesSuspense } from "~/api/queries/useCategories"; -import { allCoursesQueryOptions, useCourses } from "~/api/queries/useCourses"; +import { allCoursesQueryOptions } from "~/api/queries/useCourses"; import { useStudentCourses } from "~/api/queries/useStudentCourses"; import { queryClient } from "~/api/queryClient"; import { ButtonGroup } from "~/components/ButtonGroup/ButtonGroup"; import { Icon } from "~/components/Icon"; import { PageWrapper } from "~/components/PageWrapper"; -import { useUserRole } from "~/hooks/useUserRole"; import { cn } from "~/lib/utils"; import { SORT_OPTIONS, type SortOption } from "~/types/sorting"; @@ -82,7 +81,6 @@ function reducer(state: State, action: Action): State { } export default function CoursesPage() { - const { isAdmin, isTeacher } = useUserRole(); const { t } = useTranslation(); const [state, dispatch] = useReducer(reducer, { searchTitle: undefined, @@ -98,25 +96,6 @@ export default function CoursesPage() { sort: state.sort, }); - const { data: allCourses, isLoading: isAllCoursesLoading } = useCourses( - { - title: state.searchTitle, - category: state.category, - sort: state.sort, - }, - { enabled: isAdmin || isTeacher }, - ); - - const availableCourses = match(isAdmin) - .with(true, () => allCourses ?? []) - .with(false, () => userAvailableCourses ?? []) - .exhaustive(); - - const isCoursesLoading = match(isAdmin) - .with(true, () => isAllCoursesLoading) - .with(false, () => isAvailableCoursesLoading) - .exhaustive(); - const { data: categories, isLoading: isCategoriesLoading } = useCategoriesSuspense(); const { courseListLayout, setCourseListLayout } = useLayoutsStore(); @@ -241,11 +220,14 @@ export default function CoursesPage() { block: courseListLayout === "table", })} > - {availableCourses && !isEmpty(availableCourses) && ( - + {userAvailableCourses && !isEmpty(userAvailableCourses) && ( + )} - {!availableCourses || - (isEmpty(availableCourses) && ( + {!userAvailableCourses || + (isEmpty(userAvailableCourses) && (
@@ -260,7 +242,7 @@ export default function CoursesPage() {
))} - {isCoursesLoading && ( + {isAvailableCoursesLoading && (
diff --git a/apps/web/app/modules/Courses/Lesson/Lesson.page.tsx b/apps/web/app/modules/Courses/Lesson/Lesson.page.tsx index 4dbca5400..74d03990c 100644 --- a/apps/web/app/modules/Courses/Lesson/Lesson.page.tsx +++ b/apps/web/app/modules/Courses/Lesson/Lesson.page.tsx @@ -128,7 +128,7 @@ export default function LessonPage() { isLastLesson={isLast} />
- +
); diff --git a/apps/web/app/modules/Courses/Lesson/LessonSidebar.tsx b/apps/web/app/modules/Courses/Lesson/LessonSidebar.tsx index dd0a00660..b2aff831a 100644 --- a/apps/web/app/modules/Courses/Lesson/LessonSidebar.tsx +++ b/apps/web/app/modules/Courses/Lesson/LessonSidebar.tsx @@ -3,7 +3,6 @@ import { last, startCase } from "lodash-es"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useCourse } from "~/api/queries"; import CourseProgress from "~/components/CourseProgress"; import { Icon } from "~/components/Icon"; import { @@ -17,8 +16,10 @@ import { CategoryChip } from "~/components/ui/CategoryChip"; import { cn } from "~/lib/utils"; import { LessonTypesIcons } from "~/modules/Courses/CourseView/lessonTypes"; +import type { GetCourseResponse } from "~/api/generated-api"; + type LessonSidebarProps = { - courseId: string; + course: GetCourseResponse["data"]; lessonId: string; }; @@ -28,9 +29,7 @@ const progressBadge = { not_started: "NotStartedRounded", } as const; -export const LessonSidebar = ({ courseId, lessonId }: LessonSidebarProps) => { - const { data: course } = useCourse(courseId); - +export const LessonSidebar = ({ course, lessonId }: LessonSidebarProps) => { const { state } = useLocation(); const [activeChapter, setActiveChapter] = useState(state?.chapterId); const { t } = useTranslation(); @@ -101,7 +100,7 @@ export const LessonSidebar = ({ courseId, lessonId }: LessonSidebarProps) => { return ( = ({ availableCourses, courseListLayout }) => match(courseListLayout) diff --git a/apps/web/app/modules/Courses/components/CoursesCarousel.tsx b/apps/web/app/modules/Courses/components/CoursesCarousel.tsx index 0ae186786..5344daa18 100644 --- a/apps/web/app/modules/Courses/components/CoursesCarousel.tsx +++ b/apps/web/app/modules/Courses/components/CoursesCarousel.tsx @@ -8,10 +8,10 @@ import { import { cn } from "~/lib/utils"; import CourseCard from "~/modules/Dashboard/Courses/CourseCard"; -import type { GetAllCoursesResponse } from "~/api/generated-api"; +import type { GetAvailableCoursesResponse } from "~/api/generated-api"; type CoursesCarouselProps = { - courses?: GetAllCoursesResponse["data"]; + courses?: GetAvailableCoursesResponse["data"]; buttonContainerClasses?: string; }; diff --git a/apps/web/app/modules/Dashboard/Courses/CourseList.tsx b/apps/web/app/modules/Dashboard/Courses/CourseList.tsx index ce8482109..99a00bb2d 100644 --- a/apps/web/app/modules/Dashboard/Courses/CourseList.tsx +++ b/apps/web/app/modules/Dashboard/Courses/CourseList.tsx @@ -4,11 +4,11 @@ import { TableCourseList } from "~/modules/Courses/components/TableCourseList"; import { CardCourseList } from "./CardCourseList"; -import type { GetAllCoursesResponse } from "~/api/generated-api"; +import type { GetAvailableCoursesResponse } from "~/api/generated-api"; import type { CourseListLayout } from "~/types/shared"; export const CourseList: React.FC<{ - availableCourses: GetAllCoursesResponse["data"]; + availableCourses: GetAvailableCoursesResponse["data"]; courseListLayout: CourseListLayout; }> = ({ availableCourses, courseListLayout }) => match(courseListLayout) diff --git a/apps/web/app/modules/Dashboard/Courses/CoursesCarousel.tsx b/apps/web/app/modules/Dashboard/Courses/CoursesCarousel.tsx index 0ae186786..5344daa18 100644 --- a/apps/web/app/modules/Dashboard/Courses/CoursesCarousel.tsx +++ b/apps/web/app/modules/Dashboard/Courses/CoursesCarousel.tsx @@ -8,10 +8,10 @@ import { import { cn } from "~/lib/utils"; import CourseCard from "~/modules/Dashboard/Courses/CourseCard"; -import type { GetAllCoursesResponse } from "~/api/generated-api"; +import type { GetAvailableCoursesResponse } from "~/api/generated-api"; type CoursesCarouselProps = { - courses?: GetAllCoursesResponse["data"]; + courses?: GetAvailableCoursesResponse["data"]; buttonContainerClasses?: string; };