Improve logic for upgrade and limits button

feat/ai-courses
Kamran Ahmed 2 months ago
parent a80d012c23
commit 3f4a5bf4bd
  1. 74
      src/components/Billing/UpgradeAccountModal.tsx
  2. 36
      src/components/Billing/VerifyUpgrade.tsx
  3. 38
      src/components/GenerateCourse/AICourseLimit.tsx
  4. 33
      src/components/GenerateCourse/AILimitsPopup.tsx

@ -130,8 +130,8 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
return ( return (
<Modal <Modal
onClose={onClose} onClose={onClose}
bodyClassName="p-6 bg-white" bodyClassName="p-4 sm:p-6 bg-white"
wrapperClassName="h-auto rounded-xl max-w-3xl w-full min-h-[540px]" wrapperClassName="h-auto rounded-xl max-w-3xl w-full min-h-[540px] mx-2 sm:mx-4"
overlayClassName="items-start md:items-center" overlayClassName="items-start md:items-center"
> >
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
@ -140,16 +140,16 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
{loader} {loader}
{!isLoading && !error && ( {!isLoading && !error && (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="mb-8 text-left"> <div className="mb-6 sm:mb-8 text-left">
<h2 className="text-2xl font-bold text-black"> <h2 className="text-xl sm:text-2xl font-bold text-black">
Unlock Premium Features Unlock Premium Features
</h2> </h2>
<p className="mt-2 text-gray-600"> <p className="mt-1 sm:mt-2 text-sm sm:text-base text-gray-600">
Supercharge your learning experience with premium benefits Supercharge your learning experience with premium benefits
</p> </p>
</div> </div>
<div className="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="mb-6 sm:mb-8 grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2">
{USER_SUBSCRIPTION_PLAN_PRICES.map((plan) => { {USER_SUBSCRIPTION_PLAN_PRICES.map((plan) => {
const isCurrentPlanSelected = const isCurrentPlanSelected =
currentPlan?.priceId === plan.priceId; currentPlan?.priceId === plan.priceId;
@ -159,41 +159,41 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
<div <div
key={plan.interval} key={plan.interval}
className={cn( className={cn(
'flex flex-col space-y-4 rounded-lg bg-white p-6', 'flex flex-col space-y-3 sm:space-y-4 rounded-lg bg-white p-4 sm:p-6',
isYearly isYearly
? 'border-2 border-yellow-400 shadow-lg shadow-yellow-400/20' ? 'border-2 border-yellow-400'
: 'border border-gray-200', : 'border border-gray-200',
)} )}
> >
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div> <div>
<h4 className="font-semibold text-black"> <h4 className="text-sm sm:text-base font-semibold text-black">
{isYearly ? 'Yearly Payment' : 'Monthly Payment'} {isYearly ? 'Yearly Payment' : 'Monthly Payment'}
</h4> </h4>
{isYearly && ( {isYearly && (
<span className="text-sm font-medium text-green-600"> <span className="text-xs sm:text-sm font-medium text-green-600">
(2 months free) (2 months free)
</span> </span>
)} )}
</div> </div>
{isYearly && ( {isYearly && (
<span className="rounded-full bg-yellow-400 px-2 py-1 text-xs font-semibold text-black"> <span className="rounded-full bg-yellow-400 px-1.5 py-0.5 sm:px-2 sm:py-1 text-xs font-semibold text-black">
Most Popular Most Popular
</span> </span>
)} )}
</div> </div>
<div className="flex items-baseline"> <div className="flex items-baseline">
{isYearly && ( {isYearly && (
<p className="mr-2 text-sm text-gray-400 line-through"> <p className="mr-2 text-xs sm:text-sm text-gray-400 line-through">
$ $
{calculateYearlyPrice( {calculateYearlyPrice(
USER_SUBSCRIPTION_PLAN_PRICES[0].amount, USER_SUBSCRIPTION_PLAN_PRICES[0].amount,
)} )}
</p> </p>
)} )}
<p className="text-3xl font-bold text-black"> <p className="text-2xl sm:text-3xl font-bold text-black">
${plan.amount}{' '} ${plan.amount}{' '}
<span className="text-sm font-normal text-gray-500"> <span className="text-xs sm:text-sm font-normal text-gray-500">
/ {isYearly ? 'year' : 'month'} / {isYearly ? 'year' : 'month'}
</span> </span>
</p> </p>
@ -204,7 +204,7 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
<div> <div>
<button <button
className={cn( className={cn(
'flex min-h-11 w-full items-center justify-center rounded-md py-2.5 font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-yellow-400 disabled:cursor-not-allowed disabled:opacity-60', 'flex min-h-9 sm:min-h-11 w-full items-center justify-center rounded-md py-2 sm:py-2.5 text-sm sm:text-base font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-yellow-400 disabled:cursor-not-allowed disabled:opacity-60',
'bg-yellow-400 text-black hover:bg-yellow-500', 'bg-yellow-400 text-black hover:bg-yellow-500',
)} )}
disabled={ disabled={
@ -229,7 +229,7 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
> >
{isCreatingCheckoutSession && {isCreatingCheckoutSession &&
selectedPlan === plan.interval ? ( selectedPlan === plan.interval ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-3.5 w-3.5 sm:h-4 sm:w-4 animate-spin" />
) : isCurrentPlanSelected ? ( ) : isCurrentPlanSelected ? (
'Current Plan' 'Current Plan'
) : ( ) : (
@ -243,58 +243,58 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) {
</div> </div>
{/* Benefits Section */} {/* Benefits Section */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-2">
<div className="flex items-start space-x-3"> <div className="flex items-start space-x-2 sm:space-x-3">
<Zap className="mt-0.5 h-5 w-5 text-yellow-400" /> <Zap className="mt-0.5 h-4 w-4 sm:h-5 sm:w-5 text-yellow-400" />
<div> <div>
<h4 className="font-medium text-black"> <h4 className="text-sm sm:text-base font-medium text-black">
Unlimited AI Course Generations Unlimited AI Course Generations
</h4> </h4>
<p className="text-sm text-gray-600"> <p className="text-xs sm:text-sm text-gray-600">
Generate as many custom courses as you need Generate as many custom courses as you need
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-start space-x-3"> <div className="flex items-start space-x-2 sm:space-x-3">
<Infinity className="mt-0.5 h-5 w-5 text-yellow-400" /> <Infinity className="mt-0.5 h-4 w-4 sm:h-5 sm:w-5 text-yellow-400" />
<div> <div>
<h4 className="font-medium text-black"> <h4 className="text-sm sm:text-base font-medium text-black">
No Daily Limits on course features No Daily Limits on course features
</h4> </h4>
<p className="text-sm text-gray-600"> <p className="text-xs sm:text-sm text-gray-600">
Use all features without restrictions Use all features without restrictions
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-start space-x-3"> <div className="flex items-start space-x-2 sm:space-x-3">
<MessageSquare className="mt-0.5 h-5 w-5 text-yellow-400" /> <MessageSquare className="mt-0.5 h-4 w-4 sm:h-5 sm:w-5 text-yellow-400" />
<div> <div>
<h4 className="font-medium text-black"> <h4 className="text-sm sm:text-base font-medium text-black">
Unlimited Course Follow-ups Unlimited Course Follow-ups
</h4> </h4>
<p className="text-sm text-gray-600"> <p className="text-xs sm:text-sm text-gray-600">
Ask as many questions as you need Ask as many questions as you need
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-start space-x-3"> <div className="flex items-start space-x-2 sm:space-x-3">
<Sparkles className="mt-0.5 h-5 w-5 text-yellow-400" /> <Sparkles className="mt-0.5 h-4 w-4 sm:h-5 sm:w-5 text-yellow-400" />
<div> <div>
<h4 className="font-medium text-black"> <h4 className="text-sm sm:text-base font-medium text-black">
Early Access to Features Early Access to Features
</h4> </h4>
<p className="text-sm text-gray-600"> <p className="text-xs sm:text-sm text-gray-600">
Be the first to try new tools and features Be the first to try new tools and features
</p> </p>
</div> </div>
</div> </div>
<div className="flex items-start space-x-3"> <div className="flex items-start space-x-2 sm:space-x-3">
<Heart className="mt-0.5 h-5 w-5 text-yellow-400" /> <Heart className="mt-0.5 h-4 w-4 sm:h-5 sm:w-5 text-yellow-400" />
<div> <div>
<h4 className="font-medium text-black"> <h4 className="text-sm sm:text-base font-medium text-black">
Support Development Support Development
</h4> </h4>
<p className="text-sm text-gray-600"> <p className="text-xs sm:text-sm text-gray-600">
Help us continue building roadmap.sh Help us continue building roadmap.sh
</p> </p>
</div> </div>

@ -13,7 +13,7 @@ type VerifyUpgradeProps = {
export function VerifyUpgrade(props: VerifyUpgradeProps) { export function VerifyUpgrade(props: VerifyUpgradeProps) {
const { newPriceId } = props; const { newPriceId } = props;
const { data: userBillingDetails, isFetching } = useQuery( const { data: userBillingDetails } = useQuery(
{ {
...billingDetailsOptions(), ...billingDetailsOptions(),
refetchInterval: 1000, refetchInterval: 1000,
@ -42,32 +42,34 @@ export function VerifyUpgrade(props: VerifyUpgradeProps) {
onClose={() => {}} onClose={() => {}}
bodyClassName="rounded-xl bg-white p-6" bodyClassName="rounded-xl bg-white p-6"
> >
<div className="flex flex-col items-center text-center mb-4"> <div className="mb-4 flex flex-col items-center text-center">
<CheckCircle className="h-12 w-12 text-green-600 mb-3" /> <CheckCircle className="mb-3 h-12 w-12 text-green-600" />
<h3 className="text-xl font-bold text-black">Subscription Activated</h3> <h3 className="text-xl font-bold text-black">Subscription Activated</h3>
</div> </div>
<p className="mt-2 text-balance text-gray-600 text-center"> <p className="mt-2 text-balance text-center text-gray-600">
Your subscription has been activated successfully. Your subscription has been activated successfully.
</p> </p>
<p className="mt-4 text-balance text-gray-600 text-center"> <p className="mt-4 text-balance text-center text-gray-600">
It might take a few minutes for the changes to reflect. We will{' '} It might take a minute for the changes to reflect. We will{' '}
<b className="text-black">reload</b> the page for you. <b className="text-black">reload</b> the page for you.
</p> </p>
{isFetching && ( <div className="my-6 flex animate-pulse items-center justify-center gap-2">
<div className="flex animate-pulse items-center justify-center gap-2 mt-4"> <Loader2 className="size-4 animate-spin stroke-[2.5px] text-green-600" />
<Loader2 className="h-5 w-5 animate-spin stroke-[2.5px] text-green-600" /> <span className="text-gray-600">Please wait...</span>
<span className="text-gray-600">Refreshing</span> </div>
</div>
)}
<p className="mt-6 text-gray-500 text-center text-sm"> <p className="text-center text-sm text-gray-500">
If it takes longer than expected, please{' '} If it takes longer than expected, please email us at{' '}
<a className="text-blue-600 underline underline-offset-2 hover:text-blue-700"> <a
contact us href="mailto:info@roadmap.sh"
</a>. className="text-blue-600 underline underline-offset-2 hover:text-blue-700"
>
info@roadmap.sh
</a>
.
</p> </p>
</Modal> </Modal>
); );

@ -30,6 +30,10 @@ export function AICourseLimit() {
const totalPercentage = getPercentage(used, limit); const totalPercentage = getPercentage(used, limit);
// has consumed 80% of the limit
const isNearLimit = used >= limit * 0.8;
const isPaidUser = userBillingDetails.status !== 'none';
return ( return (
<> <>
<button <button
@ -52,24 +56,26 @@ export function AICourseLimit() {
/> />
)} )}
<button {(!isPaidUser || isNearLimit) && (
onClick={() => { <button
setShowAILimitsPopup(true); onClick={() => {
}} setShowAILimitsPopup(true);
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"
>
<span className="relative z-10">
{totalPercentage}% of the daily limit used
</span>
<div
className="absolute inset-0 h-full bg-gray-200/80"
style={{
width: `${totalPercentage}%`,
}} }}
></div> 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"
</button> >
<span className="relative z-10">
{totalPercentage}% of the daily limit used
</span>
<div
className="absolute inset-0 h-full bg-gray-200/80"
style={{
width: `${totalPercentage}%`,
}}
></div>
</button>
)}
{userBillingDetails.status === 'none' && ( {!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"

@ -1,6 +1,9 @@
import { Gift } from 'lucide-react'; import { Gift } from 'lucide-react';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { formatCommaNumber } from '../../lib/number'; import { formatCommaNumber } from '../../lib/number';
import { billingDetailsOptions } from '../../queries/billing';
import { queryClient } from '../../stores/query-client';
import { useQuery } from '@tanstack/react-query';
type AILimitsPopupProps = { type AILimitsPopupProps = {
used: number; used: number;
@ -12,6 +15,11 @@ type AILimitsPopupProps = {
export function AILimitsPopup(props: AILimitsPopupProps) { export function AILimitsPopup(props: AILimitsPopupProps) {
const { used, limit, onClose, onUpgrade } = props; const { used, limit, onClose, onUpgrade } = props;
const { data: userBillingDetails, isLoading: isBillingDetailsLoading } =
useQuery(billingDetailsOptions(), queryClient);
const isPaidUser = userBillingDetails?.status !== 'none';
return ( return (
<Modal <Modal
onClose={onClose} onClose={onClose}
@ -50,7 +58,7 @@ export function AILimitsPopup(props: AILimitsPopupProps) {
<p className="text-2xl font-bold">{formatCommaNumber(used)}</p> <p className="text-2xl font-bold">{formatCommaNumber(used)}</p>
</div> </div>
<div> <div>
<p className="text-sm text-gray-500">Daily Token Limit</p> <p className="text-sm text-gray-500">Daily Limit</p>
<p className="text-2xl font-bold">{formatCommaNumber(limit)}</p> <p className="text-2xl font-bold">{formatCommaNumber(limit)}</p>
</div> </div>
</div> </div>
@ -60,25 +68,28 @@ export function AILimitsPopup(props: AILimitsPopupProps) {
<div className="mt-2"> <div className="mt-2">
<div className="space-y-3 text-gray-600"> <div className="space-y-3 text-gray-600">
<p className="text-sm"> <p className="text-sm">
Limit resets every 24 hours. Consider upgrading for more tokens. Limit resets every 24 hours.{' '}
{!isPaidUser && 'Consider upgrading for more tokens.'}
</p> </p>
</div> </div>
</div> </div>
{/* Action Button */} {/* Action Button */}
<div className="mt-auto flex flex-col gap-2 pt-4"> <div className="mt-auto flex flex-col gap-2 pt-4">
<button {!isPaidUser && (
onClick={onUpgrade} <button
className="flex w-full items-center justify-center gap-2 rounded-lg bg-yellow-400 px-4 py-2.5 font-medium text-black transition-colors hover:bg-yellow-500" onClick={onUpgrade}
> className="flex w-full items-center justify-center gap-2 rounded-lg bg-yellow-400 px-4 py-2.5 text-sm font-medium text-black transition-colors hover:bg-yellow-500"
<Gift className="size-4" /> >
Upgrade <Gift className="size-4" />
</button> Upgrade to Unlimited
</button>
)}
<button <button
onClick={onClose} onClick={onClose}
className="w-full rounded-lg bg-gray-200 px-4 py-2.5 text-gray-600 transition-colors hover:bg-gray-300" className="w-full rounded-lg bg-gray-200 px-4 py-2.5 text-sm text-gray-600 transition-colors hover:bg-gray-300"
> >
Dismiss Close
</button> </button>
</div> </div>
</Modal> </Modal>

Loading…
Cancel
Save