Skip to content

Commit 7b7a6c1

Browse files
committed
feat: implement modules page
1 parent 6c6e4c6 commit 7b7a6c1

File tree

9 files changed

+130
-25
lines changed

9 files changed

+130
-25
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Button } from '@/components/ui/button'
2+
import {
3+
Card,
4+
CardContent,
5+
CardDescription,
6+
CardFooter,
7+
CardHeader,
8+
CardTitle,
9+
} from '@/components/ui/card'
10+
import { CourseDetail, Module } from '@/data/course'
11+
import Link from 'next/link'
12+
13+
type Props = {
14+
course: CourseDetail
15+
module: Module
16+
}
17+
18+
export default function ModuleCard({ course, module }: Props) {
19+
const coursePageUrl = `/${course.sourceLanguage.code}/courses/${course.targetLanguage.code}/courses/${module.slug}`
20+
21+
return (
22+
<Card>
23+
<CardHeader>
24+
<CardTitle>{module.title}</CardTitle>
25+
<CardDescription>Card Description</CardDescription>
26+
</CardHeader>
27+
<CardContent>
28+
<p>Card Content</p>
29+
</CardContent>
30+
<CardFooter>
31+
<Button asChild>
32+
<Link href={coursePageUrl}>Learn</Link>
33+
</Button>
34+
</CardFooter>
35+
</Card>
36+
)
37+
}
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,40 @@
1-
import { getCourseDetail, getCourseId, listAvailableCourses } from "@/data/course"
1+
import {
2+
getCourseDetail,
3+
getCourseId,
4+
listAvailableCourses,
5+
} from '@/data/course'
6+
import ModuleCard from './module-card'
27

38
export async function generateStaticParams() {
4-
const courses = await listAvailableCourses()
9+
const courses = await listAvailableCourses()
510

6-
return courses.map((course) => ({
7-
sourceLanguageCode: course.uiLanguage,
8-
targetLanguageCode: course.languageCode,
9-
}))
11+
return courses.map((course) => ({
12+
sourceLanguageCode: course.uiLanguage,
13+
targetLanguageCode: course.languageCode,
14+
}))
1015
}
1116

1217
type Props = {
13-
params: {
14-
sourceLanguageCode: string
15-
targetLanguageCode: string
16-
}
18+
params: {
19+
sourceLanguageCode: string
20+
targetLanguageCode: string
21+
}
1722
}
1823

