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 { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { useQuery } from '@tanstack/react-query';
import { listUserAiCoursesOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { AICourseCard } from './AICourseCard';
import { UserCoursesList } from './UserCoursesList';
export const difficultyLevels = [
'beginner',
@ -21,11 +18,6 @@ export function AICourse(props: AICourseProps) {
const [keyword, setKeyword] = useState('');
const [difficulty, setDifficulty] = useState<DifficultyLevel>('beginner');
const { data: userAiCourses, isLoading: isUserAiCoursesLoading } = useQuery(
listUserAiCoursesOptions(),
queryClient,
);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && keyword.trim()) {
onSubmit();
@ -48,12 +40,12 @@ export function AICourse(props: AICourseProps) {
AI Course Generator
</h1>
<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>
<div className="rounded-lg border border-gray-200 bg-white p-6">
<form
className="flex flex-col gap-4"
className="flex flex-col gap-5"
onSubmit={(e) => {
e.preventDefault();
onSubmit();
@ -62,7 +54,7 @@ export function AICourse(props: AICourseProps) {
<div className="flex flex-col">
<label
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
</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"
maxLength={50}
/>
<span className="absolute bottom-3 right-3 text-xs text-gray-400">
{keyword.length}/50
</span>
</div>
</div>
<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
</label>
<div className="flex gap-2">
@ -97,7 +86,7 @@ export function AICourse(props: AICourseProps) {
type="button"
onClick={() => setDifficulty(level)}
className={cn(
'rounded-md border px-4 py-1 text-sm capitalize',
'rounded-md border px-4 py-2 capitalize',
difficulty === level
? 'border-gray-800 bg-gray-800 text-white'
: 'border-gray-200 bg-gray-100 text-gray-700 hover:bg-gray-200',
@ -126,23 +115,7 @@ export function AICourse(props: AICourseProps) {
</div>
<div className="mt-8 min-h-[200px]">
<h2 className="mb-2 text-lg font-semibold">Your Courses</h2>
{!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>
)}
<UserCoursesList />
</div>
</div>
</section>

@ -1,6 +1,6 @@
import type { AICourseListItem } from '../../queries/ai-course';
import type { DifficultyLevel } from './AICourse';
import { BookOpen, Calendar } from 'lucide-react';
import { BookOpen } from 'lucide-react';
type AICourseCardProps = {
course: AICourseListItem;
@ -20,16 +20,10 @@ export function AICourseCard(props: AICourseCardProps) {
// Map difficulty to color
const difficultyColor =
{
beginner: 'bg-green-100 text-green-700',
intermediate: 'bg-blue-100 text-blue-700',
advanced: 'bg-purple-100 text-purple-700',
}[course.difficulty as DifficultyLevel] || 'bg-gray-100 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;
beginner: 'text-green-700',
intermediate: 'text-blue-700',
advanced: 'text-purple-700',
}[course.difficulty as DifficultyLevel] || 'text-gray-700';
// Calculate progress percentage
const totalTopics = course.lessonCount || 0;
@ -40,34 +34,24 @@ export function AICourseCard(props: AICourseCardProps) {
return (
<a
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
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}
</span>
{formattedDate && (
<span className="flex items-center text-xs text-gray-500">
<Calendar className="mr-1 h-3 w-3" />
{formattedDate}
</span>
)}
</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}
</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="flex items-center text-xs text-gray-600">
<BookOpen className="mr-1 h-3.5 w-3.5" />
<span>{totalTopics} topics</span>
<span>{totalTopics} lessons</span>
</div>
{totalTopics > 0 && (

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