feat/ai-courses
Kamran Ahmed 1 month ago
parent a26714c7f9
commit 2fbde8743e
  1. 20
      src/components/GenerateCourse/AICourse.tsx
  2. 10
      src/components/GenerateCourse/GetAICourse.tsx
  3. 92
      src/components/GenerateCourse/UserCoursesList.tsx

@ -35,15 +35,15 @@ export function AICourse(props: AICourseProps) {
return (
<section className="flex flex-grow flex-col bg-gray-100">
<div className="container mx-auto flex max-w-3xl flex-col max-sm:py-4 py-24">
<h1 className="mb-2 max-sm:mb-2 max-sm:text-left text-center max-sm:text-xl text-3xl font-bold">
AI Course Generator
<div className="container mx-auto flex max-w-3xl flex-col py-24 max-sm:py-4">
<h1 className="mb-2.5 text-center text-4xl font-bold max-sm:mb-2 max-sm:text-left max-sm:text-xl">
Learn anything with AI
</h1>
<p className="mb-6 max-sm:hidden max-sm:text-sm max-sm:text-left text-center text-gray-600">
Enter a topic below to generate a course for it
<p className="mb-6 text-center text-lg text-gray-600 max-sm:hidden max-sm:text-left max-sm:text-sm">
Enter a topic below to generate a personalized course for it
</p>
<div className="rounded-lg border border-gray-200 bg-white max-sm:p-4 p-6">
<div className="rounded-lg border border-gray-200 bg-white p-6 max-sm:p-4">
<form
className="flex flex-col gap-5"
onSubmit={(e) => {
@ -69,7 +69,7 @@ export function AICourse(props: AICourseProps) {
onChange={(e) => setKeyword(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="e.g., Algebra, JavaScript, Photography"
className="w-full max-sm:placeholder:text-base 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 max-sm:placeholder:text-base"
maxLength={50}
/>
</div>
@ -79,14 +79,14 @@ export function AICourse(props: AICourseProps) {
<label className="mb-2.5 text-sm font-medium text-gray-700">
Difficulty Level
</label>
<div className="flex max-sm:flex-col max-sm:gap-1 gap-2">
<div className="flex gap-2 max-sm:flex-col max-sm:gap-1">
{difficultyLevels.map((level) => (
<button
key={level}
type="button"
onClick={() => setDifficulty(level)}
className={cn(
'rounded-md max-sm:text-sm border px-4 py-2 capitalize',
'rounded-md border px-4 py-2 capitalize max-sm:text-sm',
difficulty === level
? 'border-gray-800 bg-gray-800 text-white'
: 'border-gray-200 bg-gray-100 text-gray-700 hover:bg-gray-200',
@ -102,7 +102,7 @@ export function AICourse(props: AICourseProps) {
type="submit"
disabled={!keyword.trim()}
className={cn(
'mt-2 max-sm:text-sm flex items-center justify-center rounded-md px-4 py-2 font-medium text-white transition-colors',
'mt-2 flex items-center justify-center rounded-md px-4 py-2 font-medium text-white transition-colors max-sm:text-sm',
!keyword.trim()
? 'cursor-not-allowed bg-gray-400'
: 'bg-black hover:bg-gray-800',

@ -4,7 +4,7 @@ import { queryClient } from '../../stores/query-client';
import { useEffect, useState } from 'react';
import { AICourseContent } from './AICourseContent';
import { generateAiCourseStructure } from '../../lib/ai';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { isLoggedIn } from '../../lib/jwt';
type GetAICourseProps = {
courseSlug: string;
@ -23,11 +23,17 @@ export function GetAICourse(props: GetAICourseProps) {
course: generateAiCourseStructure(data.data),
};
},
enabled: !!courseSlug,
enabled: !!courseSlug && !!isLoggedIn(),
},
queryClient,
);
useEffect(() => {
if (!isLoggedIn()) {
window.location.href = '/ai-tutor';
}
}, [isLoggedIn]);
useEffect(() => {
if (!aiCourse) {
return;

@ -1,17 +1,36 @@
import { useQuery } from '@tanstack/react-query';
import { listUserAiCoursesOptions } from '../../queries/ai-course';
import {
getAiCourseLimitOptions,
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 { Gift, Loader2, Search, User2 } from 'lucide-react';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { cn } from '../../lib/classname';
import { billingDetailsOptions } from '../../queries/billing';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
type UserCoursesListProps = {};
export function UserCoursesList(props: UserCoursesListProps) {
const [searchTerm, setSearchTerm] = useState('');
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [showUpgradePopup, setShowUpgradePopup] = useState(false);
const { data: limits, isLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const { used, limit } = limits ?? { used: 0, limit: 0 };
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
const isPaidUser = userBillingDetails?.status !== 'none';
const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery(
listUserAiCoursesOptions(),
@ -37,36 +56,67 @@ export function UserCoursesList(props: UserCoursesListProps) {
const isAuthenticated = isLoggedIn();
const canSearch =
!isInitialLoading &&
!isUserAiCoursesLoading &&
isAuthenticated &&
userAiCourses?.length !== 0;
const limitUsedPercentage = Math.round((used / limit) * 100);
return (
<>
<div className="mb-3 max-sm:mb-1 flex min-h-[35px] items-center justify-between">
{showUpgradePopup && (
<UpgradeAccountModal onClose={() => setShowUpgradePopup(false)} />
)}
<div className="mb-3 flex min-h-[35px] items-center justify-between max-sm:mb-1">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">Your Courses</h2>
</div>
<div className="relative max-sm:hidden 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 className="flex items-center gap-2">
<div
className={cn(
'flex items-center gap-2 opacity-0 transition-opacity',
{
'opacity-100': !isPaidUser,
},
)}
>
<p className="flex items-center text-sm text-yellow-600">
{limitUsedPercentage}% of daily limit used{' '}
<button
onClick={() => {
setShowUpgradePopup(true);
}}
className="ml-1.5 flex items-center gap-1 rounded-full bg-yellow-600 py-0.5 pl-1.5 pr-2 text-xs text-white"
>
<Gift className="size-4" />
Upgrade
</button>
</p>
</div>
<div className={cn('relative w-64 max-sm:hidden', {
})}>
<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 transition-all focus:border-gray-300 focus:outline-none focus:ring-blue-500 disabled:opacity-70 sm:text-sm"
placeholder="Search your courses..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</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} />
<User2 className="mb-2 size-8 text-gray-300" />
<p className="max-w-sm text-balance text-center text-gray-500">
<button
onClick={() => {
@ -76,7 +126,7 @@ export function UserCoursesList(props: UserCoursesListProps) {
>
Sign up (free and takes 2s) or login
</button>{' '}
to start generating courses.
to generate and save courses.
</p>
</div>
)}

Loading…
Cancel
Save