From 9ea47e694be85349d4a5d5bb751d5fe1454058f6 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Wed, 31 Jul 2024 22:54:00 +0200 Subject: [PATCH] feat: implement modules page --- .../[targetLanguageCode]/module-card.tsx | 37 ++++++++++++++++ .../courses/[targetLanguageCode]/page.tsx | 44 +++++++++++++------ apps/librelingo-web/src/data/course.ts | 35 +++++++++++---- apps/librelingo-web/src/data/utils.ts | 5 +++ e2e-tests/course.spec.ts | 23 ++++++++++ e2e-tests/home.spec.ts | 4 +- src/librelingo_json_export/module.py | 1 + .../tests/test_course_get_course_data.py | 5 ++- 8 files changed, 129 insertions(+), 25 deletions(-) create mode 100644 apps/librelingo-web/src/app/[sourceLanguageCode]/courses/[targetLanguageCode]/module-card.tsx create mode 100644 apps/librelingo-web/src/data/utils.ts diff --git a/apps/librelingo-web/src/app/[sourceLanguageCode]/courses/[targetLanguageCode]/module-card.tsx b/apps/librelingo-web/src/app/[sourceLanguageCode]/courses/[targetLanguageCode]/module-card.tsx new file mode 100644 index 000000000000..43c4be1a9024 --- /dev/null +++ b/apps/librelingo-web/src/app/[sourceLanguageCode]/courses/[targetLanguageCode]/module-card.tsx @@ -0,0 +1,37 @@ +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { CourseDetail, Module } from '@/data/course' +import Link from 'next/link' + +type Props = { + course: CourseDetail + module: Module +} + +export default function ModuleCard({ course, module }: Props) { + const coursePageUrl = `/${course.sourceLanguage.code}/courses/${course.targetLanguage.code}/courses/${module.slug}` + + return ( + + + {module.title} + Card Description + + +

Card Content

+
+ + + +
+ ) +} diff --git a/apps/librelingo-web/src/app/[sourceLanguageCode]/courses/[targetLanguageCode]/page.tsx b/apps/librelingo-web/src/app/[sourceLanguageCode]/courses/[targetLanguageCode]/page.tsx index fea9b2f476bc..f7bddd146a2b 100644 --- a/apps/librelingo-web/src/app/[sourceLanguageCode]/courses/[targetLanguageCode]/page.tsx +++ b/apps/librelingo-web/src/app/[sourceLanguageCode]/courses/[targetLanguageCode]/page.tsx @@ -1,24 +1,40 @@ -import { getCourseDetail, getCourseId, listAvailableCourses } from "@/data/course" +import { + getCourseDetail, + getCourseId, + listAvailableCourses, +} from '@/data/course' +import ModuleCard from './module-card' export async function generateStaticParams() { - const courses = await listAvailableCourses() + const courses = await listAvailableCourses() - return courses.map((course) => ({ - sourceLanguageCode: course.uiLanguage, - targetLanguageCode: course.languageCode, - })) + return courses.map((course) => ({ + sourceLanguageCode: course.uiLanguage, + targetLanguageCode: course.languageCode, + })) } type Props = { - params: { - sourceLanguageCode: string - targetLanguageCode: string - } + params: { + sourceLanguageCode: string + targetLanguageCode: string + } } -export default async function CourseHomePage({params}: Props) { - const courseId = await getCourseId(params) - const detail = await getCourseDetail(courseId) +export default async function CourseHomePage({ params }: Props) { + const courseId = await getCourseId(params) + const detail = await getCourseDetail(courseId) - return

{detail.targetLanguage.name}

+ return ( + <> +

{detail.targetLanguage.name}

+ + + ) } diff --git a/apps/librelingo-web/src/data/course.ts b/apps/librelingo-web/src/data/course.ts index 6f8a7273bc35..01429923909c 100644 --- a/apps/librelingo-web/src/data/course.ts +++ b/apps/librelingo-web/src/data/course.ts @@ -4,6 +4,7 @@ import path from 'node:path' import courseConfig from '@/courses/config.json' import fs from 'node:fs' import { notFound } from 'next/navigation' +import { getAbsoluteCoursePath } from './utils' export type CourseIdentityDescription = { sourceLanguageCode: string @@ -20,13 +21,7 @@ export type Course = { } function getFullJsonPath(jsonPath: string) { - return path.join( - process.cwd(), - 'src', - 'courses', - jsonPath, - 'courseData.json' - ) + return path.join(getAbsoluteCoursePath(jsonPath), 'courseData.json') } async function getCourseMetadataByJsonPath(jsonPath: string) { @@ -86,12 +81,34 @@ export async function getCourseId( return course.id } -export async function getCourseDetail(courseId: string) { - const { languageName } = await getCourseMetadataByJsonPath(courseId) +export type Module = { + title: string + slug: string +} + +export type CourseDetail = { + targetLanguage: { + name: string + code: string + } + sourceLanguage: { + code: string + } + modules: Module[] +} + +export async function getCourseDetail(courseId: string): Promise { + const { languageName, modules, uiLanguage, languageCode } = + await getCourseMetadataByJsonPath(courseId) return { targetLanguage: { name: languageName, + code: languageCode, + }, + sourceLanguage: { + code: uiLanguage }, + modules, } } diff --git a/apps/librelingo-web/src/data/utils.ts b/apps/librelingo-web/src/data/utils.ts new file mode 100644 index 000000000000..8e6cb0e68b6d --- /dev/null +++ b/apps/librelingo-web/src/data/utils.ts @@ -0,0 +1,5 @@ +import path from 'node:path' + +export function getAbsoluteCoursePath(jsonPath: string) { + return path.join(process.cwd(), 'src', 'courses', jsonPath) +} diff --git a/e2e-tests/course.spec.ts b/e2e-tests/course.spec.ts index bc7f324a8ba6..09df4361bcef 100644 --- a/e2e-tests/course.spec.ts +++ b/e2e-tests/course.spec.ts @@ -7,3 +7,26 @@ test('has the correct content', async ({ page }) => { page.getByRole('heading', { name: 'Test Language' }) ).toBeVisible() }) + +test('has cards leading to module pages', async ({ page }) => { + const courseHomePagePattern = new RegExp( + `[^/]*/courses/[^/]+/modules/[^/]+` + ) + await page.goto('/en/courses/test-1') + + const cards = await page.getByRole('listitem').all() + + expect(cards.length).toBeGreaterThanOrEqual(1) + const urls = new Set() + + for (const card of cards) { + const button = card.getByRole('link', { name: 'Learn' }) + const url = await button.getAttribute('href') + + expect(url).toMatch(courseHomePagePattern) + urls.add(url) + } + + // each course has to have a unique url + expect(urls.size).toBeGreaterThanOrEqual(cards.length) +}) diff --git a/e2e-tests/home.spec.ts b/e2e-tests/home.spec.ts index 551c219b1a10..2544c66f808f 100644 --- a/e2e-tests/home.spec.ts +++ b/e2e-tests/home.spec.ts @@ -10,7 +10,9 @@ test('has the correct content', async ({ page }) => { await expect(firstCard.getByRole('link', { name: 'Learn' })).toBeVisible() }) -test('all card buttons lead to URLs matching the pattern', async ({ page }) => { +test('has cards for each course leading to the course page', async ({ + page, +}) => { const courseHomePagePattern = new RegExp(`[^/]*/courses/[^/]+`) await page.goto('/') diff --git a/src/librelingo_json_export/module.py b/src/librelingo_json_export/module.py index 20d21b29f6aa..d51377f0d3af 100644 --- a/src/librelingo_json_export/module.py +++ b/src/librelingo_json_export/module.py @@ -29,6 +29,7 @@ def get_levels(words, phrases): return calculate_number_of_levels(len(words), len(phrases)) return { + "slug": slugify(module.title), "title": module.title, "skills": [ { diff --git a/src/librelingo_json_export/tests/test_course_get_course_data.py b/src/librelingo_json_export/tests/test_course_get_course_data.py index e2564f117830..b4394506f80e 100644 --- a/src/librelingo_json_export/tests/test_course_get_course_data.py +++ b/src/librelingo_json_export/tests/test_course_get_course_data.py @@ -39,6 +39,7 @@ def test__get_course_data_return_value(): }, "modules": [ { + "slug": "basics", "title": "Basics", "skills": [ { @@ -67,7 +68,7 @@ def test__get_course_data_return_value(): }, ], }, - {"title": "Phrases", "skills": []}, + {"slug": "phrases", "title": "Phrases", "skills": []}, ], } @@ -89,6 +90,7 @@ def test__get_course_data_return_value_2(): }, "modules": [ { + "slug": "animals", "title": "Animals", "skills": [ { @@ -122,6 +124,7 @@ def test__get_course_data_return_value_with_introduction(): }, "modules": [ { + "slug": "animals", "title": "Animals", "skills": [ {