Refactor staff picks and community

feat/ai-tutor-redesign
Kamran Ahmed 3 days ago
parent 61f5a81d20
commit 4fbea4680c
  1. 66
      src/components/AITutor/AIFeaturedCoursesListing.tsx
  2. 46
      src/components/AITutor/AITutorHeader.tsx
  3. 45
      src/components/AITutor/AITutorLimits.tsx
  4. 96
      src/components/GenerateCourse/UserCoursesList.tsx
  5. 6
      src/pages/ai/staff-picks.astro

@ -4,10 +4,13 @@ import {
} from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { useEffect, useState } from 'react';
import { Loader2 } from 'lucide-react';
import { getUrlParams, setUrlParams, deleteUrlParam } from '../../lib/browser';
import { AICourseCard } from '../GenerateCourse/AICourseCard';
import { Pagination } from '../Pagination/Pagination';
import { AILoadingState } from './AILoadingState';
import { AITutorTallMessage } from './AITutorTallMessage';
import { BookOpen } from 'lucide-react';
import { AITutorHeader } from './AITutorHeader';
type AIFeaturedCoursesListingProps = {};
@ -47,34 +50,45 @@ export function AIFeaturedCoursesListing(props: AIFeaturedCoursesListingProps) {
}
}, [pageState]);
return (
<>
<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">Staff Picks</h2>
</div>
</div>
if (isInitialLoading || isFeaturedAiCoursesLoading) {
return (
<AILoadingState
title="Loading featured courses"
subtitle="This may take a moment..."
/>
);
}
{(isFeaturedAiCoursesLoading || 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>
)}
if (courses.length === 0) {
return (
<AITutorTallMessage
title="No featured courses"
subtitle="There are no featured courses available at the moment."
icon={BookOpen}
buttonText="Browse all courses"
onButtonClick={() => {
window.location.href = '/ai';
}}
/>
);
}
return (
<div className="w-full">
<AITutorHeader title="Featured Courses" />
{!isFeaturedAiCoursesLoading && courses && courses.length > 0 && (
<div className="flex flex-col gap-2">
{courses.map((course) => (
<AICourseCard
key={course._id}
course={course}
showActions={false}
showProgress={false}
/>
))}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
{courses.map((course) => (
<AICourseCard
key={course._id}
course={course}
showActions={false}
showProgress={false}
/>
))}
</div>
<Pagination
totalCount={featuredAiCourses?.totalCount || 0}
@ -98,6 +112,6 @@ export function AIFeaturedCoursesListing(props: AIFeaturedCoursesListingProps) {
</p>
</div>
)}
</>
</div>
);
}

@ -0,0 +1,46 @@
import { useQuery } from '@tanstack/react-query';
import { AITutorLimits } from './AITutorLimits';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
type AITutorHeaderProps = {
title: string;
isPaidUser: boolean;
isPaidUserLoading: boolean;
setShowUpgradePopup: (show: boolean) => void;
children?: React.ReactNode;
};
export function AITutorHeader(props: AITutorHeaderProps) {
const {
title,
isPaidUser,
isPaidUserLoading,
setShowUpgradePopup,
children,
} = props;
const { data: limits } = useQuery(getAiCourseLimitOptions(), queryClient);
const { used, limit } = limits ?? { used: 0, limit: 0 };
return (
<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">{title}</h2>
</div>
<div className="flex items-center gap-2">
<AITutorLimits
used={used}
limit={limit}
isPaidUser={isPaidUser}
isPaidUserLoading={isPaidUserLoading}
onUpgradeClick={() => setShowUpgradePopup(true)}
/>
{children}
</div>
</div>
);
}

@ -0,0 +1,45 @@
import { Gift } from 'lucide-react';
import { cn } from '../../lib/classname';
type AITutorLimitsProps = {
used: number;
limit: number;
isPaidUser: boolean;
isPaidUserLoading: boolean;
onUpgradeClick: () => void;
};
export function AITutorLimits(props: AITutorLimitsProps) {
const limitUsedPercentage = Math.round((props.used / props.limit) * 100);
if (props.used <= 0 || props.limit <= 0 || props.isPaidUserLoading) {
return null;
}
return (
<div
className={cn(
'pointer-events-none flex items-center gap-2 opacity-0 transition-opacity',
{
'pointer-events-auto opacity-100': !props.isPaidUser,
},
)}
>
<p className="flex items-center text-sm text-yellow-600">
<span className="max-md:hidden">
{limitUsedPercentage}% of daily limit used{' '}
</span>
<span className="inline md:hidden">
{limitUsedPercentage}% used
</span>
<button
onClick={props.onUpgradeClick}
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
</button>
</p>
</div>
);
}

@ -1,23 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import { BookOpen } from 'lucide-react';
import { useEffect, useState } from 'react';
import { deleteUrlParam, getUrlParams, setUrlParams } from '../../lib/browser';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import {
getAiCourseLimitOptions,
listUserAiCoursesOptions,
type ListUserAiCoursesQuery,
} from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client';
import { AICourseCard } from './AICourseCard';
import { useEffect, useState } from 'react';
import { BookOpen, Gift } from 'lucide-react';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { cn } from '../../lib/classname';
import { useIsPaidUser } from '../../queries/billing';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { getUrlParams, setUrlParams, deleteUrlParam } from '../../lib/browser';
import { AICourseSearch } from './AICourseSearch';
import { Pagination } from '../Pagination/Pagination';
import { queryClient } from '../../stores/query-client';
import { AILoadingState } from '../AITutor/AILoadingState';
import { AITutorHeader } from '../AITutor/AITutorHeader';
import { AITutorTallMessage } from '../AITutor/AITutorTallMessage';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { Pagination } from '../Pagination/Pagination';
import { AICourseCard } from './AICourseCard';
import { AICourseSearch } from './AICourseSearch';
type UserCoursesListProps = {};
@ -31,12 +30,6 @@ export function UserCoursesList(props: UserCoursesListProps) {
query: '',
});
const { data: limits, isLoading: isLimitsLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const { used, limit } = limits ?? { used: 0, limit: 0 };
const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser();
const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery(
@ -49,8 +42,6 @@ export function UserCoursesList(props: UserCoursesListProps) {
}, [userAiCourses]);
const courses = userAiCourses?.data ?? [];
const isAuthenticated = isLoggedIn();
const limitUsedPercentage = Math.round((used / limit) * 100);
useEffect(() => {
const queryParams = getUrlParams();
@ -116,55 +107,24 @@ export function UserCoursesList(props: UserCoursesListProps) {
{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">
<span className="max-md:hidden">Your </span>Courses
</h2>
</div>
<div className="flex items-center gap-2">
{used > 0 && limit > 0 && !isPaidUserLoading && (
<div
className={cn(
'pointer-events-none flex items-center gap-2 opacity-0 transition-opacity',
{
'pointer-events-auto opacity-100': !isPaidUser,
},
)}
>
<p className="flex items-center text-sm text-yellow-600">
<span className="max-md:hidden">
{limitUsedPercentage}% of daily limit used{' '}
</span>
<span className="inline md:hidden">
{limitUsedPercentage}% used
</span>
<button
onClick={() => {
setShowUpgradePopup(true);
}}
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
</button>
</p>
</div>
)}
<AICourseSearch
value={pageState?.query || ''}
onChange={(value) => {
setPageState({
...pageState,
query: value,
currPage: '1',
});
}}
/>
</div>
</div>
<AITutorHeader
title="Your Courses"
isPaidUser={isPaidUser}
isPaidUserLoading={isPaidUserLoading}
setShowUpgradePopup={setShowUpgradePopup}
>
<AICourseSearch
value={pageState?.query || ''}
onChange={(value) => {
setPageState({
...pageState,
query: value,
currPage: '1',
});
}}
/>
</AITutorHeader>
{!isUserAiCoursesLoading && courses && courses.length > 0 && (
<div className="flex flex-col gap-2">

@ -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='staff-picks' 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'>
<AIFeaturedCoursesListing client:load />
</div>
</section>
<AIFeaturedCoursesListing client:load />
</AITutorLayout>
</SkeletonLayout>

Loading…
Cancel
Save