diff --git a/src/api/ai-roadmap.ts b/src/api/ai-roadmap.ts index 944b120ed..9a4227f71 100644 --- a/src/api/ai-roadmap.ts +++ b/src/api/ai-roadmap.ts @@ -18,3 +18,16 @@ export function aiRoadmapApi(context: APIContext) { }, }; } + +export interface AICourseDocument { + _id: string; + userId: string; + title: string; + slug?: string; + keyword: string; + difficulty: string; + data: string; + viewCount: number; + createdAt: Date; + updatedAt: Date; +} \ No newline at end of file diff --git a/src/components/GenerateCourse/AICourseContent.tsx b/src/components/GenerateCourse/AICourseContent.tsx index 2400b54b5..9a7417ed7 100644 --- a/src/components/GenerateCourse/AICourseContent.tsx +++ b/src/components/GenerateCourse/AICourseContent.tsx @@ -1,4 +1,11 @@ -import { ArrowLeft, BookOpenCheck, Loader2, Menu, X } from 'lucide-react'; +import { + ArrowLeft, + BookOpenCheck, + CheckCircleIcon, + Loader2, + Menu, + X, +} from 'lucide-react'; import { useState } from 'react'; import { cn } from '../../lib/classname'; import { ErrorIcon } from '../ReactIcons/ErrorIcon'; @@ -8,6 +15,8 @@ import { useQuery } from '@tanstack/react-query'; import { queryClient } from '../../stores/query-client'; import { AICourseModuleList } from './AICourseModuleList'; import { AICourseModuleView } from './AICourseModuleView'; +import { slugify } from '../../lib/slugger'; +import { CheckIcon } from '../ReactIcons/CheckIcon'; type Lesson = string; @@ -26,28 +35,26 @@ type AICourseContentProps = { courseSlug?: string; course: AiCourse; isLoading: boolean; + error?: string; }; export function AICourseContent(props: AICourseContentProps) { - const { course, courseSlug, isLoading } = props; - - const [courseId, setCourseId] = useState(''); - const [error, setError] = useState(null); + const { course, courseSlug, isLoading, error } = props; const [activeModuleIndex, setActiveModuleIndex] = useState(0); const [activeLessonIndex, setActiveLessonIndex] = useState(0); const [sidebarOpen, setSidebarOpen] = useState(true); const [viewMode, setViewMode] = useState<'module' | 'full'>('full'); - const [expandedModules, setExpandedModules] = useState< - Record - >({}); - const { data: aiCourseProgress } = useQuery( getAiCourseProgressOptions({ aiCourseSlug: courseSlug || '' }), queryClient, ); + const [expandedModules, setExpandedModules] = useState< + Record + >({}); + // Navigation helpers const goToNextModule = () => { if (activeModuleIndex < course.modules.length - 1) { @@ -192,6 +199,7 @@ export function AICourseContent(props: AICourseContentProps) { {course.title ? (
- {course.modules.map((module, moduleIdx) => ( -
-

- {module.title} -

-
- {module.lessons.map((lesson, lessonIdx) => ( -
{ - setActiveModuleIndex(moduleIdx); - setActiveLessonIndex(lessonIdx); - // Expand only this module in the sidebar - setExpandedModules((prev) => { - const newState: Record = {}; - // Set all modules to collapsed - course.modules.forEach((_, idx) => { - newState[idx] = false; - }); - // Expand only the current module - newState[moduleIdx] = true; - return newState; - }); - // Ensure sidebar is visible on mobile - setSidebarOpen(true); - setViewMode('module'); - }} - > - - {lessonIdx + 1} - -

- {lesson} -

- - View → - -
- ))} + {course.modules.map((module, moduleIdx) => { + return ( +
+

+ {module.title} +

+
+ {module.lessons.map((lesson, lessonIdx) => { + const key = `${slugify(module.title)}__${slugify(lesson)}`; + const isCompleted = + aiCourseProgress?.done.includes(key); + + return ( +
{ + setActiveModuleIndex(moduleIdx); + setActiveLessonIndex(lessonIdx); + // Expand only this module in the sidebar + setExpandedModules((prev) => { + const newState: Record = + {}; + // Set all modules to collapsed + course.modules.forEach((_, idx) => { + newState[idx] = false; + }); + // Expand only the current module + newState[moduleIdx] = true; + return newState; + }); + // Ensure sidebar is visible on mobile + setSidebarOpen(true); + setViewMode('module'); + }} + > + {!isCompleted && ( + + {lessonIdx + 1} + + )} + + {isCompleted && ( + + )} + +

+ {lesson} +

+ + View → + +
+ ); + })} +
-
- ))} + ); + })}
) : (
diff --git a/src/components/GenerateCourse/AICourseModuleList.tsx b/src/components/GenerateCourse/AICourseModuleList.tsx index 61cd2a9d9..3ed31bb01 100644 --- a/src/components/GenerateCourse/AICourseModuleList.tsx +++ b/src/components/GenerateCourse/AICourseModuleList.tsx @@ -1,10 +1,20 @@ import { type Dispatch, type SetStateAction, useState } from 'react'; import type { AiCourse } from '../../lib/ai'; -import { ChevronDownIcon, ChevronRightIcon } from 'lucide-react'; +import { + CheckCircleIcon, + ChevronDownIcon, + ChevronRightIcon, +} from 'lucide-react'; import { cn } from '../../lib/classname'; +import { getAiCourseProgressOptions } from '../../queries/ai-course'; +import { useQuery } from '@tanstack/react-query'; +import { queryClient } from '../../stores/query-client'; +import { slugify } from '../../lib/slugger'; +import { CheckIcon } from '../ReactIcons/CheckIcon'; type AICourseModuleListProps = { course: AiCourse; + courseSlug?: string; activeModuleIndex: number; setActiveModuleIndex: (index: number) => void; activeLessonIndex: number; @@ -22,6 +32,7 @@ type AICourseModuleListProps = { export function AICourseModuleList(props: AICourseModuleListProps) { const { course, + courseSlug, activeModuleIndex, setActiveModuleIndex, activeLessonIndex, @@ -32,6 +43,11 @@ export function AICourseModuleList(props: AICourseModuleListProps) { setExpandedModules, } = props; + const { data: aiCourseProgress } = useQuery( + getAiCourseProgressOptions({ aiCourseSlug: courseSlug || '' }), + queryClient, + ); + const toggleModule = (index: number) => { setExpandedModules((prev) => { // If this module is already expanded, collapse it @@ -54,6 +70,8 @@ export function AICourseModuleList(props: AICourseModuleListProps) { }); }; + const { done = [] } = aiCourseProgress || {}; + return (
diff --git a/src/components/GenerateCourse/AICourseModuleView.tsx b/src/components/GenerateCourse/AICourseModuleView.tsx index 6b018e8e6..fb7730250 100644 --- a/src/components/GenerateCourse/AICourseModuleView.tsx +++ b/src/components/GenerateCourse/AICourseModuleView.tsx @@ -4,10 +4,11 @@ import { useEffect, useState } from 'react'; import { isLoggedIn, removeAuthToken } from '../../lib/jwt'; import { readAICourseLessonStream } from '../../helper/read-stream'; import { markdownToHtml } from '../../lib/markdown'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { queryClient } from '../../stores/query-client'; import { httpPost } from '../../lib/query-http'; import { slugify } from '../../lib/slugger'; +import { getAiCourseProgressOptions } from '../../queries/ai-course'; type AICourseModuleViewProps = { courseSlug: string; @@ -43,6 +44,13 @@ export function AICourseModuleView(props: AICourseModuleViewProps) { const [error, setError] = useState(''); const [lessonHtml, setLessonHtml] = useState(''); + const { data: aiCourseProgress } = useQuery( + getAiCourseProgressOptions({ aiCourseSlug: courseSlug || '' }), + queryClient, + ); + + const lessonId = `${slugify(currentModuleTitle)}__${slugify(currentLessonTitle)}`; + const isLessonDone = aiCourseProgress?.done.includes(lessonId); const generateAiCourseContent = async () => { setIsLoading(true); @@ -114,13 +122,9 @@ export function AICourseModuleView(props: AICourseModuleViewProps) { const { mutate: markAsDone, isPending: isMarkingAsDone } = useMutation( { mutationFn: () => { - const lessonId = `${slugify(currentModuleTitle)}__${slugify(currentLessonTitle)}`; - return httpPost( - `${import.meta.env.PUBLIC_API_URL}/v1-mark-as-done-ai-lesson/${courseSlug}`, - { - lessonId: lessonId, - }, - ); + return httpPost(`/v1-mark-as-done-ai-lesson/${courseSlug}`, { + lessonId, + }); }, }, queryClient, @@ -162,7 +166,7 @@ export function AICourseModuleView(props: AICourseModuleViewProps) {
)} - {!isGenerating && !isLoading && ( + {!isGenerating && !isLoading && !isLessonDone && (