parent
38860d35e5
commit
a366abaf44
8 changed files with 533 additions and 243 deletions
@ -1,3 +1,4 @@ |
||||
PUBLIC_API_URL=https://api.roadmap.sh |
||||
PUBLIC_AVATAR_BASE_URL=https://dodrc8eu8m09s.cloudfront.net/avatars |
||||
PUBLIC_EDITOR_APP_URL=https://draw.roadmap.sh |
||||
PUBLIC_EDITOR_APP_URL=https://draw.roadmap.sh |
||||
PUBLIC_COURSE_APP_URL=http://localhost:5173 |
@ -0,0 +1,164 @@ |
||||
import { type APIContext } from 'astro'; |
||||
import { api } from './api.ts'; |
||||
|
||||
export const allowedCourseDifficulties = [ |
||||
'beginner', |
||||
'intermediate', |
||||
'advanced', |
||||
] as const; |
||||
export type AllowedCourseDifficulty = |
||||
(typeof allowedCourseDifficulties)[number]; |
||||
|
||||
export interface CourseDocument { |
||||
_id: string; |
||||
|
||||
title: string; |
||||
slug: string; |
||||
description?: string; |
||||
difficulty?: AllowedCourseDifficulty; |
||||
|
||||
briefTitle?: string; |
||||
briefDescription?: string; |
||||
|
||||
creatorId: string; |
||||
|
||||
willLearn?: string[]; |
||||
prerequisites?: string[]; |
||||
|
||||
// AI Configurations
|
||||
setting: { |
||||
prompt?: string; |
||||
}; |
||||
|
||||
createdAt: Date; |
||||
updatedAt: Date; |
||||
} |
||||
|
||||
export interface CourseChapterDocument { |
||||
_id: string; |
||||
|
||||
courseId: string; |
||||
creatorId: string; |
||||
|
||||
title: string; |
||||
slug: string; |
||||
|
||||
// AI Configurations
|
||||
setting: { |
||||
prompt?: string; |
||||
}; |
||||
|
||||
sort: number; |
||||
|
||||
createdAt: Date; |
||||
updatedAt: Date; |
||||
} |
||||
|
||||
export const allowedLessonType = ['lesson', 'quiz', 'challenge'] as const; |
||||
export type AllowedLessonType = (typeof allowedLessonType)[number]; |
||||
|
||||
export const allowedSQLChallengeType = [ |
||||
'DDL', |
||||
'DML', |
||||
'DQL', |
||||
'DCL', |
||||
'TCL', |
||||
] as const; |
||||
export type AllowedSQLChallengeType = (typeof allowedSQLChallengeType)[number]; |
||||
|
||||
export type SQLChallenge = { |
||||
editor: 'sql'; |
||||
type: AllowedSQLChallengeType; |
||||
setupQuery: string; |
||||
defaultQuery?: string; |
||||
expectedQuery?: string; |
||||
}; |
||||
|
||||
export type PythonChallenge = { |
||||
editor: 'python'; |
||||
setupCode: string; |
||||
defaultCode: string; |
||||
expectedCode: string; |
||||
}; |
||||
|
||||
export type LessonChallenge = SQLChallenge | PythonChallenge; |
||||
|
||||
export type LessonQuestionOption = { |
||||
id: string; |
||||
text: string; |
||||
isCorrect: boolean; |
||||
}; |
||||
|
||||
export type LessonQuestion = { |
||||
id: string; |
||||
question: string; |
||||
options: LessonQuestionOption[]; |
||||
}; |
||||
|
||||
export interface CourseLessonDocument { |
||||
_id: string; |
||||
|
||||
courseId: string; |
||||
chapterId: string; |
||||
creatorId: string; |
||||
|
||||
title: string; |
||||
slug: string; |
||||
type: AllowedLessonType; |
||||
|
||||
// lesson
|
||||
content?: string; |
||||
|
||||
quiz?: { |
||||
questions: LessonQuestion[]; |
||||
}; |
||||
|
||||
challenge?: LessonChallenge & { |
||||
shouldVerifyResult?: boolean; |
||||
}; |
||||
|
||||
setting: { |
||||
prompt?: string; |
||||
}; |
||||
|
||||
isLocked: boolean; |
||||
sort: number; |
||||
|
||||
createdAt: Date; |
||||
updatedAt: Date; |
||||
} |
||||
|
||||
export type CourseDetailsResponse = Omit<CourseDocument, 'setting'> & { |
||||
chapters: (Pick< |
||||
CourseChapterDocument, |
||||
'_id' | 'title' | 'slug' | 'sort' | 'courseId' |
||||
> & { |
||||
lessons: Pick< |
||||
CourseLessonDocument, |
||||
| '_id' |
||||
| 'title' |
||||
| 'slug' |
||||
| 'type' |
||||
| 'sort' |
||||
| 'chapterId' |
||||
| 'courseId' |
||||
| 'isLocked' |
||||
>[]; |
||||
})[]; |
||||
|
||||
enrolled: number; |
||||
rating: { |
||||
average: number; |
||||
count: number; |
||||
}; |
||||
}; |
||||
|
||||
export function courseApi(context: APIContext) { |
||||
return { |
||||
getCourse: (courseSlug: string) => { |
||||
return api(context).get<CourseDetailsResponse>( |
||||
`${import.meta.env.PUBLIC_API_URL}/v1-course-details/${courseSlug}`, |
||||
); |
||||
}, |
||||
}; |
||||
} |
@ -1,34 +1,19 @@ |
||||
--- |
||||
import { courseApi } from '../../../api/course'; |
||||
import { CourseLanding } from '../../../components/CourseLanding/CourseLanding'; |
||||
import BaseLayout from '../../../layouts/BaseLayout.astro'; |
||||
import { |
||||
getAllCourses, |
||||
getCourseById, |
||||
type CourseFileType, |
||||
} from '../../../lib/course'; |
||||
|
||||
export async function getStaticPaths() { |
||||
const courses = await getAllCourses(); |
||||
export const prerender = false; |
||||
|
||||
return courses.map((course) => ({ |
||||
params: { courseId: course.id }, |
||||
props: { course }, |
||||
})); |
||||
} |
||||
|
||||
interface Params extends Record<string, string | undefined> { |
||||
interface Params { |
||||
courseId: string; |
||||
} |
||||
|
||||
interface Props { |
||||
course: CourseFileType; |
||||
} |
||||
|
||||
const { courseId } = Astro.params; |
||||
const { course } = Astro.props; |
||||
const courseClient = courseApi(Astro); |
||||
const { response: course, error } = await courseClient.getCourse(courseId!); |
||||
--- |
||||
|
||||
<BaseLayout title={course.frontmatter.title}> |
||||
<CourseLanding client:load /> |
||||
<div slot='page-footer'></div> |
||||
<BaseLayout title={course?.title || 'Course'} description={course?.description}> |
||||
{course && <CourseLanding course={course} client:load />} |
||||
</BaseLayout> |
||||
|
@ -0,0 +1,40 @@ |
||||
import { queryOptions } from '@tanstack/react-query'; |
||||
import { isLoggedIn } from '../lib/jwt'; |
||||
import { httpGet } from '../lib/query-http'; |
||||
|
||||
export interface CourseProgressDocument { |
||||
_id: string; |
||||
userId: string; |
||||
courseId: string; |
||||
completed: { |
||||
chapterId: string; |
||||
lessonId: string; |
||||
completedAt: Date; |
||||
}[]; |
||||
review?: { |
||||
rating: number; |
||||
feedback?: string; |
||||
}; |
||||
|
||||
startedAt?: Date; |
||||
completedAt?: Date; |
||||
createdAt: Date; |
||||
updatedAt: Date; |
||||
} |
||||
|
||||
export type CourseProgressResponse = Pick< |
||||
CourseProgressDocument, |
||||
'completed' | 'completedAt' | 'review' | 'startedAt' |
||||
>; |
||||
|
||||
export function courseProgressOptions(courseSlug: string) { |
||||
return queryOptions({ |
||||
queryKey: ['course-progress', courseSlug], |
||||
queryFn: async () => { |
||||
return httpGet<CourseProgressResponse>( |
||||
`/v1-course-progress/${courseSlug}`, |
||||
); |
||||
}, |
||||
enabled: !!isLoggedIn(), |
||||
}); |
||||
} |
Loading…
Reference in new issue