wip: course rating

feat/course
Arik Chakma 4 weeks ago
parent f7130b42da
commit 28e7b1e9d2
  1. 27
      src/components/Course/CertificateView.tsx
  2. 52
      src/components/Course/RateCourseForm.tsx
  3. 12
      src/hooks/use-course.ts
  4. 63
      src/pages/learn/[courseId]/index.json.ts

@ -6,6 +6,7 @@ import { useCourseProgress } from '../../hooks/use-course';
import { Loader2 } from 'lucide-react';
import { CertificateModal } from './Certificate';
import { useAuth } from '../../hooks/use-auth';
import { DateTime } from 'luxon';
type CertificateViewProps = {
courseTitle: string;
@ -46,10 +47,11 @@ export function CertificateView(props: CertificateViewProps) {
}, [chapters]);
const isCourseCompleted = useMemo(() => {
return allLessonLinks.every((lessonLink) =>
completeLessonSet.has(lessonLink),
return (
allLessonLinks.every((lessonLink) => completeLessonSet.has(lessonLink)) &&
courseProgress?.completedAt !== null
);
}, [allLessonLinks, completeLessonSet]);
}, [allLessonLinks, completeLessonSet, courseProgress]);
const {
chapters: chaptersCount,
@ -99,19 +101,29 @@ export function CertificateView(props: CertificateViewProps) {
setIsLoading(false);
}, [courseProgress]);
useEffect(() => {
if (!courseProgress) {
return;
}
setRating(courseProgress?.review?.rating || 0);
}, [courseProgress]);
return (
<>
{showRatingForm && (
<RateCourseForm
defaultRating={rating}
courseId={currentCourseId}
rating={rating}
feedback={courseProgress?.review?.feedback || ''}
onClose={() => {
setRating(0);
setRating(courseProgress?.review?.rating || 0);
setShowRatingForm(false);
}}
/>
)}
{showCertificateModal && (
{showCertificateModal && courseProgress?.completedAt !== null && (
<CertificateModal
userName={user?.name || 'N/A'}
courseName={courseTitle}
@ -121,8 +133,7 @@ export function CertificateView(props: CertificateViewProps) {
onClose={() => {
setShowCertificateModal(false);
}}
// FIXME: This should be the actual date of course completion
issuedDate={new Date().toISOString()}
issuedDate={DateTime.now().toFormat('yyyy-MM-dd')}
/>
)}

@ -2,17 +2,51 @@ import { useState } from 'react';
import { Modal } from '../Modal';
import { Rating } from '../Rating/Rating';
import { cn } from '../../lib/classname';
import { useMutation } from '@tanstack/react-query';
import { queryClient } from '../../stores/query-client';
import { httpPost } from '../../lib/query-http';
import { useToast } from '../../hooks/use-toast';
import { Loader2 } from 'lucide-react';
type RateCourseFormProps = {
defaultRating?: number;
courseId: string;
rating?: number;
feedback?: string;
onClose: () => void;
};
export function RateCourseForm(props: RateCourseFormProps) {
const { onClose, defaultRating = 0 } = props;
const {
courseId,
onClose,
rating: defaultRating = 0,
feedback: defaultFeedback,
} = props;
const toast = useToast();
const [userRating, setUserRating] = useState(defaultRating);
const [userFeedback, setUserFeedback] = useState('');
const [userFeedback, setUserFeedback] = useState(defaultFeedback ?? '');
const submitReview = useMutation(
{
mutationFn: async (data: { rating: number; feedback?: string }) => {
return httpPost(`/v1-submit-course-review/${courseId}`, data);
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ['course-progress', courseId],
});
},
onError: (error) => {
toast.error(error?.message || 'Something went wrong');
},
onSuccess: () => {
toast.success('Review submitted successfully');
onClose();
},
},
queryClient,
);
return (
<Modal onClose={onClose} bodyClassName="bg-zinc-800 p-5 rounded-lg">
@ -23,6 +57,10 @@ export function RateCourseForm(props: RateCourseFormProps) {
className="mt-4"
onSubmit={(e) => {
e.preventDefault();
submitReview.mutate({
rating: userRating,
feedback: userFeedback,
});
}}
>
<Rating
@ -43,7 +81,7 @@ export function RateCourseForm(props: RateCourseFormProps) {
<textarea
id="rating-feedback"
className="min-h-24 rounded-md border border-zinc-700 bg-transparent p-2 text-sm outline-none focus:border-zinc-500"
placeholder="Share your thoughts with the roadmap creator"
placeholder="Share your thoughts with us"
value={userFeedback}
onChange={(e) => {
setUserFeedback(e.target.value);
@ -60,9 +98,13 @@ export function RateCourseForm(props: RateCourseFormProps) {
Cancel
</button>
<button
className="flex h-10 w-full items-center justify-center rounded-full bg-white p-2.5 text-sm font-medium text-black hover:bg-zinc-100 disabled:opacity-60"
className="flex h-10 w-full items-center justify-center gap-2 rounded-full bg-white p-2.5 text-sm font-medium text-black hover:bg-zinc-100 disabled:opacity-60"
type="submit"
disabled={submitReview.isPending}
>
{submitReview.isPending && (
<Loader2 className="size-4 animate-spin stroke-[2.5] text-zinc-700" />
)}
Submit Rating
</button>
</div>

@ -12,13 +12,19 @@ export interface CourseProgressDocument {
lessonId: string;
completedAt: Date;
}[];
review?: {
rating: number;
feedback?: string;
};
completedAt?: Date;
createdAt: Date;
updatedAt: Date;
}
export type CourseProgressResponse = {
completed: CourseProgressDocument['completed'];
};
export type CourseProgressResponse = Pick<
CourseProgressDocument,
'completed' | 'completedAt' | 'review'
>;
export function useCourseProgress(courseId: string) {
return useQuery(

@ -0,0 +1,63 @@
import type { APIRoute } from 'astro';
import { getAllCourses, getChaptersByCourseId } from '../../../lib/course';
export async function getStaticPaths() {
const courses = await getAllCourses();
const coursesWithChapters = await Promise.all(
courses.map(async (course) => {
const filteredCourse = course.frontmatter;
const chapters = await getChaptersByCourseId(course.id);
const enrichedChapters = chapters
.filter((chapter) => chapter?.lessons?.length > 0)
.map((chapter) => {
return {
...chapter.frontmatter,
id: chapter.id,
lessons:
chapter?.lessons?.map((lesson) => {
return {
...lesson.frontmatter,
id: lesson.id,
};
}) ?? [],
};
});
const lessonsCount = enrichedChapters.reduce(
(acc, chapter) => acc + chapter?.lessons?.length,
0,
);
const chaptersCount = enrichedChapters.length;
return {
id: course.id,
...filteredCourse,
lessonsCount,
chaptersCount,
// FIXME: let's discuss if we need to include the chapters here
// or if we should just include the count
// chapters: enrichedChapters,
};
}),
);
return coursesWithChapters.map((course) => {
return {
params: {
courseId: course.id,
},
props: {
course,
},
};
});
}
export const GET: APIRoute = async function ({ params, request, props }) {
return new Response(JSON.stringify(props.course), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
};
Loading…
Cancel
Save