Update course functionality

feat/ai-courses
Kamran Ahmed 2 months ago
parent fc82401430
commit 7c60b79942
  1. 41
      src/components/GenerateCourse/AICourse.tsx
  2. 36
      src/components/GenerateCourse/AICourseCard.tsx
  3. 4
      src/components/GenerateCourse/AICourseModuleView.tsx
  4. 125
      src/components/GenerateCourse/UserCoursesList.tsx

@ -3,10 +3,7 @@ import { useState } from 'react';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { isLoggedIn } from '../../lib/jwt'; import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup'; import { showLoginPopup } from '../../lib/popup';
import { useQuery } from '@tanstack/react-query'; import { UserCoursesList } from './UserCoursesList';
import { listUserAiCoursesOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { AICourseCard } from './AICourseCard';
export const difficultyLevels = [ export const difficultyLevels = [
'beginner', 'beginner',
@ -21,11 +18,6 @@ export function AICourse(props: AICourseProps) {
const [keyword, setKeyword] = useState(''); const [keyword, setKeyword] = useState('');
const [difficulty, setDifficulty] = useState<DifficultyLevel>('beginner'); const [difficulty, setDifficulty] = useState<DifficultyLevel>('beginner');
const { data: userAiCourses, isLoading: isUserAiCoursesLoading } = useQuery(
listUserAiCoursesOptions(),
queryClient,
);
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && keyword.trim()) { if (e.key === 'Enter' && keyword.trim()) {
onSubmit(); onSubmit();
@ -48,12 +40,12 @@ export function AICourse(props: AICourseProps) {
AI Course Generator AI Course Generator
</h1> </h1>
<p className="mb-6 text-center text-gray-600"> <p className="mb-6 text-center text-gray-600">
Enter a topic below to generate a course on it. Enter a topic below to generate a course on it
</p> </p>
<div className="rounded-lg border border-gray-200 bg-white p-6"> <div className="rounded-lg border border-gray-200 bg-white p-6">
<form <form
className="flex flex-col gap-4" className="flex flex-col gap-5"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
onSubmit(); onSubmit();
@ -62,7 +54,7 @@ export function AICourse(props: AICourseProps) {
<div className="flex flex-col"> <div className="flex flex-col">
<label <label
htmlFor="keyword" htmlFor="keyword"
className="mb-2 text-sm font-medium text-gray-700" className="mb-2.5 text-sm font-medium text-gray-700"
> >
Course Topic Course Topic
</label> </label>
@ -80,14 +72,11 @@ export function AICourse(props: AICourseProps) {
className="w-full rounded-md border border-gray-300 bg-white p-3 pl-10 text-gray-900 focus:outline-none focus:ring-1 focus:ring-gray-500" className="w-full rounded-md border border-gray-300 bg-white p-3 pl-10 text-gray-900 focus:outline-none focus:ring-1 focus:ring-gray-500"
maxLength={50} maxLength={50}
/> />
<span className="absolute bottom-3 right-3 text-xs text-gray-400">
{keyword.length}/50
</span>
</div> </div>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<label className="mb-2 text-sm font-medium text-gray-700"> <label className="mb-2.5 text-sm font-medium text-gray-700">
Difficulty Level Difficulty Level
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
@ -97,7 +86,7 @@ export function AICourse(props: AICourseProps) {
type="button" type="button"
onClick={() => setDifficulty(level)} onClick={() => setDifficulty(level)}
className={cn( className={cn(
'rounded-md border px-4 py-1 text-sm capitalize', 'rounded-md border px-4 py-2 capitalize',
difficulty === level difficulty === level
? 'border-gray-800 bg-gray-800 text-white' ? 'border-gray-800 bg-gray-800 text-white'
: 'border-gray-200 bg-gray-100 text-gray-700 hover:bg-gray-200', : 'border-gray-200 bg-gray-100 text-gray-700 hover:bg-gray-200',
@ -126,23 +115,7 @@ export function AICourse(props: AICourseProps) {
</div> </div>
<div className="mt-8 min-h-[200px]"> <div className="mt-8 min-h-[200px]">
<h2 className="mb-2 text-lg font-semibold">Your Courses</h2> <UserCoursesList />
{!isUserAiCoursesLoading && userAiCourses?.length === 0 && (
<p className="text-gray-600">
You haven't generated any courses yet.
</p>
)}
{!isUserAiCoursesLoading &&
userAiCourses &&
userAiCourses.length > 0 && (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{userAiCourses.map((course) => (
<AICourseCard key={course._id} course={course} />
))}
</div>
)}
</div> </div>
</div> </div>
</section> </section>

@ -1,6 +1,6 @@
import type { AICourseListItem } from '../../queries/ai-course'; import type { AICourseListItem } from '../../queries/ai-course';
import type { DifficultyLevel } from './AICourse'; import type { DifficultyLevel } from './AICourse';
import { BookOpen, Calendar } from 'lucide-react'; import { BookOpen } from 'lucide-react';
type AICourseCardProps = { type AICourseCardProps = {
course: AICourseListItem; course: AICourseListItem;
@ -20,16 +20,10 @@ export function AICourseCard(props: AICourseCardProps) {
// Map difficulty to color // Map difficulty to color
const difficultyColor = const difficultyColor =
{ {
beginner: 'bg-green-100 text-green-700', beginner: 'text-green-700',
intermediate: 'bg-blue-100 text-blue-700', intermediate: 'text-blue-700',
advanced: 'bg-purple-100 text-purple-700', advanced: 'text-purple-700',
}[course.difficulty as DifficultyLevel] || 'bg-gray-100 text-gray-700'; }[course.difficulty as DifficultyLevel] || 'text-gray-700';
// Get a short description preview if available
const descriptionPreview = course.data
? JSON.parse(course.data)?.description?.substring(0, 100) +
(JSON.parse(course.data)?.description?.length > 100 ? '...' : '')
: null;
// Calculate progress percentage // Calculate progress percentage
const totalTopics = course.lessonCount || 0; const totalTopics = course.lessonCount || 0;
@ -40,34 +34,24 @@ export function AICourseCard(props: AICourseCardProps) {
return ( return (
<a <a
href={`/ai-tutor/${course.slug}`} href={`/ai-tutor/${course.slug}`}
className="group relative flex w-full flex-col overflow-hidden rounded-lg border border-gray-300 bg-white p-4 text-left transition-all hover:border-gray-3 00 hover:bg-gray-50" className="hover:border-gray-3 00 group relative flex w-full flex-col overflow-hidden rounded-lg border border-gray-200 bg-white p-4 text-left transition-all hover:bg-gray-50"
> >
<div className="mb-2 flex items-center justify-between"> <div className="flex items-center justify-between">
<span <span
className={`rounded-full px-2.5 py-1 text-xs font-medium capitalize ${difficultyColor}`} className={`rounded-full text-xs font-medium capitalize opacity-80 ${difficultyColor}`}
> >
{course.difficulty} {course.difficulty}
</span> </span>
{formattedDate && (
<span className="flex items-center text-xs text-gray-500">
<Calendar className="mr-1 h-3 w-3" />
{formattedDate}
</span>
)}
</div> </div>
<h3 className="mb-2 text-base font-semibold text-gray-900"> <h3 className="my-2 text-base font-semibold text-gray-900">
{course.title} {course.title}
</h3> </h3>
{descriptionPreview && (
<p className="mb-3 text-xs text-gray-600">{descriptionPreview}</p>
)}
<div className="mt-auto flex items-center justify-between pt-2"> <div className="mt-auto flex items-center justify-between pt-2">
<div className="flex items-center text-xs text-gray-600"> <div className="flex items-center text-xs text-gray-600">
<BookOpen className="mr-1 h-3.5 w-3.5" /> <BookOpen className="mr-1 h-3.5 w-3.5" />
<span>{totalTopics} topics</span> <span>{totalTopics} lessons</span>
</div> </div>
{totalTopics > 0 && ( {totalTopics > 0 && (

@ -293,8 +293,8 @@ export function AICourseModuleView(props: AICourseModuleViewProps) {
)} )}
{!isLoggedIn() && ( {!isLoggedIn() && (
<div className="mt-8 flex flex-col items-center justify-center gap-4 rounded-lg border border-gray-200 p-8"> <div className="mt-8 flex min-h-[152px] flex-col items-center justify-center gap-3 rounded-lg border border-gray-200 p-8">
<LockIcon className="size-10 stroke-[2.5] text-gray-400" /> <LockIcon className="size-7 stroke-[2] text-gray-400/90" />
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Please login to generate course content Please login to generate course content
</p> </p>

@ -0,0 +1,125 @@
import { useQuery } from '@tanstack/react-query';
import { listUserAiCoursesOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { AICourseCard } from './AICourseCard';
import { useEffect, useState } from 'react';
import { Loader2, Search, Lock } from 'lucide-react';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
type UserCoursesListProps = {};
export function UserCoursesList(props: UserCoursesListProps) {
const [searchTerm, setSearchTerm] = useState('');
const [isInitialLoading, setIsInitialLoading] = useState(true);
const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery(
listUserAiCoursesOptions(),
queryClient,
);
useEffect(() => {
setIsInitialLoading(false);
}, [userAiCourses]);
const filteredCourses = userAiCourses?.filter((course) => {
if (!searchTerm.trim()) {
return true;
}
const searchLower = searchTerm.toLowerCase();
return (
course.title.toLowerCase().includes(searchLower) ||
course.keyword.toLowerCase().includes(searchLower)
);
});
const isAuthenticated = isLoggedIn();
return (
<>
<div className="mb-3 flex min-h-[35px] items-center justify-between">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">Your Courses</h2>
</div>
<div className="relative w-64">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<Search className="h-4 w-4 text-gray-400" />
</div>
<input
type="text"
className="block w-full rounded-md border border-gray-200 bg-white py-1.5 pl-10 pr-3 leading-5 placeholder-gray-500 focus:border-gray-300 focus:outline-none focus:ring-blue-500 disabled:opacity-70 sm:text-sm"
placeholder="Search your courses..."
value={searchTerm}
disabled={
isInitialLoading ||
isUserAiCoursesLoading ||
!isAuthenticated ||
userAiCourses?.length === 0
}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
{!isInitialLoading && !isUserAiCoursesLoading && !isAuthenticated && (
<div className="flex min-h-[152px] flex-col items-center justify-center rounded-lg border border-gray-200 bg-white px-6 py-4">
<Lock className="mb-3 size-6 text-gray-300/90" strokeWidth={2.5} />
<p className="max-w-sm text-balance text-center text-gray-500">
<button
onClick={() => {
showLoginPopup();
}}
className="font-medium text-black underline underline-offset-2 hover:opacity-80"
>
Sign up (free and takes 2s) or login
</button>{' '}
to start generating courses.
</p>
</div>
)}
{!isUserAiCoursesLoading &&
!isInitialLoading &&
userAiCourses?.length === 0 && (
<div className="flex min-h-[152px] items-center justify-center rounded-lg border border-gray-200 bg-white py-4">
<p className="text-sm text-gray-600">
You haven't generated any courses yet.
</p>
</div>
)}
{(isUserAiCoursesLoading || isInitialLoading) && (
<div className="flex min-h-[152px] items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white py-4">
<Loader2
className="size-4 animate-spin text-gray-400"
strokeWidth={2.5}
/>
<p className="text-sm font-medium text-gray-600">Loading...</p>
</div>
)}
{!isUserAiCoursesLoading &&
filteredCourses &&
filteredCourses.length > 0 && (
<div className="flex flex-col gap-2">
{filteredCourses.map((course) => (
<AICourseCard key={course._id} course={course} />
))}
</div>
)}
{!isUserAiCoursesLoading &&
(userAiCourses?.length || 0 > 0) &&
filteredCourses?.length === 0 && (
<div className="flex min-h-[114px] items-center justify-center rounded-lg border border-gray-200 bg-white py-4">
<p className="text-sm text-gray-600">
No courses match your search.
</p>
</div>
)}
</>
);
}
Loading…
Cancel
Save