382 lines
15 KiB
TypeScript
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>
|
|
);
|
|
}
|