19-
export default async function CourseHomePage({params}: Props) {
20-
const courseId = await getCourseId(params)
21-
const detail = await getCourseDetail(courseId)
24+
export default async function CourseHomePage({ params }: Props) {
25+
const courseId = await getCourseId(params)
26+
const detail = await getCourseDetail(courseId)
2227

23-
return <h1>{detail.targetLanguage.name}</h1>
28+
return (
29+
<>
30+
<h1>{detail.targetLanguage.name}</h1>
31+
<ul className="flex space-y-6 flex-col p-6">
32+
{detail.modules.map((module) => (
33+
<li key={module.slug}>
34+
<ModuleCard course={detail} module={module} />
35+
</li>
36+
))}
37+
</ul>
38+
</>
39+
)
2440
}

Diff for: apps/librelingo-web/src/courses/test-1/courseData.json

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
},
3131
"modules": [
3232
{
33+
"slug": "basics",
3334
"title": "Basics",
3435
"skills": [
3536
{

Diff for: apps/librelingo-web/src/data/course.ts

+26-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'node:path'
44
import courseConfig from '@/courses/config.json'
55
import fs from 'node:fs'
66
import { notFound } from 'next/navigation'
7+
import { getAbsoluteCoursePath } from './utils'
78

89
export type CourseIdentityDescription = {
910
sourceLanguageCode: string
@@ -20,13 +21,7 @@ export type Course = {
2021
}
2122

2223
function getFullJsonPath(jsonPath: string) {
23-
return path.join(
24-
process.cwd(),
25-
'src',
26-
'courses',
27-
jsonPath,
28-
'courseData.json'
29-
)
24+
return path.join(getAbsoluteCoursePath(jsonPath), 'courseData.json')
3025
}
3126

3227
async function getCourseMetadataByJsonPath(jsonPath: string) {
@@ -86,12 +81,34 @@ export async function getCourseId(
8681
return course.id
8782
}
8883

89-
export async function getCourseDetail(courseId: string) {
90-
const { languageName } = await getCourseMetadataByJsonPath(courseId)
84+
export type Module = {
85+
title: string
86+
slug: string
87+
}
88+
89+
export type CourseDetail = {
90+
targetLanguage: {
91+
name: string
92+
code: string
93+
}
94+
sourceLanguage: {
95+
code: string
96+
}
97+
modules: Module[]
98+
}
99+
100+
export async function getCourseDetail(courseId: string): Promise<CourseDetail> {
101+
const { languageName, modules, uiLanguage, languageCode } =
102+
await getCourseMetadataByJsonPath(courseId)
91103

92104
return {
93105
targetLanguage: {
94106
name: languageName,
107+
code: languageCode,
108+
},
109+
sourceLanguage: {
110+
code: uiLanguage
95111
},
112+
modules,
96113
}
97114
}

Diff for: apps/librelingo-web/src/data/utils.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import path from 'node:path'
2+
3+
export function getAbsoluteCoursePath(jsonPath: string) {
4+
return path.join(process.cwd(), 'src', 'courses', jsonPath)
5+
}

Diff for: e2e-tests/course.spec.ts

+23
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,26 @@ test('has the correct content', async ({ page }) => {
77
page.getByRole('heading', { name: 'Test Language' })
88
).toBeVisible()
99
})
10+
11+
test('has cards leading to module pages', async ({ page }) => {
12+
const courseHomePagePattern = new RegExp(
13+
`[^/]*/courses/[^/]+/modules/[^/]+`
14+
)
15+
await page.goto('/en/courses/test-1')
16+
17+
const cards = await page.getByRole('listitem').all()
18+
19+
expect(cards.length).toBeGreaterThanOrEqual(1)
20+
const urls = new Set()
21+
22+
for (const card of cards) {
23+
const button = card.getByRole('link', { name: 'Learn' })
24+
const url = await button.getAttribute('href')
25+
26+
expect(url).toMatch(courseHomePagePattern)
27+
urls.add(url)
28+
}
29+
30+
// each course has to have a unique url
31+
expect(urls.size).toBeGreaterThanOrEqual(cards.length)
32+
})

Diff for: e2e-tests/home.spec.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ test('has the correct content', async ({ page }) => {
1010
await expect(firstCard.getByRole('link', { name: 'Learn' })).toBeVisible()
1111
})
1212

13-
test('all card buttons lead to URLs matching the pattern', async ({ page }) => {
13+
test('has cards for each course leading to the course page', async ({
14+
page,
15+
}) => {
1416
const courseHomePagePattern = new RegExp(`[^/]*/courses/[^/]+`)
1517
await page.goto('/')
1618

Diff for: src/librelingo_json_export/module.py

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def get_levels(words, phrases):
2929
return calculate_number_of_levels(len(words), len(phrases))
3030

3131
return {
32+
"slug": slugify(module.title),
3233
"title": module.title,
3334
"skills": [
3435
{

Diff for: src/librelingo_json_export/tests/test_course_get_course_data.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def test__get_course_data_return_value():
3939
},
4040
"modules": [
4141
{
42+
"slug": "basics",
4243
"title": "Basics",
4344
"skills": [
4445
{
@@ -67,7 +68,7 @@ def test__get_course_data_return_value():
6768
},
6869
],
6970
},
70-
{"title": "Phrases", "skills": []},
71+
{"slug": "phrases", "title": "Phrases", "skills": []},
7172
],
7273
}
7374

@@ -89,6 +90,7 @@ def test__get_course_data_return_value_2():
8990
},
9091
"modules": [
9192
{
93+
"slug": "animals",
9294
"title": "Animals",
9395
"skills": [
9496
{
@@ -122,6 +124,7 @@ def test__get_course_data_return_value_with_introduction():
122124
},
123125
"modules": [
124126
{
127+
"slug": "animals",
125128
"title": "Animals",
126129
"skills": [
127130
{

0 commit comments

Comments
 (0)