Limits and errors

feat/ai-courses
Kamran Ahmed 1 month ago
parent 3f4a5bf4bd
commit 5c99f318e8
  1. 30
      src/components/GenerateCourse/AICourseContent.tsx
  2. 37
      src/components/GenerateCourse/AICourseLimit.tsx
  3. 29
      src/components/GenerateCourse/AICourseModuleView.tsx
  4. 12
      src/components/GenerateCourse/AILimitsPopup.tsx
  5. 14
      src/queries/ai-course.ts

@ -11,6 +11,8 @@ import { ErrorIcon } from '../ReactIcons/ErrorIcon';
import { AICourseLimit } from './AICourseLimit'; import { AICourseLimit } from './AICourseLimit';
import { AICourseModuleList } from './AICourseModuleList'; import { AICourseModuleList } from './AICourseModuleList';
import { AICourseModuleView } from './AICourseModuleView'; import { AICourseModuleView } from './AICourseModuleView';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { AILimitsPopup } from './AILimitsPopup';
type AICourseContentProps = { type AICourseContentProps = {
courseSlug?: string; courseSlug?: string;
@ -22,6 +24,9 @@ type AICourseContentProps = {
export function AICourseContent(props: AICourseContentProps) { export function AICourseContent(props: AICourseContentProps) {
const { course, courseSlug, isLoading, error } = props; const { course, courseSlug, isLoading, error } = props;
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const [showAILimitsPopup, setShowAILimitsPopup] = useState(false);
const [activeModuleIndex, setActiveModuleIndex] = useState(0); const [activeModuleIndex, setActiveModuleIndex] = useState(0);
const [activeLessonIndex, setActiveLessonIndex] = useState(0); const [activeLessonIndex, setActiveLessonIndex] = useState(0);
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
@ -114,6 +119,20 @@ export function AICourseContent(props: AICourseContentProps) {
return ( return (
<section className="flex h-screen flex-grow flex-col overflow-hidden bg-gray-50"> <section className="flex h-screen flex-grow flex-col overflow-hidden bg-gray-50">
{showUpgradeModal && (
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
)}
{showAILimitsPopup && (
<AILimitsPopup
onClose={() => setShowAILimitsPopup(false)}
onUpgrade={() => {
setShowAILimitsPopup(false);
setShowUpgradeModal(true);
}}
/>
)}
<div className="border-b border-gray-200 bg-gray-100"> <div className="border-b border-gray-200 bg-gray-100">
<div className="flex items-center justify-between px-4 py-2"> <div className="flex items-center justify-between px-4 py-2">
<a <a
@ -126,7 +145,10 @@ export function AICourseContent(props: AICourseContentProps) {
</a> </a>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex flex-row lg:hidden"> <div className="flex flex-row lg:hidden">
<AICourseLimit /> <AICourseLimit
onUpgrade={() => setShowUpgradeModal(true)}
onShowLimits={() => setShowAILimitsPopup(true)}
/>
</div> </div>
<button <button
@ -179,7 +201,10 @@ export function AICourseContent(props: AICourseContentProps) {
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<div className="hidden gap-2 lg:flex"> <div className="hidden gap-2 lg:flex">
<AICourseLimit /> <AICourseLimit
onUpgrade={() => setShowUpgradeModal(true)}
onShowLimits={() => setShowAILimitsPopup(true)}
/>
</div> </div>
{viewMode === 'module' && ( {viewMode === 'module' && (
@ -274,6 +299,7 @@ export function AICourseContent(props: AICourseContentProps) {
onGoToPrevLesson={goToPrevLesson} onGoToPrevLesson={goToPrevLesson}
onGoToNextLesson={goToNextLesson} onGoToNextLesson={goToNextLesson}
key={`${courseSlug}-${activeModuleIndex}-${activeLessonIndex}`} key={`${courseSlug}-${activeModuleIndex}-${activeLessonIndex}`}
onUpgrade={() => setShowUpgradeModal(true)}
/> />
)} )}

@ -1,16 +1,17 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { getAiCourseLimitOptions } from '../../queries/ai-course'; import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client'; import { queryClient } from '../../stores/query-client';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { billingDetailsOptions } from '../../queries/billing'; import { billingDetailsOptions } from '../../queries/billing';
import { getPercentage } from '../../helper/number'; import { getPercentage } from '../../helper/number';
import { Gift, Info } from 'lucide-react'; import { Gift, Info } from 'lucide-react';
import { AILimitsPopup } from './AILimitsPopup';
export function AICourseLimit() { type AICourseLimitProps = {
const [showUpgradeModal, setShowUpgradeModal] = useState(false); onUpgrade: () => void;
const [showAILimitsPopup, setShowAILimitsPopup] = useState(false); onShowLimits: () => void;
};
export function AICourseLimit(props: AICourseLimitProps) {
const { onUpgrade, onShowLimits } = props;
const { data: limits, isLoading } = useQuery( const { data: limits, isLoading } = useQuery(
getAiCourseLimitOptions(), getAiCourseLimitOptions(),
@ -38,28 +39,16 @@ export function AICourseLimit() {
<> <>
<button <button
className="mr-1 flex items-center gap-1 text-sm font-medium underline underline-offset-2 lg:hidden" className="mr-1 flex items-center gap-1 text-sm font-medium underline underline-offset-2 lg:hidden"
onClick={() => setShowAILimitsPopup(true)} onClick={() => onShowLimits()}
> >
<Info className="size-4" /> <Info className="size-4" />
{totalPercentage}% limit used {totalPercentage}% limit used
</button> </button>
{showAILimitsPopup && (
<AILimitsPopup
used={used}
limit={limit}
onClose={() => setShowAILimitsPopup(false)}
onUpgrade={() => {
setShowAILimitsPopup(false);
setShowUpgradeModal(true);
}}
/>
)}
{(!isPaidUser || isNearLimit) && ( {(!isPaidUser || isNearLimit) && (
<button <button
onClick={() => { onClick={() => {
setShowAILimitsPopup(true); onShowLimits();
}} }}
className="relative hidden h-full min-h-[38px] cursor-pointer items-center overflow-hidden rounded-lg border border-gray-300 px-3 py-1.5 text-sm hover:bg-gray-50 lg:flex" className="relative hidden h-full min-h-[38px] cursor-pointer items-center overflow-hidden rounded-lg border border-gray-300 px-3 py-1.5 text-sm hover:bg-gray-50 lg:flex"
> >
@ -76,19 +65,13 @@ export function AICourseLimit() {
)} )}
{!isPaidUser && ( {!isPaidUser && (
<>
<button <button
className="hidden items-center justify-center gap-1 rounded-md bg-yellow-400 px-4 py-1 text-sm font-medium underline-offset-2 hover:bg-yellow-500 lg:flex" className="hidden items-center justify-center gap-1 rounded-md bg-yellow-400 px-4 py-1 text-sm font-medium underline-offset-2 hover:bg-yellow-500 lg:flex"
onClick={() => setShowUpgradeModal(true)} onClick={() => onUpgrade()}
> >
<Gift className="size-4" /> <Gift className="size-4" />
Upgrade Upgrade
</button> </button>
{showUpgradeModal && (
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
)}
</>
)} )}
</> </>
); );

@ -38,6 +38,8 @@ type AICourseModuleViewProps = {
onGoToPrevLesson: () => void; onGoToPrevLesson: () => void;
onGoToNextLesson: () => void; onGoToNextLesson: () => void;
onUpgrade: () => void;
}; };
export function AICourseModuleView(props: AICourseModuleViewProps) { export function AICourseModuleView(props: AICourseModuleViewProps) {
@ -53,6 +55,8 @@ export function AICourseModuleView(props: AICourseModuleViewProps) {
onGoToPrevLesson, onGoToPrevLesson,
onGoToNextLesson, onGoToNextLesson,
onUpgrade,
} = props; } = props;
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -262,8 +266,29 @@ export function AICourseModuleView(props: AICourseModuleViewProps) {
)} )}
{error && isLoggedIn() && ( {error && isLoggedIn() && (
<div className="mt-8 flex items-center justify-center"> <div className="mt-8 flex min-h-[300px] items-center justify-center rounded-xl bg-red-50/80">
<p className="text-red-500">{error}</p> {error.includes('reached the limit') ? (
<div className="flex max-w-sm flex-col items-center text-center">
<h2 className="text-xl font-semibold text-red-600">
Limit reached
</h2>
<p className="my-3 text-red-600">
You have reached the AI usage limit for today. Please upgrade
your account to continue.
</p>
<button
onClick={() => {
onUpgrade();
}}
className="rounded-full bg-red-600 px-4 py-1 text-white hover:bg-red-700"
>
Upgrade Account
</button>
</div>
) : (
<p className="text-red-600">{error}</p>
)}
</div> </div>
)} )}

@ -4,16 +4,22 @@ import { formatCommaNumber } from '../../lib/number';
import { billingDetailsOptions } from '../../queries/billing'; import { billingDetailsOptions } from '../../queries/billing';
import { queryClient } from '../../stores/query-client'; import { queryClient } from '../../stores/query-client';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { getAiCourseLimitOptions } from '../../queries/ai-course';
type AILimitsPopupProps = { type AILimitsPopupProps = {
used: number;
limit: number;
onClose: () => void; onClose: () => void;
onUpgrade: () => void; onUpgrade: () => void;
}; };
export function AILimitsPopup(props: AILimitsPopupProps) { export function AILimitsPopup(props: AILimitsPopupProps) {
const { used, limit, onClose, onUpgrade } = props; const { onClose, onUpgrade } = props;
const { data: limits, isLoading } = useQuery(
getAiCourseLimitOptions(),
queryClient,
);
const { used, limit } = limits ?? { used: 0, limit: 0 };
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } = const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient); useQuery(billingDetailsOptions(), queryClient);

@ -15,10 +15,6 @@ type GetAICourseProgressParams = {
aiCourseSlug: string; aiCourseSlug: string;
}; };
type GetAICourseProgressBody = {};
type GetAICourseProgressQuery = {};
type GetAICourseProgressResponse = AICourseProgressDocument; type GetAICourseProgressResponse = AICourseProgressDocument;
export function getAiCourseProgressOptions(params: GetAICourseProgressParams) { export function getAiCourseProgressOptions(params: GetAICourseProgressParams) {
@ -50,10 +46,6 @@ export interface AICourseDocument {
updatedAt: Date; updatedAt: Date;
} }
type GetAICourseBody = {};
type GetAICourseQuery = {};
type GetAICourseResponse = AICourseDocument; type GetAICourseResponse = AICourseDocument;
export function getAiCourseOptions(params: GetAICourseParams) { export function getAiCourseOptions(params: GetAICourseParams) {
@ -67,9 +59,7 @@ export function getAiCourseOptions(params: GetAICourseParams) {
}; };
} }
type GetAICourseLimitParams = {}; export type GetAICourseLimitResponse = {
type GetAICourseLimitResponse = {
used: number; used: number;
limit: number; limit: number;
}; };
@ -84,8 +74,6 @@ export function getAiCourseLimitOptions() {
}); });
} }
type ListUserAiCoursesParams = {};
type ListUserAiCoursesResponse = (AICourseDocument & { type ListUserAiCoursesResponse = (AICourseDocument & {
progress: AICourseProgressDocument; progress: AICourseProgressDocument;
})[]; })[];

Loading…
Cancel
Save