Alaguraj0361 df5f593369
Some checks failed
CI / ci (push) Has been cancelled
CI / docker-build (push) Has been cancelled
Publish Docker image / docker (push) Has been cancelled
Upload sourcemaps / upload (push) Has been cancelled
add billing and subscription management system with Stripe integration
2026-10-17 19:40:15 +05:30

382 lines
15 KiB
TypeScript

import * as React from "react";
import {
Check,
X,
Sparkles,
Zap,
ArrowRight,
Box,
Search,
} from "lucide-react";
import {
CRAWLERX_PLANS,
ADD_ON_PACKS,
TRUST_BADGES,
getPlanCtaLabel,
} from "./PricingData";
import type { Currency, BillingInterval, PricingPlan } from "./PricingData";
import {
Floating3DChartCard,
Floating3DSearchCard,
DotGridMatrix,
renderPlanIcon,
renderTrustIcon,
} from "./PricingGraphics";
export function PricingTable({
onSelectPlan,
isProcessing,
loadingPlanId,
currentPlanId = "free",
}: {
onSelectPlan?: (plan: PricingPlan, interval: BillingInterval, currency: Currency) => void;
isProcessing?: boolean;
loadingPlanId?: string | null;
currentPlanId?: string;
}) {
const [currency, setCurrency] = React.useState<Currency>("INR");
const [interval, setInterval] = React.useState<BillingInterval>("monthly");
return (
<div className="relative w-full space-y-12">
<DotGridMatrix className="hidden lg:block absolute -top-8 left-4 text-slate-300 opacity-60" />
<DotGridMatrix className="hidden lg:block absolute -top-8 right-6 text-slate-300 opacity-60" />
<Floating3DChartCard className="hidden xl:block absolute -top-12 -left-6 2xl:-left-16 z-10" />
<Floating3DSearchCard className="hidden xl:block absolute -top-8 -right-6 2xl:-right-14 z-10" />
<PricingHero
currency={currency}
interval={interval}
onCurrencyChange={setCurrency}
onIntervalChange={setInterval}
/>
<div
id="pricing-plans"
className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 sm:gap-7 items-stretch relative z-10 scroll-mt-8"
>
{CRAWLERX_PLANS.map((plan) => (
<PricingCard
key={plan.id}
plan={plan}
currency={currency}
interval={interval}
currentPlanId={currentPlanId}
isProcessing={isProcessing}
loadingPlanId={loadingPlanId}
onSelectPlan={onSelectPlan}
/>
))}
</div>
<PricingAddOns currency={currency} />
<PricingTrustBadges />
</div>
);
}
function PricingHero({
currency,
interval,
onCurrencyChange,
onIntervalChange,
}: {
currency: Currency;
interval: BillingInterval;
onCurrencyChange: (c: Currency) => void;
onIntervalChange: (i: BillingInterval) => void;
}) {
return (
<div className="text-center space-y-4 max-w-3xl mx-auto relative z-10">
<div className="inline-flex items-center gap-2 px-3.5 py-1 rounded-full bg-[#f0f6fc] border border-[#d6e4f3] text-[#3779b9] text-xs font-semibold tracking-wide">
<Sparkles className="size-3.5 text-[#3779b9]" />
<span>Transparent • Fair Pricing</span>
</div>
<h1 className="text-3xl sm:text-5xl font-black tracking-tight text-slate-900 leading-tight">
Powerful SEO tools{" "}
<span className="bg-gradient-to-r from-[#3779b9] via-[#2563eb] to-[#4faede] bg-clip-text text-transparent">
for real growth.
</span>
</h1>
<p className="text-slate-500 text-sm sm:text-base leading-relaxed max-w-2xl mx-auto">
Everything you need to audit, fix, track and grow — at a fraction of the cost of Semrush.
</p>
<div className="pt-2 flex flex-wrap items-center justify-center gap-3 sm:gap-5">
<div className="inline-flex items-center p-1 rounded-full bg-white border border-slate-200 shadow-xs text-xs font-semibold">
<button
type="button"
className={`px-3.5 py-1.5 rounded-full transition-all cursor-pointer flex items-center gap-1.5 ${
currency === "INR"
? "bg-slate-100 text-[#0f1d34] font-bold"
: "text-slate-500 hover:text-slate-900"
}`}
onClick={() => onCurrencyChange("INR")}
>
<span>🇮🇳</span>
<span>INR (₹)</span>
</button>
<button
type="button"
className={`px-3.5 py-1.5 rounded-full transition-all cursor-pointer flex items-center gap-1.5 ${
currency === "USD"
? "bg-slate-100 text-[#0f1d34] font-bold"
: "text-slate-500 hover:text-slate-900"
}`}
onClick={() => onCurrencyChange("USD")}
>
<span>🇺🇸</span>
<span>USD ($)</span>
</button>
</div>
<div className="inline-flex items-center p-1 rounded-full bg-slate-100/90 border border-slate-200 shadow-xs text-xs font-semibold">
<button
type="button"
className={`px-4 py-1.5 rounded-full transition-all cursor-pointer ${
interval === "monthly"
? "bg-[#3779b9] text-white shadow-xs font-bold"
: "text-slate-600 hover:text-slate-900"
}`}
onClick={() => onIntervalChange("monthly")}
>
Monthly
</button>
<button
type="button"
className={`px-3.5 py-1.5 rounded-full transition-all flex items-center gap-2 cursor-pointer ${
interval === "annual"
? "bg-[#3779b9] text-white shadow-xs font-bold"
: "text-slate-600 hover:text-slate-900"
}`}
onClick={() => onIntervalChange("annual")}
>
<span>Annual</span>
<span className="px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 text-[10px] font-bold">
7 Days Free Trial
</span>
</button>
</div>
</div>
</div>
);
}
function PricingCard({
plan,
currency,
interval,
currentPlanId,
isProcessing,
loadingPlanId,
onSelectPlan,
}: {
plan: PricingPlan;
currency: Currency;
interval: BillingInterval;
currentPlanId?: string;
isProcessing?: boolean;
loadingPlanId?: string | null;
onSelectPlan?: (plan: PricingPlan, interval: BillingInterval, currency: Currency) => void;
}) {
const isPro = plan.isPopular;
const isCurrent = currentPlanId === plan.id;
const cta = getPlanCtaLabel(plan, currentPlanId);
const price =
interval === "monthly"
? plan.monthlyPrice[currency]
: plan.annualMonthlyPrice[currency];
const isThisCardLoading = loadingPlanId ? loadingPlanId === plan.id : Boolean(isProcessing);
const isAnyPlanLoading = Boolean(loadingPlanId || isProcessing);
return (
<div
className={`relative flex flex-col justify-between rounded-[28px] p-7 sm:p-8 transition-all duration-300 ${
isPro
? "bg-gradient-to-b from-[#0f1d34] via-[#10274b] to-[#1e4a78] text-white border-2 border-[#4faede] shadow-[0_25px_60px_rgba(55,121,185,0.4),0_0_35px_rgba(79,174,222,0.25)] ring-4 ring-[#4faede]/20 -translate-y-2"
: "bg-white text-slate-900 border border-slate-200/90 hover:border-slate-300 hover:shadow-xl shadow-xs"
}`}
>
{isCurrent ? (
<div className="absolute -top-4 left-1/2 -translate-x-1/2 inline-flex items-center gap-1 px-3.5 py-1.5 bg-emerald-600 text-white font-black text-[10px] tracking-wider uppercase rounded-full shadow-md shadow-emerald-600/30 whitespace-nowrap z-20">
<Check className="size-3" strokeWidth={3} />
<span>YOUR ACTIVE PLAN</span>
</div>
) : isPro ? (
<div className="absolute -top-4 left-1/2 -translate-x-1/2 inline-flex items-center rounded-full shadow-md shadow-[#3779b9]/30 overflow-hidden whitespace-nowrap z-20">
<span className="px-3.5 py-1.5 bg-gradient-to-r from-[#3779b9] to-[#2563eb] text-white font-black text-[10px] tracking-wider uppercase flex items-center gap-1">
👑 MOST POPULAR
</span>
<span className="px-3.5 py-1.5 bg-[#0f1d34] text-[#4faede] font-extrabold text-[10px] tracking-wider uppercase">
76% AGENCY CHOICE
</span>
</div>
) : null}
<div>
<div className="flex items-center justify-between gap-3 mb-5">
<div
className={`size-12 rounded-2xl flex items-center justify-center shrink-0 ${
isPro
? "bg-gradient-to-tr from-[#3779b9] to-[#4faede] text-white shadow-lg shadow-[#3779b9]/30"
: "bg-[#f0f6fc] border border-[#d6e4f3] text-[#3779b9]"
}`}
>
{renderPlanIcon(plan.iconType)}
</div>
<span
className={`text-[11px] font-bold tracking-wider px-3.5 py-1.5 rounded-full uppercase whitespace-nowrap shrink-0 ${
isPro
? "bg-white/10 text-[#4faede] border border-[#4faede]/30"
: "bg-[#f0f6fc] text-[#3779b9]"
}`}
>
{plan.categoryTag}
</span>
</div>
<h3 className={`text-3xl font-extrabold tracking-tight ${isPro ? "text-white" : "text-slate-900"}`}>
{plan.name}
</h3>
<p className={`text-xs mt-1.5 font-medium min-h-[32px] ${isPro ? "text-slate-200" : "text-slate-500"}`}>
{plan.subhead}
</p>
<div className={`mt-5 mb-6 pb-6 border-b ${isPro ? "border-white/15" : "border-slate-100"}`}>
<div className="flex items-baseline gap-1">
<span className={`text-4xl sm:text-[42px] font-black tracking-tight ${isPro ? "text-white" : "text-slate-900"}`}>
{currency === "INR" ? `₹${price.toLocaleString("en-IN")}` : `$${price}`}
</span>
<span className={`text-xs font-semibold ${isPro ? "text-blue-200" : "text-slate-500"}`}>
/month
</span>
</div>
</div>
<ul className="space-y-3 mb-8 text-[13px]">
{plan.features.map((feature) => (
<li
key={feature.name}
className={`flex items-start gap-2.5 ${
isPro
? "text-white font-medium"
: feature.included
? "text-slate-700"
: "text-slate-400 opacity-60"
}`}
>
{feature.included ? (
<Check
className={`size-4 shrink-0 mt-0.5 ${isPro ? "text-[#4faede]" : "text-[#3779b9]"}`}
strokeWidth={2.5}
/>
) : (
<X className="size-4 text-slate-300 shrink-0 mt-0.5" strokeWidth={2} />
)}
<span>{feature.name}</span>
</li>
))}
</ul>
</div>
<div className="pt-2">
<button
type="button"
disabled={isAnyPlanLoading || isCurrent}
onClick={() => onSelectPlan?.(plan, interval, currency)}
className={`w-full h-12 rounded-2xl font-bold text-sm transition-all duration-200 flex items-center justify-center gap-2 cursor-pointer disabled:cursor-not-allowed ${
isCurrent
? "bg-emerald-50 text-emerald-800 border-2 border-emerald-300 cursor-default"
: isPro
? "bg-gradient-to-r from-[#3779b9] via-[#2563eb] to-[#4faede] hover:from-[#2e68a3] hover:to-[#38bdf8] text-white shadow-xl shadow-[#3779b9]/30 border border-white/20 active:scale-[0.99]"
: cta.actionType === "downgrade"
? "bg-white hover:bg-slate-50 text-slate-700 border-2 border-slate-200 active:scale-[0.99]"
: plan.id === "starter"
? "bg-[#0f1d34] hover:bg-[#1a2e4e] text-white shadow-md shadow-[#0f1d34]/20 active:scale-[0.99]"
: "bg-white hover:bg-[#f6f8fa] text-[#0f1d34] border-2 border-[#e6eaef] hover:border-[#3779b9]/40 active:scale-[0.99]"
}`}
>
{isThisCardLoading && !isCurrent ? (
<span className="loading loading-spinner loading-xs" />
) : (
<>
{isCurrent ? <Check className="size-4 text-emerald-600" /> : null}
<span>{cta.label}</span>
{!isCurrent ? <ArrowRight className="size-4" /> : null}
</>
)}
</button>
</div>
</div>
);
}
function PricingAddOns({ currency }: { currency: Currency }) {
return (
<div className="rounded-[28px] border border-slate-200/90 bg-white/95 backdrop-blur-md p-6 sm:p-7 shadow-xs relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-center">
<div className="flex items-start gap-4">
<div className="size-12 rounded-2xl bg-[#f0f6fc] border border-[#d6e4f3] text-[#3779b9] flex items-center justify-center shrink-0">
<Zap className="size-6 text-[#3779b9]" />
</div>
<div>
<h4 className="text-base font-bold text-slate-900">
Need Extra Capacity? Add-on Packs
</h4>
<p className="text-xs text-slate-500 mt-0.5 leading-relaxed">
Top-up quota anytime without moving to a higher tier. Capped and transparent.
</p>
</div>
</div>
{ADD_ON_PACKS.map((addon) => (
<div
key={addon.id}
className="p-4 rounded-2xl bg-[#f8fafc] border border-slate-200/80 flex items-center justify-between gap-4 hover:bg-slate-50 transition-colors"
>
<div className="flex items-center gap-3.5">
<div className="size-10 rounded-xl bg-white border border-[#d6e4f3] flex items-center justify-center text-[#3779b9] font-bold shrink-0 shadow-2xs">
{addon.id === "keywords" ? <Box className="size-4.5" /> : <Search className="size-4.5" />}
</div>
<div>
<div className="text-xs font-bold text-slate-900">{addon.name}</div>
<div className="text-[11px] text-slate-500 mt-0.5 line-clamp-1">{addon.description}</div>
</div>
</div>
<div className="text-right shrink-0">
<span className="text-xs font-bold text-[#3779b9]">
{currency === "INR" ? addon.inr : addon.usd} →
</span>
</div>
</div>
))}
</div>
</div>
);
}
function PricingTrustBadges() {
return (
<div className="rounded-[28px] border border-slate-200/90 bg-white/95 backdrop-blur-md p-6 shadow-xs relative z-10">
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 divide-y md:divide-y-0 md:divide-x divide-slate-100 text-left">
{TRUST_BADGES.map((badge, idx) => (
<div
key={badge.id}
className={`flex items-start gap-3.5 ${idx !== 0 ? "pt-3 md:pt-0 md:pl-5" : ""}`}
>
<div className="size-9 rounded-xl bg-[#f0f6fc] border border-[#d6e4f3] text-[#3779b9] flex items-center justify-center shrink-0 mt-0.5">
{renderTrustIcon(badge.id)}
</div>
<div>
<div className="text-xs font-bold text-slate-900">{badge.title}</div>
<div className="text-[11px] text-slate-400 mt-0.5 leading-snug">{badge.desc}</div>
</div>
</div>
))}
</div>
</div>
);
}