From d0f8fc4e6af06e5b65967bfd3f65cf1e6e25a323 Mon Sep 17 00:00:00 2001 From: Kamran Ahmed <kamranahmed.se@gmail.com> Date: Wed, 9 Apr 2025 14:18:02 +0100 Subject: [PATCH] AI landing page changes --- src/components/AITutor/AILoadingState.tsx | 27 +++ src/components/AITutor/AITutorLayout.tsx | 4 +- src/components/AITutor/AITutorTallMessage.tsx | 31 ++++ src/components/GenerateCourse/AICourse.tsx | 175 +++++++++--------- .../GenerateCourse/AICourseCard.tsx | 12 +- .../GenerateCourse/UserCoursesList.tsx | 86 +++++---- src/pages/ai/courses.astro | 6 +- 7 files changed, 196 insertions(+), 145 deletions(-) create mode 100644 src/components/AITutor/AILoadingState.tsx create mode 100644 src/components/AITutor/AITutorTallMessage.tsx diff --git a/src/components/AITutor/AILoadingState.tsx b/src/components/AITutor/AILoadingState.tsx new file mode 100644 index 000000000..66ed9de4e --- /dev/null +++ b/src/components/AITutor/AILoadingState.tsx @@ -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> + ); +} \ No newline at end of file diff --git a/src/components/AITutor/AITutorLayout.tsx b/src/components/AITutor/AITutorLayout.tsx index 18b23649f..5cfe7f3ce 100644 --- a/src/components/AITutor/AITutorLayout.tsx +++ b/src/components/AITutor/AITutorLayout.tsx @@ -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> ); } diff --git a/src/components/AITutor/AITutorTallMessage.tsx b/src/components/AITutor/AITutorTallMessage.tsx new file mode 100644 index 000000000..a0000a3ff --- /dev/null +++ b/src/components/AITutor/AITutorTallMessage.tsx @@ -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> + ); +} diff --git a/src/components/GenerateCourse/AICourse.tsx b/src/components/GenerateCourse/AICourse.tsx index 5708a505c..428fb21fe 100644 --- a/src/components/GenerateCourse/AICourse.tsx +++ b/src/components/GenerateCourse/AICourse.tsx @@ -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> ); } diff --git a/src/components/GenerateCourse/AICourseCard.tsx b/src/components/GenerateCourse/AICourseCard.tsx index 0d758978b..9db197881 100644 --- a/src/components/GenerateCourse/AICourseCard.tsx +++ b/src/components/GenerateCourse/AICourseCard.tsx @@ -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 diff --git a/src/components/GenerateCourse/UserCoursesList.tsx b/src/components/GenerateCourse/UserCoursesList.tsx index 798de8d5f..ee56d665a 100644 --- a/src/components/GenerateCourse/UserCoursesList.tsx +++ b/src/components/GenerateCourse/UserCoursesList.tsx @@ -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} diff --git a/src/pages/ai/courses.astro b/src/pages/ai/courses.astro index e54254731..25a37a443 100644 --- a/src/pages/ai/courses.astro +++ b/src/pages/ai/courses.astro @@ -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>