AI landing page changes

feat/ai-tutor-redesign
Kamran Ahmed 4 days ago
parent d1208047a5
commit d0f8fc4e6a
  1. 27
      src/components/AITutor/AILoadingState.tsx
  2. 4
      src/components/AITutor/AITutorLayout.tsx
  3. 31
      src/components/AITutor/AITutorTallMessage.tsx
  4. 175
      src/components/GenerateCourse/AICourse.tsx
  5. 12
      src/components/GenerateCourse/AICourseCard.tsx
  6. 86
      src/components/GenerateCourse/UserCoursesList.tsx
  7. 6
      src/pages/ai/courses.astro

@ -0,0 +1,27 @@
import { Loader2 } from 'lucide-react';
type AILoadingStateProps = {
title: string;
subtitle?: string;
};
export function AILoadingState(props: AILoadingStateProps) {
const { title, subtitle } = props;
return (
<div className="flex min-h-full w-full flex-col items-center justify-center gap-4 rounded-lg border border-gray-200 bg-white p-8">
<div className="relative">
<Loader2 className="size-12 animate-spin text-gray-300" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="size-4 rounded-full bg-white"></div>
</div>
</div>
<div className="text-center">
<p className="text-lg font-medium text-gray-900">{title}</p>
{subtitle && (
<p className="mt-1 text-sm text-gray-500">{subtitle}</p>
)}
</div>
</div>
);
}

@ -11,7 +11,9 @@ export function AITutorLayout(props: AITutorLayoutProps) {
return (
<div className="flex flex-grow flex-row">
<AITutorSidebar activeTab={activeTab} />
<div className="flex flex-grow flex-col">{children}</div>
<div className="flex flex-grow flex-col bg-gray-100 px-4 py-4">
{children}
</div>
</div>
);
}

@ -0,0 +1,31 @@
import { type LucideIcon } from 'lucide-react';
type AITutorTallMessageProps = {
title: string;
subtitle?: string;
icon: LucideIcon;
buttonText?: string;
onButtonClick?: () => void;
};
export function AITutorTallMessage(props: AITutorTallMessageProps) {
const { title, subtitle, icon: Icon, buttonText, onButtonClick } = props;
return (
<div className="flex min-h-full flex-grow flex-col items-center justify-center rounded-lg">
<Icon className="size-12 text-gray-300" />
<div className="my-4 text-center">
<h2 className="mb-2 text-xl font-semibold">{title}</h2>
{subtitle && <p className="text-base text-gray-600">{subtitle}</p>}
</div>
{buttonText && onButtonClick && (
<button
onClick={onButtonClick}
className="rounded-lg bg-black px-4 py-2 text-sm text-white hover:opacity-80"
>
{buttonText}
</button>
)}
</div>
);
}

@ -3,7 +3,6 @@ import { useEffect, useState } from 'react';
import { cn } from '../../lib/classname';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { UserCoursesList } from './UserCoursesList';
import { FineTuneCourse } from './FineTuneCourse';
import {
clearFineTuneData,
@ -72,97 +71,95 @@ export function AICourse(props: AICourseProps) {
}
return (
<section className="flex grow flex-col bg-gray-100">
<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 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 p-6 max-sm:p-4">
<form
className="flex flex-col gap-5"
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}
>
<div className="flex flex-col">
<label
htmlFor="keyword"
className="mb-2.5 text-sm font-medium text-gray-700"
>
Course Topic
</label>
<div className="relative">
<div className="absolute top-1/2 left-3 -translate-y-1/2 text-gray-400">
<SearchIcon size={18} />
</div>
<input
id="keyword"
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="e.g., Algebra, JavaScript, Photography"
className="w-full rounded-md border border-gray-300 bg-white p-3 pl-10 text-gray-900 focus:ring-1 focus:ring-gray-500 focus:outline-hidden max-sm:placeholder:text-base"
maxLength={50}
/>
<div className="flex w-full max-w-3xl mx-auto flex-grow flex-col justify-center">
<h1 className="mb-2.5 text-center text-4xl font-bold max-sm:mb-2 max-sm:text-left max-sm:text-xl">
What can I help you learn?
</h1>
<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 p-6 max-sm:p-4">
<form
className="flex flex-col gap-5"
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}
>
<div className="flex flex-col">
<label
htmlFor="keyword"
className="mb-2.5 text-sm font-medium text-gray-700"
>
Course Topic
</label>
<div className="relative">
<div className="absolute top-1/2 left-3 -translate-y-1/2 text-gray-400">
<SearchIcon size={18} />
</div>
<input
id="keyword"
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="e.g., Algebra, JavaScript, Photography"
className="w-full rounded-md border border-gray-300 bg-white p-3 pl-10 text-gray-900 focus:ring-1 focus:ring-gray-500 focus:outline-hidden max-sm:placeholder:text-base"
maxLength={50}
/>
</div>
<div className="flex flex-col">
<label className="mb-2.5 text-sm font-medium text-gray-700">
Difficulty Level
</label>
<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 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',
)}
>
{level}
</button>
))}
</div>
</div>
<div className="flex flex-col">
<label className="mb-2.5 text-sm font-medium text-gray-700">
Difficulty Level
</label>
<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 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',
)}
>
{level}
</button>
))}
</div>
<FineTuneCourse
hasFineTuneData={hasFineTuneData}
setHasFineTuneData={setHasFineTuneData}
about={about}
goal={goal}
customInstructions={customInstructions}
setAbout={setAbout}
setGoal={setGoal}
setCustomInstructions={setCustomInstructions}
/>
<button
type="submit"
disabled={!keyword.trim()}
className={cn(
'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',
)}
>
<WandIcon size={18} className="mr-2" />
Generate Course
</button>
</form>
</div>
</div>
<FineTuneCourse
hasFineTuneData={hasFineTuneData}
setHasFineTuneData={setHasFineTuneData}
about={about}
goal={goal}
customInstructions={customInstructions}
setAbout={setAbout}
setGoal={setGoal}
setCustomInstructions={setCustomInstructions}
/>
<button
type="submit"
disabled={!keyword.trim()}
className={cn(
'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',
)}
>
<WandIcon size={18} className="mr-2" />
Generate Course
</button>
</form>
</div>
</section>
</div>
);
}

