diff --git a/src/components/AccountSidebar.astro b/src/components/AccountSidebar.astro index 2ade4e569..c1c2d9575 100644 --- a/src/components/AccountSidebar.astro +++ b/src/components/AccountSidebar.astro @@ -69,7 +69,7 @@ const sidebarLinks = [ href: '/account/billing', title: 'Billing', id: 'billing', - isNew: true, + isNew: false, icon: { glyph: 'credit-card', classes: 'h-4 w-4', diff --git a/src/components/Billing/UpgradeAccountModal.tsx b/src/components/Billing/UpgradeAccountModal.tsx index 5cdf21bc3..d62225d92 100644 --- a/src/components/Billing/UpgradeAccountModal.tsx +++ b/src/components/Billing/UpgradeAccountModal.tsx @@ -1,66 +1,47 @@ -import { - Loader2, - Zap, - Infinity, - MessageSquare, - Sparkles, - Heart, - MapIcon, -} from 'lucide-react'; -import type { LucideIcon } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { getUser } from '../../lib/jwt'; import { useMutation, useQuery } from '@tanstack/react-query'; -import { Modal } from '../Modal'; +import type { LucideIcon } from 'lucide-react'; +import { Archive, Crown, Loader2, Map, MessageCircleIcon, X, Zap } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useToast } from '../../hooks/use-toast'; +import { getUser } from '../../lib/jwt'; +import { httpPost } from '../../lib/query-http'; import { billingDetailsOptions, USER_SUBSCRIPTION_PLAN_PRICES, type AllowedSubscriptionInterval, } from '../../queries/billing'; -import { cn } from '../../lib/classname'; import { queryClient } from '../../stores/query-client'; -import { httpPost } from '../../lib/query-http'; -import { useToast } from '../../hooks/use-toast'; +import { Modal } from '../Modal'; import { UpdatePlanConfirmation } from './UpdatePlanConfirmation'; -// Define the perk type type Perk = { icon: LucideIcon; title: string; description: string; + highlight?: boolean; }; -// Define the perks array const PREMIUM_PERKS: Perk[] = [ { icon: Zap, - title: 'AI Course Generations', - description: 'No limits on the number of AI courses', + title: 'Unlimited Courses and Guides', + description: 'No limits on number of courses, guides, and quizzes', + highlight: true, }, { - icon: MapIcon, - title: 'AI Roadmaps', - description: 'No limits on the number of AI roadmaps', + icon: MessageCircleIcon, + title: 'Extended Chat Limits', + description: 'Chat with AI Tutor and Roadmaps without limits', }, { - icon: Infinity, - title: 'Extended Daily Limits', - description: 'Generate more content in a day', + icon: Archive, + title: 'Chat History', + description: 'Access your AI Tutor and roadmap chats later', }, { - icon: MessageSquare, - title: 'Course Follow-ups', - description: 'Ask as many questions as you need', - }, - { - icon: Sparkles, - title: 'Early Access to Features', - description: 'Be the first to try new tools and features', - }, - { - icon: Heart, - title: 'Support Development', - description: 'Help us continue building roadmap.sh', + icon: Map, + title: 'Custom Roadmaps', + description: 'Create upto 100 custom roadmaps', }, ]; @@ -76,7 +57,6 @@ type CreateSubscriptionCheckoutSessionResponse = { type UpgradeAccountModalProps = { onClose: () => void; - success?: string; cancel?: string; }; @@ -85,7 +65,7 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) { const { onClose, success, cancel } = props; const [selectedPlan, setSelectedPlan] = - useState('month'); + useState('year'); const [isUpdatingPlan, setIsUpdatingPlan] = useState(false); const user = getUser(); @@ -132,11 +112,17 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) { (plan) => plan.priceId === currentPlanPriceId, ); + const monthlyPlan = USER_SUBSCRIPTION_PLAN_PRICES.find( + (p) => p.interval === 'month', + ); + const yearlyPlan = USER_SUBSCRIPTION_PLAN_PRICES.find( + (p) => p.interval === 'year', + ); + useEffect(() => { if (!currentPlan) { return; } - setSelectedPlan(currentPlan.interval); }, [currentPlan]); @@ -152,25 +138,44 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) { return null; } - const loader = isLoading ? ( -
- -
- ) : null; + if (isLoading) { + return ( + +
+ +
+
+ ); + } - const error = billingError; - const errorContent = error ? ( -
-

- {error?.message || - 'An error occurred while loading the billing details.'} -

-
- ) : null; - - const calculateYearlyPrice = (monthlyPrice: number) => { - return (monthlyPrice * 12).toFixed(2); - }; + if (billingError) { + return ( + +
+
+ +
+

Error

+

+ {billingError?.message || + 'An error occurred while loading billing details.'} +

+
+
+ ); + } if (isUpdatingPlan && selectedPlanDetails) { return ( @@ -182,175 +187,162 @@ export function UpgradeAccountModal(props: UpgradeAccountModalProps) { ); } + const handlePlanSelect = (plan: typeof selectedPlanDetails) => { + if (!plan) return; + + setSelectedPlan(plan.interval); + + if (!currentPlanPriceId) { + const currentUrlPath = window.location.pathname; + const encodedCurrentUrlPath = encodeURIComponent(currentUrlPath); + const successPage = `/thank-you?next=${encodedCurrentUrlPath}&s=1`; + + window?.fireEvent({ + action: 'tutor_checkout', + category: 'ai_tutor', + label: 'Checkout Started', + }); + + createCheckoutSession( + { + priceId: plan.priceId, + success: success || successPage, + cancel: cancel || `${currentUrlPath}?s=0`, + }, + { + onSuccess: () => { + window?.fireEvent({ + action: `tutor_checkout_${plan.interval === 'month' ? 'mo' : 'an'}`, + category: 'ai_tutor', + label: `${plan.interval} Plan Checkout Started`, + }); + }, + }, + ); + return; + } + setIsUpdatingPlan(true); + }; + return ( -
e.stopPropagation()}> - {errorContent} +
+ {/* Close button */} + - {loader} - {!isLoading && !error && ( -
-
-

- Unlock Premium Features -

-

- Supercharge your learning experience with premium benefits -

-
- -
- {USER_SUBSCRIPTION_PLAN_PRICES.map((plan) => { - const isCurrentPlanSelected = - currentPlan?.priceId === plan.priceId; - const isYearly = plan.interval === 'year'; - - return ( -
-
-
-

- {isYearly ? 'Yearly Payment' : 'Monthly Payment'} -

- {isYearly && ( - - (2 months free) - - )} -
- {isYearly && ( - - Most Popular - - )} -
-
- {isYearly && ( -

- $ - {calculateYearlyPrice( - USER_SUBSCRIPTION_PLAN_PRICES[0].amount, - )} -

- )} -

- ${plan.amount}{' '} - - / {isYearly ? 'year' : 'month'} - -

-
- -
- -
- -
-
- ); - })} -
- - {/* Benefits Section */} -
- {PREMIUM_PERKS.map((perk, index) => { - const Icon = perk.icon; - return ( -
- -
-

- {perk.title} -

-

- {perk.description} -

-
-
- ); - })} -
+ {/* Header */} +
+
+
- )} +

+ Upgrade to Premium +

+

+ Unlock all features and supercharge your learning +

+
+ + {/* Features List */} +
+
+ {PREMIUM_PERKS.map((perk, index) => { + const Icon = perk.icon; + return ( +
+ +
+

+ {perk.title} +

+

{perk.description}

+
+
+ ); + })} +
+
+ + {/* Buttons */} +
+
+ {/* Yearly Button */} + {yearlyPlan && ( + + )} + + {/* Monthly Button */} + {monthlyPlan && ( + + )} +
+ + {/* Trust indicators */} +
+

+ By upgrading you agree to our terms and conditions +

+
+
); diff --git a/src/components/CustomRoadmap/PersonalRoadmapList.tsx b/src/components/CustomRoadmap/PersonalRoadmapList.tsx index 515aecc68..80a8f0249 100644 --- a/src/components/CustomRoadmap/PersonalRoadmapList.tsx +++ b/src/components/CustomRoadmap/PersonalRoadmapList.tsx @@ -23,11 +23,19 @@ import { RoadmapIcon } from '../ReactIcons/RoadmapIcon.tsx'; type PersonalRoadmapListType = { roadmaps: GetRoadmapListResponse['personalRoadmaps']; onDelete: (roadmapId: string) => void; + onUpgrade: () => void; setAllRoadmaps: Dispatch>; + maxLimit?: number; }; export function PersonalRoadmapList(props: PersonalRoadmapListType) { - const { roadmaps: roadmapList, onDelete, setAllRoadmaps } = props; + const { + roadmaps: roadmapList, + onDelete, + setAllRoadmaps, + onUpgrade, + maxLimit = -1, + } = props; const toast = useToast(); @@ -105,10 +113,19 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) { return (
{shareSettingsModal} -
- - {roadmapList.length} custom roadmap(s) - +
+ {maxLimit === -1 && <>{roadmapList.length} custom roadmap(s)} + {maxLimit !== -1 && ( + <> + {roadmapList.length} of {maxLimit} roadmaps{' '} + + + )}
    {roadmapList.map((roadmap) => { @@ -145,7 +162,7 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) { key={roadmap._id!} >
    -

    +

    {roadmap.title}

    @@ -235,7 +252,7 @@ function VisibilityBadge(props: VisibilityLabelProps) { return (
    diff --git a/src/components/CustomRoadmap/RoadmapListPage.tsx b/src/components/CustomRoadmap/RoadmapListPage.tsx index 1e8354a15..859795eef 100644 --- a/src/components/CustomRoadmap/RoadmapListPage.tsx +++ b/src/components/CustomRoadmap/RoadmapListPage.tsx @@ -9,6 +9,8 @@ import { PersonalRoadmapList } from './PersonalRoadmapList'; import { useToast } from '../../hooks/use-toast'; import { SharedRoadmapList } from './SharedRoadmapList'; import type { FriendshipStatus } from '../Befriend'; +import { useIsPaidUser } from '../../queries/billing'; +import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal'; export type FriendUserType = { id: string; @@ -37,11 +39,14 @@ const tabTypes: TabType[] = [ { label: 'Shared by Friends', value: 'shared' }, ]; +const MAX_ROADMAP_LIMIT = 3; + export function RoadmapListPage() { const toast = useToast(); const [isLoading, setIsLoading] = useState(true); const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false); + const [showUpgradeModal, setShowUpgradeModal] = useState(false); const [activeTab, setActiveTab] = useState('personal'); const [allRoadmaps, setAllRoadmaps] = useState({ @@ -49,10 +54,12 @@ export function RoadmapListPage() { sharedRoadmaps: [], }); + const { isPaidUser, isLoading: isLoadingIsPaidUser } = useIsPaidUser(); + async function loadRoadmapList() { setIsLoading(true); const { response, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-user-roadmap-list` + `${import.meta.env.PUBLIC_API_URL}/v1-get-user-roadmap-list`, ); if (error || !response) { @@ -65,7 +72,7 @@ export function RoadmapListPage() { response! || { personalRoadmaps: [], sharedRoadmaps: [], - } + }, ); } @@ -80,20 +87,27 @@ export function RoadmapListPage() { return null; } + const totalRoadmaps = allRoadmaps.personalRoadmaps.length; + const hasCrossedLimit = !isPaidUser && totalRoadmaps >= MAX_ROADMAP_LIMIT; + return (
    {isCreatingRoadmap && ( setIsCreatingRoadmap(false)} /> )} + {showUpgradeModal && ( + setShowUpgradeModal(false)} /> + )} +
    {tabTypes.map((tab) => { return (
    @@ -113,13 +135,17 @@ export function RoadmapListPage() {
    {activeTab === 'personal' && ( setShowUpgradeModal(true)} onDelete={(roadmapId) => { setAllRoadmaps({ ...allRoadmaps, personalRoadmaps: allRoadmaps.personalRoadmaps.filter( - (r) => r._id !== roadmapId + (r) => r._id !== roadmapId, ), }); }} diff --git a/src/components/UserPublicProfile/UserPublicProfileHeader.tsx b/src/components/UserPublicProfile/UserPublicProfileHeader.tsx index 794e8abca..a492eeb88 100644 --- a/src/components/UserPublicProfile/UserPublicProfileHeader.tsx +++ b/src/components/UserPublicProfile/UserPublicProfileHeader.tsx @@ -80,6 +80,7 @@ export function UserLink(props: UserLinkProps) { target="_blank" href={href} className="flex h-6 w-6 items-center justify-center rounded-md border" + rel="nofollow noopener noreferrer" > diff --git a/src/pages/u/[username].astro b/src/pages/u/[username].astro index c536240d0..67d9c3a40 100644 --- a/src/pages/u/[username].astro +++ b/src/pages/u/[username].astro @@ -27,12 +27,15 @@ if (error || !userDetails) { const projectDetails = await getProjectList(); const origin = Astro.url.origin; const ogImage = `${origin}/og/user/${username}`; + +const hasAnyRoadmaps = (userDetails?.roadmaps || []).length > 0; --- { !errorMessage && (