Skip to content

Commit

Permalink
refactor: Course Preview + fixes (#403)
Browse files Browse the repository at this point in the history
* refactor: add missing translations

* fix: missing values in seeds

* refactor: change name and course_description

* fix: questionAnswerToString

* feat: course preview

* refactor: add missing properties to mock useUserRole hook

* refactor: apply review feedback
  • Loading branch information
piotr-pajak authored Jan 28, 2025
1 parent 2a625c1 commit 8af07f7
Show file tree
Hide file tree
Showing 17 changed files with 89 additions and 58 deletions.
3 changes: 1 addition & 2 deletions apps/api/src/lesson/lesson.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { RolesGuard } from "src/common/guards/roles.guard";
import { USER_ROLES, UserRole } from "src/user/schemas/userRoles";

import {
AnswerQuestionBody,
answerQuestionsForLessonBody,
CreateLessonBody,
createLessonSchema,
Expand All @@ -29,7 +30,6 @@ import {
updateLessonSchema,
UpdateQuizLessonBody,
updateQuizLessonSchema,
AnswerQuestionBody,
} from "./lesson.schema";
import { AdminLessonService } from "./services/adminLesson.service";
import { LessonService } from "./services/lesson.service";
Expand All @@ -45,7 +45,6 @@ export class LessonController {
) {}

@Get(":id")
@Roles(...Object.values(USER_ROLES))
@Validate({
response: baseResponse(lessonShowSchema),
})
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/lesson/services/lesson.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class LessonService {

if (!lesson) throw new NotFoundException("Lesson not found");

if (!lesson.isFreemium && !lesson.isEnrolled)
if (isStudent && !lesson.isFreemium && !lesson.isEnrolled)
throw new UnauthorizedException("You don't have access");

if (lesson.type === LESSON_TYPES.TEXT && !lesson.fileUrl) return lesson;
Expand Down
10 changes: 5 additions & 5 deletions apps/api/src/questions/question.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,12 @@ export class QuestionService {
}

private questionAnswerToString(answers: string[]): SQL<unknown> {
const convertedAnswers = answers.reduce((acc, answer, index) => {
acc.push(`'${index + 1}'`, `'${answer}'`);
return acc;
}, [] as string[]);
const convertedAnswers: (SQL<unknown> | string)[] = answers.flatMap((answer, index) => [
sql`(${index + 1}::int)`,
sql`${answer}::text`,
]);

return sql`json_build_object(${sql.raw(convertedAnswers.join(","))})`;
return sql`json_build_object(${sql.join(convertedAnswers, sql`, `)})`;
}

private isAnswerWithId(answer: unknown): answer is OnlyAnswerIdAsnwer | FullAnswer {
Expand Down
8 changes: 7 additions & 1 deletion apps/api/src/seed/e2e-data-seeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { NiceCourseData } from "../utils/types/test-types";

export const e2eCourses: NiceCourseData[] = [
{
title: "For E2E Testing",
title: "E2E Test: Automated Course for Full-Stack Development",
description:
"This course is specifically generated for end-to-end testing purposes. It includes mock content to simulate a comprehensive learning experience in full-stack web development. Topics cover front-end frameworks like React and Next.js, back-end technologies such as Node.js and Nest.js, and database integration. This course ensures thorough testing of user interactions, workflows, and application features.",
isPublished: true,
Expand Down Expand Up @@ -119,6 +119,12 @@ export const e2eCourses: NiceCourseData[] = [
description: "E2E testing is used to test the [word] of an application.",
solutionExplanation:
"E2E testing is used to test the <strong>workflow</strong> of an application.",
options: [
{
optionText: "workflow",
isCorrect: true,
},
],
},
],
},
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/seed/nice-data-seeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1779,7 +1779,7 @@ export const niceCourses: NiceCourseData[] = [
},
{
optionText: "feel",
isCorrect: true,
isCorrect: false,
},
{
optionText: "start",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/Guards/__tests__/RouteGuard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe("RouteGuard", () => {
role: "admin",
isAdmin: true,
isTeacher: false,
isAdminLike: true,
});

const RemixStub = createRemixStub([
Expand Down Expand Up @@ -47,6 +48,7 @@ describe("RouteGuard", () => {
role: "student",
isAdmin: false,
isTeacher: false,
isAdminLike: false,
});

const RemixStub = createRemixStub([
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/hooks/useUserRole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const useUserRole = () => {

const isAdmin = role === USER_ROLE.admin;
const isTeacher = role === USER_ROLE.teacher;
const isAdminLike = isAdmin || isTeacher;

return { role, isAdmin, isTeacher };
return { role, isAdmin, isTeacher, isAdminLike };
};
1 change: 1 addition & 0 deletions apps/web/app/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@
"clientStatisticsView": {
"header": "Welcome back,",
"button": {
"enroll": "Enroll",
"searchCourses": "Search courses",
"continue": "Continue",
"start": "Start",
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/locales/pl/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@
"clientStatisticsView": {
"header": "Witaj ponownie,",
"button": {
"enroll": "Zapisz się",
"searchCourses": "Szukaj kursów",
"continue": "Kontynuuj",
"start": "Rozpocznij",
Expand Down
14 changes: 9 additions & 5 deletions apps/web/app/modules/Admin/EditCourse/EditCourse.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useParams, useSearchParams } from "@remix-run/react";
import { Link, useParams, useSearchParams } from "@remix-run/react";
import { useTranslation } from "react-i18next";

import { useBetaCourseById } from "~/api/queries/admin/useBetaCourse";
Expand Down Expand Up @@ -61,10 +61,14 @@ const EditCourse = () => {
</span>
)}
</h4>
<Button className="flex justify-end border border-neutral-200 bg-transparent text-primary-700">
<Icon name="Eye" className="mr-2" />
{t("adminCourseView.common.preview")}{" "}
<Icon name="ArrowDown" className="ml-2 text-neutral-500" />
<Button
asChild
className="flex justify-end border border-neutral-200 bg-transparent text-primary-700"
>
<Link to={`/course/${course?.id}`}>
<Icon name="Eye" className="mr-2" />
{t("adminCourseView.common.preview")}{" "}
</Link>
</Button>
</div>
<TabsList className="w-min">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const findFirstNotStartedLessonId = (course: CourseProgressProps["course"]) => {
};

export const CourseProgress = ({ course }: CourseProgressProps) => {
const { isAdmin, isTeacher } = useUserRole();
const { isAdminLike } = useUserRole();
const navigate = useNavigate();
const { t } = useTranslation();
const nonStartedLessonId = findFirstNotStartedLessonId(course);
Expand All @@ -33,8 +33,6 @@ export const CourseProgress = ({ course }: CourseProgressProps) => {
({ completedLessonCount }) => completedLessonCount,
);

const isAdminLike = isAdmin || isTeacher;

return (
<>
<h4 className="h6 pb-1 text-neutral-950">
Expand Down Expand Up @@ -63,32 +61,32 @@ export const CourseProgress = ({ course }: CourseProgressProps) => {
/>
<span>{t("studentCourseView.sideSection.button.shareCourse")}</span>
</CopyUrlButton>
{!isAdminLike && (
<>
<Button
className="gap-x-2"
disabled={!nonStartedLessonId}
onClick={() =>
navigate(`lesson/${nonStartedLessonId}`, {
state: { chapterId: notStartedChapterId },
})
}
>
<Icon name="Play" className="h-auto w-6 text-white" />
<span>
{t(
hasCourseProgress
<>
<Button
className="gap-x-2"
disabled={!nonStartedLessonId}
onClick={() =>
navigate(`lesson/${nonStartedLessonId}`, {
state: { chapterId: notStartedChapterId },
})
}
>
<Icon name="Play" className="h-auto w-6 text-white" />
<span>
{t(
isAdminLike
? "adminCourseView.common.preview"
: hasCourseProgress
? "studentCourseView.sideSection.button.continueLearning"
: "studentCourseView.sideSection.button.startLearning",
)}
</span>
</Button>
<p className="details flex items-center justify-center gap-x-2 text-neutral-800">
<Icon name="Info" className="h-auto w-4 text-neutral-800" />
<span>{t("studentCourseView.sideSection.other.informationText")}</span>
</p>
</>
)}
)}
</span>
</Button>
<p className="details flex items-center justify-center gap-x-2 text-neutral-800">
<Icon name="Info" className="h-auto w-4 text-neutral-800" />
<span>{t("studentCourseView.sideSection.other.informationText")}</span>
</p>
</>
</div>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ type CourseViewSidebar = {

export const CourseViewSidebar = ({ course }: CourseViewSidebar) => {
const { data: userDetails } = useUserDetails(course?.authorId ?? "");
const { isAdmin, isTeacher } = useUserRole();
const { isAdminLike } = useUserRole();
const { t } = useTranslation();

const shouldShowCourseOptions = !isAdmin && !isTeacher && !course?.enrolled;
const shouldShowCourseOptions = !course?.enrolled && !isAdminLike;

return (
<section className="sticky left-0 top-6 flex h-min flex-col gap-y-6 rounded-b-lg rounded-t-2xl bg-white p-8 drop-shadow xl:w-full xl:max-w-[480px] 3xl:top-12">
Expand Down
7 changes: 7 additions & 0 deletions apps/web/app/modules/Courses/Lesson/LessonContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Icon } from "~/components/Icon";
import Viewer from "~/components/RichText/Viever";
import { Button } from "~/components/ui/button";
import { Video } from "~/components/VideoPlayer/Video";
import { useUserRole } from "~/hooks/useUserRole";
import { LessonType } from "~/modules/Admin/EditCourse/EditCourse.types";
import { Quiz } from "~/modules/Courses/Lesson/Quiz";

Expand Down Expand Up @@ -35,8 +36,11 @@ export const LessonContent = ({
const [isNextDisabled, setIsNextDisabled] = useState(false);
const { mutate: markLessonAsCompleted } = useMarkLessonAsCompleted();
const { t } = useTranslation();
const { isAdminLike } = useUserRole();

useEffect(() => {
if (isAdminLike) return;

if (lesson.type == LessonType.QUIZ || lesson.type == LessonType.VIDEO) {
return setIsNextDisabled(!lesson.lessonCompleted);
}
Expand All @@ -62,6 +66,9 @@ export const LessonContent = ({

const handleMarkLessonAsComplete = () => {
handleNext();

if (isAdminLike) return;

markLessonAsCompleted({ lessonId: lesson.id });
};

Expand Down
7 changes: 4 additions & 3 deletions apps/web/app/modules/Courses/Lesson/LessonSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const LessonSidebar = ({ course, lessonId }: LessonSidebarProps) => {
label={t("studentLessonView.sideSection.other.courseProgress")}
completedLessonCount={course.completedChapterCount ?? 0}
courseLessonCount={course.courseChapterCount ?? 0}
isCompleted={course.completedChapterCount === course.courseChapterCount}
/>
</div>
<div className="flex flex-col gap-y-4 px-4">
Expand Down Expand Up @@ -93,7 +94,7 @@ export const LessonSidebar = ({ course, lessonId }: LessonSidebarProps) => {
iconClasses="w-6 h-auto shrink-0"
/>
<div className="body-base-md w-full text-start text-neutral-950">{title}</div>
<Icon name="CarretDownLarge" className="size-6 text-primary-700" />
<Icon name="CarretDownLarge" className="text-primary-700 size-6" />
</AccordionTrigger>
<AccordionContent className="flex flex-col rounded-b-lg border border-t-0">
{lessons?.map(({ id, title, status, type }) => {
Expand All @@ -103,7 +104,7 @@ export const LessonSidebar = ({ course, lessonId }: LessonSidebarProps) => {
to={status === "completed" ? `/course/${course.id}/lesson/${id}` : "#"}
className={cn("flex gap-x-4 px-6 py-2 hover:bg-neutral-50", {
"cursor-not-allowed": status === "not_started",
"border-l-2 border-l-primary-600 bg-primary-50 last:rounded-es-lg":
"border-l-primary-600 bg-primary-50 border-l-2 last:rounded-es-lg":
lessonId === id,
})}
>
Expand All @@ -124,7 +125,7 @@ export const LessonSidebar = ({ course, lessonId }: LessonSidebarProps) => {
</div>
<Icon
name={LessonTypesIcons[type]}
className="size-6 text-primary-700"
className="text-primary-700 size-6"
/>
</Link>
);
Expand Down
4 changes: 3 additions & 1 deletion apps/web/app/modules/Courses/Lesson/Quiz.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { queryClient } from "~/api/queryClient";
import { Icon } from "~/components/Icon";
import { Button } from "~/components/ui/button";
import { toast } from "~/components/ui/use-toast";
import { useUserRole } from "~/hooks/useUserRole";

import { Questions } from "./Questions";
import { QuizFormSchema } from "./schemas";
Expand All @@ -23,6 +24,7 @@ type QuizProps = {
export const Quiz = ({ lesson }: QuizProps) => {
const { lessonId = "" } = useParams();
const { t } = useTranslation();
const { isAdminLike } = useUserRole();

const questions = lesson.quizDetails?.questions;

Expand Down Expand Up @@ -55,7 +57,7 @@ export const Quiz = ({ lesson }: QuizProps) => {
})}
>
<Questions questions={questions} isQuizCompleted={lesson.lessonCompleted} />
<Button type="submit" className="flex items-center gap-x-2 self-end">
<Button type="submit" className="flex items-center gap-x-2 self-end" disabled={isAdminLike}>
<span>{t("studentLessonView.button.submit")}</span>
<Icon name="ArrowRight" className="h-auto w-4" />
</Button>
Expand Down
7 changes: 4 additions & 3 deletions apps/web/e2e/tests/admin/course-settings.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { expect, test, type Page } from "@playwright/test";
import { expect, type Page, test } from "@playwright/test";

const TEST_COURSE = {
name: "For E2E Testing",
course_description: "DO NOT DELETE THIS COURSE.",
name: "E2E Test: Automated Course for Full-Stack Development",
course_description:
"This course is specifically generated for end-to-end testing purposes. It includes mock content to simulate a comprehensive learning experience in full-stack web development. Topics cover front-end frameworks like React and Next.js, back-end technologies such as Node.js and Nest.js, and database integration. This course ensures thorough testing of user interactions, workflows, and application features.",
} as const;

const URL_PATTERNS = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ const URL_PATTERNS = {
} as const;

const goToCoursePage = async (page: Page) => {
await page
.getByRole("link", { name: "E2E Test: Automated Course for Full-Stack Development" })
.getByRole("button")
.click();
await page.getByText("E2E Test: Automated Course for Full-Stack Development").click();
};

const navigateTroughTextLesson = async (page: Page, nextButton: Locator) => {
Expand Down Expand Up @@ -135,12 +132,21 @@ test.describe("Course Workflow", () => {

const enrollButton = page.locator('button:has-text("Enroll to the course")');

if ((await enrollButton.count()) > 0) {
if ((await enrollButton.count()) > 0 && (await enrollButton.isVisible())) {
await enrollButton.click();
}
await page.waitForLoadState("networkidle");

await page.getByRole("button", { name: "Continue learning" }).click();
await page.waitForTimeout(1000);

const startLearningButton = page.locator('button:has-text("Start learning")');
await page.waitForLoadState("networkidle");

if ((await startLearningButton.count()) > 0 && (await startLearningButton.isVisible())) {
await startLearningButton.click();
} else {
await page.getByRole("button", { name: "Continue learning" }).click();
}

await page.waitForURL(URL_PATTERNS.lesson);
await page.waitForLoadState("networkidle");
Expand All @@ -153,6 +159,8 @@ test.describe("Course Workflow", () => {
const nextButtonLocator = page.locator('button:has-text("Next")');
const completeButtonLocator = page.locator('button:has-text("Complete")');

await page.waitForTimeout(250);

const nextButton =
(await nextButtonLocator.count()) > 0 ? nextButtonLocator : completeButtonLocator;

Expand Down

0 comments on commit 8af07f7

Please sign in to comment.