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_API_URL=https://api.roadmap.sh |
||||||
PUBLIC_AVATAR_BASE_URL=https://dodrc8eu8m09s.cloudfront.net/avatars |
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 { CourseLanding } from '../../../components/CourseLanding/CourseLanding'; |
||||||
import BaseLayout from '../../../layouts/BaseLayout.astro'; |
import BaseLayout from '../../../layouts/BaseLayout.astro'; |
||||||
import { |
|
||||||
getAllCourses, |
|
||||||
getCourseById, |
|
||||||
type CourseFileType, |
|
||||||
} from '../../../lib/course'; |
|
||||||
|
|
||||||
export async function getStaticPaths() { |
export const prerender = false; |
||||||
const courses = await getAllCourses(); |
|
||||||
|
|
||||||
return courses.map((course) => ({ |
interface Params { |
||||||
params: { courseId: course.id }, |
|
||||||
props: { course }, |
|
||||||
})); |
|
||||||
} |
|
||||||
|
|
||||||
interface Params extends Record<string, string | undefined> { |
|
||||||
courseId: string; |
courseId: string; |
||||||
} |
} |
||||||
|
|
||||||
interface Props { |
|
||||||
course: CourseFileType; |
|
||||||
} |
|
||||||
|
|
||||||
const { courseId } = Astro.params; |
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}> |
<BaseLayout title={course?.title || 'Course'} description={course?.description}> |
||||||
<CourseLanding client:load /> |
{course && <CourseLanding course={course} client:load />} |
||||||
<div slot='page-footer'></div> |
|
||||||
</BaseLayout> |
</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