@ -12,14 +12,6 @@ type AICourseCardProps = {
export function AICourseCard(props: AICourseCardProps) {
const { course, showActions = true, showProgress = true } = props;
// Format date if available
const formattedDate = course.createdAt
? new Date(course.createdAt).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})
: null;
// Map difficulty to color
const difficultyColor =
{
@ -35,10 +27,10 @@ export function AICourseCard(props: AICourseCardProps) {
totalTopics > 0 ? Math.round((completedTopics / totalTopics) * 100) : 0;
return (
<div className="relative">
<div className="relative flex flex-grow flex-col">
<a
href={`/ai/${course.slug}`}
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 min-h-full "
className="hover:border-gray-3 00 group relative flex h-full min-h-[140px] 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="flex items-center justify-between">
<span

@ -7,7 +7,7 @@ import {
import { queryClient } from '../../stores/query-client';
import { AICourseCard } from './AICourseCard';
import { useEffect, useState } from 'react';
import { Gift, Loader2, User2 } from 'lucide-react';
import { BookOpen, Gift } from 'lucide-react';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { cn } from '../../lib/classname';
@ -16,6 +16,8 @@ import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { getUrlParams, setUrlParams, deleteUrlParam } from '../../lib/browser';
import { AICourseSearch } from './AICourseSearch';
import { Pagination } from '../Pagination/Pagination';
import { AILoadingState } from '../AITutor/AILoadingState';
import { AITutorTallMessage } from '../AITutor/AITutorTallMessage';
type UserCoursesListProps = {};
@ -72,6 +74,43 @@ export function UserCoursesList(props: UserCoursesListProps) {
}
}, [pageState]);
if (isInitialLoading || isUserAiCoursesLoading) {
return (
<AILoadingState
title="Loading your courses"
subtitle="This may take a moment..."
/>
);
}
if (!isLoggedIn()) {
return (
<AITutorTallMessage
title="Sign up or login"
subtitle="Takes 2s to sign up and generate your first course."
icon={BookOpen}
buttonText="Sign up or Login"
onButtonClick={() => {
showLoginPopup();
}}
/>
);
}
if (courses.length === 0) {
return (
<AITutorTallMessage
title="No courses found"
subtitle="You haven't generated any courses yet."
icon={BookOpen}
buttonText="Create your first course"
onButtonClick={() => {
window.location.href = '/ai';
}}
/>
);
}
return (
<>
{showUpgradePopup && (
@ -105,7 +144,7 @@ export function UserCoursesList(props: UserCoursesListProps) {
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"
className="ml-1.5 flex items-center gap-1 rounded-full bg-yellow-600 py-0.5 pr-2 pl-1.5 text-xs text-white"
>
<Gift className="size-4" />
Upgrade
@ -127,46 +166,13 @@ export function UserCoursesList(props: UserCoursesListProps) {
</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">
<User2 className="mb-2 size-8 text-gray-300" />
<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 generate and save courses.
</p>
</div>
)}
{!isUserAiCoursesLoading && !isInitialLoading && courses.length === 0 && isAuthenticated && (
<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 && courses && courses.length > 0 && (
<div className="flex flex-col gap-2">
{courses.map((course) => (
<AICourseCard key={course._id} course={course} />
))}
<div className="grid grid-cols-3 gap-2">
{courses.map((course) => (
<AICourseCard key={course._id} course={course} />
))}
</div>
<Pagination
totalCount={userAiCourses?.totalCount || 0}

@ -12,10 +12,6 @@ const ogImage = 'https://roadmap.sh/og-images/ai-tutor.png';
description='Learn anything with AI Tutor. Pick a topic, choose a difficulty level and the AI will guide you through the learning process.'
>
<AITutorLayout activeTab='courses' client:load>
<section class='flex grow flex-col bg-gray-100'>
<div class='container mx-auto flex max-w-3xl flex-col py-10 max-sm:py-4'>
<UserCoursesList client:load />
</div>
</section>
<UserCoursesList client:load />
</AITutorLayout>
</SkeletonLayout>

Loading…
Cancel
Save