274 lines
12 KiB
TypeScript
274 lines
12 KiB
TypeScript
"use client"
|
||||
|
|
|
|||
|
|
import { useState } from "react"
|
|||
|
|
import Link from "next/link"
|
|||
|
|
import { motion } from "framer-motion"
|
|||
|
|
import { Check, X, Zap, ShieldCheck } from "lucide-react"
|
|||
|
|
import { cn } from "@/lib/utils"
|
|||
|
|
|
|||
|
|
const PLANS = [
|
|||
|
|
{
|
|||
|
|
key: "starter",
|
|||
|
|
name: "Starter",
|
|||
|
|
monthlyPrice: 0,
|
|||
|
|
description: "For landlords just getting started.",
|
|||
|
|
highlight: false,
|
|||
|
|
cta: "Get started free",
|
|||
|
|
ctaHref: "/signup",
|
|||
|
|
features: ["1 property", "Up to 3 tenants", "100 MB storage", "Rent tracking", "Maintenance portal", "Lease tracking", "Tenant portal"],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "pro",
|
|||
|
|
name: "Pro",
|
|||
|
|
monthlyPrice: 29,
|
|||
|
|
description: "For active landlords growing their portfolio.",
|
|||
|
|
highlight: true,
|
|||
|
|
badge: "Most popular",
|
|||
|
|
cta: "Start Pro",
|
|||
|
|
ctaHref: "/signup",
|
|||
|
|
features: ["10 properties", "Unlimited tenants", "5 GB storage", "50 AI calls/mo", "Email reminders", "Stripe payment links", "Lease expiry alerts", "CSV export"],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "landlord",
|
|||
|
|
name: "Landlord",
|
|||
|
|
monthlyPrice: 59,
|
|||
|
|
description: "For serious portfolios at scale.",
|
|||
|
|
highlight: false,
|
|||
|
|
cta: "Start Landlord",
|
|||
|
|
ctaHref: "/signup",
|
|||
|
|
features: ["Unlimited properties", "Unlimited tenants", "25 GB storage", "200 AI calls/mo", "Team access", "White-label portal", "Priority support"],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
key: "lifetime",
|
|||
|
|
name: "Lifetime",
|
|||
|
|
monthlyPrice: null,
|
|||
|
|
fixedPrice: 199,
|
|||
|
|
description: "Pay once, own it forever.",
|
|||
|
|
highlight: false,
|
|||
|
|
badge: "Best value",
|
|||
|
|
cta: "Get lifetime access",
|
|||
|
|
ctaHref: "/signup",
|
|||
|
|
features: ["Everything in Landlord", "No recurring fees — ever", "All future updates", "White-label portal", "Team access", "25 GB storage"],
|
|||
|
|
},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const COMPARISON = [
|
|||
|
|
{ feature: "Properties", starter: "1", pro: "10", landlord: "Unlimited", lifetime: "Unlimited" },
|
|||
|
|
{ feature: "Tenants", starter: "3", pro: "Unlimited", landlord: "Unlimited", lifetime: "Unlimited" },
|
|||
|
|
{ feature: "Storage", starter: "100 MB", pro: "5 GB", landlord: "25 GB", lifetime: "25 GB" },
|
|||
|
|
{ feature: "AI calls / month", starter: false, pro: "50", landlord: "200", lifetime: "200" },
|
|||
|
|
{ feature: "Email reminders", starter: false, pro: true, landlord: true, lifetime: true },
|
|||
|
|
{ feature: "Stripe payment links", starter: false, pro: true, landlord: true, lifetime: true },
|
|||
|
|
{ feature: "Team access", starter: false, pro: false, landlord: true, lifetime: true },
|
|||
|
|
{ feature: "White-label portal", starter: false, pro: false, landlord: true, lifetime: true },
|
|||
|
|
{ feature: "CSV export", starter: true, pro: true, landlord: true, lifetime: true },
|
|||
|
|
{ feature: "Lease expiry alerts", starter: true, pro: true, landlord: true, lifetime: true },
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
function Cell({ val }: { val: string | boolean }) {
|
|||
|
|
if (val === true) return <Check className="mx-auto h-4 w-4 text-emerald-400" />
|
|||
|
|
if (val === false) return <X className="mx-auto h-4 w-4 text-white/20" />
|
|||
|
|
return <span className="text-sm font-medium text-white/80">{val}</span>
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function PricingSection() {
|
|||
|
|
const [annual, setAnnual] = useState(false)
|
|||
|
|
const DISCOUNT = 0.8
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<section id="pricing" className="relative py-24 overflow-hidden">
|
|||
|
|
{/* Section background */}
|
|||
|
|
<div className="absolute inset-0 -z-10 bg-gradient-to-b from-transparent via-indigo-950/20 to-transparent" />
|
|||
|
|
<div className="absolute inset-0 -z-10" style={{
|
|||
|
|
backgroundImage: "radial-gradient(ellipse 80% 50% at 50% 0%, rgba(99,102,241,0.08) 0%, transparent 70%)"
|
|||
|
|
}} />
|
|||
|
|
|
|||
|
|
<div className="mx-auto max-w-6xl px-4 sm:px-6">
|
|||
|
|
{/* Header */}
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ opacity: 0, y: 20 }}
|
|||
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|||
|
|
viewport={{ once: true }}
|
|||
|
|
className="mb-12 text-center"
|
|||
|
|
>
|
|||
|
|
<p className="mb-3 text-xs font-semibold uppercase tracking-widest text-indigo-400">Pricing</p>
|
|||
|
|
<h2 className="text-3xl font-bold text-white sm:text-4xl">Simple, honest pricing</h2>
|
|||
|
|
<p className="mt-3 text-white/50">Start free, upgrade when you grow. No per-unit fees, ever.</p>
|
|||
|
|
|
|||
|
|
{/* Toggle */}
|
|||
|
|
<div className="mt-8 inline-flex items-center rounded-full border border-white/10 bg-white/[0.04] p-1.5">
|
|||
|
|
<button
|
|||
|
|
onClick={() => setAnnual(false)}
|
|||
|
|
className={cn(
|
|||
|
|
"rounded-full px-4 sm:px-5 py-1.5 sm:py-2 text-xs sm:text-sm font-medium transition-all duration-200",
|
|||
|
|
!annual ? "bg-indigo-600 text-white shadow-lg shadow-indigo-500/30" : "text-white/50 hover:text-white"
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
Monthly
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setAnnual(true)}
|
|||
|
|
className={cn(
|
|||
|
|
"flex items-center gap-1.5 sm:gap-2 rounded-full px-4 sm:px-5 py-1.5 sm:py-2 text-xs sm:text-sm font-medium transition-all duration-200",
|
|||
|
|
annual ? "bg-indigo-600 text-white shadow-lg shadow-indigo-500/30" : "text-white/50 hover:text-white"
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
Annual
|
|||
|
|
<span className="rounded-full bg-emerald-500/20 px-2 py-0.5 text-[10px] font-bold text-emerald-400 border border-emerald-500/30">
|
|||
|
|
–20%
|
|||
|
|
</span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</motion.div>
|
|||
|
|
|
|||
|
|
{/* Plan cards */}
|
|||
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4 items-start">
|
|||
|
|
{PLANS.map((plan, i) => {
|
|||
|
|
const isLifetime = plan.fixedPrice !== undefined
|
|||
|
|
const displayPrice = isLifetime
|
|||
|
|
? `$${plan.fixedPrice}`
|
|||
|
|
: plan.monthlyPrice === 0
|
|||
|
|
? "Free"
|
|||
|
|
: annual
|
|||
|
|
? `$${Math.round(plan.monthlyPrice! * DISCOUNT * 12)}`
|
|||
|
|
: `$${plan.monthlyPrice}`
|
|||
|
|
const interval = isLifetime
|
|||
|
|
? " one-time"
|
|||
|
|
: plan.monthlyPrice === 0
|
|||
|
|
? ""
|
|||
|
|
: annual ? "/yr" : "/mo"
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<motion.div
|
|||
|
|
key={plan.key}
|
|||
|
|
initial={{ opacity: 0, y: 24 }}
|
|||
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|||
|
|
viewport={{ once: true }}
|
|||
|
|
transition={{ delay: i * 0.08 }}
|
|||
|
|
className={cn(
|
|||
|
|
"relative flex flex-col rounded-2xl p-6 transition-all duration-300",
|
|||
|
|
plan.highlight
|
|||
|
|
? "border-2 border-indigo-500/60 bg-gradient-to-b from-indigo-600/15 to-[#16161f] shadow-2xl shadow-indigo-500/20 ring-1 ring-indigo-500/20"
|
|||
|
|
: "border border-white/10 bg-[#16161f] hover:border-white/20"
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
{plan.badge && (
|
|||
|
|
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2 z-10">
|
|||
|
|
<span className={cn(
|
|||
|
|
"flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-xs font-semibold shadow-lg",
|
|||
|
|
plan.highlight
|
|||
|
|
? "bg-indigo-600 text-white shadow-indigo-500/40"
|
|||
|
|
: "bg-[#1d1d28] border border-white/10 text-white/70"
|
|||
|
|
)}>
|
|||
|
|
<Zap className="h-3 w-3" />
|
|||
|
|
{plan.badge}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Plan name + price */}
|
|||
|
|
<div className="mb-6 pt-1">
|
|||
|
|
<p className={cn("text-xs font-semibold uppercase tracking-wide mb-2", plan.highlight ? "text-indigo-400" : "text-white/40")}>
|
|||
|
|
{plan.name}
|
|||
|
|
</p>
|
|||
|
|
<div className="flex items-baseline gap-1">
|
|||
|
|
<span className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight">{displayPrice}</span>
|
|||
|
|
{interval && <span className="text-sm text-white/40 ml-0.5">{interval}</span>}
|
|||
|
|
</div>
|
|||
|
|
{annual && !isLifetime && plan.monthlyPrice! > 0 && (
|
|||
|
|
<p className="mt-1 text-xs text-emerald-400 font-medium">
|
|||
|
|
Saves ${Math.round(plan.monthlyPrice! * 12 * 0.2)}/year
|
|||
|
|
</p>
|
|||
|
|
)}
|
|||
|
|
<p className="mt-2 text-xs text-white/40 leading-relaxed">{plan.description}</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* CTA */}
|
|||
|
|
<Link
|
|||
|
|
href={plan.ctaHref}
|
|||
|
|
className={cn(
|
|||
|
|
"mb-6 block rounded-xl px-4 py-2.5 text-center text-sm font-semibold transition-all",
|
|||
|
|
plan.highlight
|
|||
|
|
? "bg-indigo-600 text-white hover:bg-indigo-500 hover:shadow-lg hover:shadow-indigo-500/30"
|
|||
|
|
: "border border-white/10 bg-white/[0.04] text-white/70 hover:bg-white/[0.08] hover:text-white hover:border-white/20"
|
|||
|
|
)}
|
|||
|
|
>
|
|||
|
|
{plan.cta}
|
|||
|
|
</Link>
|
|||
|
|
|
|||
|
|
{/* Features */}
|
|||
|
|
<ul className="flex-1 space-y-2.5">
|
|||
|
|
{plan.features.map((f) => (
|
|||
|
|
<li key={f} className="flex items-start gap-2.5">
|
|||
|
|
<Check className={cn("mt-0.5 h-3.5 w-3.5 flex-shrink-0", plan.highlight ? "text-indigo-400" : "text-white/40")} />
|
|||
|
|
<span className="text-xs text-white/60 leading-relaxed">{f}</span>
|
|||
|
|
</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
</motion.div>
|
|||
|
|
)
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Money-back guarantee */}
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ opacity: 0 }}
|
|||
|
|
whileInView={{ opacity: 1 }}
|
|||
|
|
viewport={{ once: true }}
|
|||
|
|
className="mt-8 flex items-center justify-center gap-2 text-xs text-white/40"
|
|||
|
|
>
|
|||
|
|
<ShieldCheck className="h-4 w-4 text-emerald-400" />
|
|||
|
|
<span>30-day money-back guarantee on all paid plans. No questions asked.</span>
|
|||
|
|
</motion.div>
|
|||
|
|
|
|||
|
|
{/* Comparison table */}
|
|||
|
|
<motion.div
|
|||
|
|
initial={{ opacity: 0, y: 20 }}
|
|||
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|||
|
|
viewport={{ once: true }}
|
|||
|
|
transition={{ delay: 0.2 }}
|
|||
|
|
className="mt-16"
|
|||
|
|
>
|
|||
|
|
<h3 className="mb-6 text-center text-lg font-semibold text-white">Full feature comparison</h3>
|
|||
|
|
<div className="overflow-x-auto rounded-2xl border border-white/[0.08] bg-[#16161f]">
|
|||
|
|
<table className="w-full min-w-[600px]">
|
|||
|
|
<thead>
|
|||
|
|
<tr className="border-b border-white/[0.08]">
|
|||
|
|
<th className="px-5 py-4 text-left text-xs font-medium text-white/40 w-[40%]">Feature</th>
|
|||
|
|
{[
|
|||
|
|
{ name: "Starter", highlight: false },
|
|||
|
|
{ name: "Pro", highlight: true },
|
|||
|
|
{ name: "Landlord", highlight: false },
|
|||
|
|
{ name: "Lifetime", highlight: false },
|
|||
|
|
].map((h) => (
|
|||
|
|
<th key={h.name} className={cn(
|
|||
|
|
"px-4 py-4 text-center text-xs font-semibold",
|
|||
|
|
h.highlight ? "text-indigo-400" : "text-white/60"
|
|||
|
|
)}>
|
|||
|
|
{h.name}
|
|||
|
|
</th>
|
|||
|
|
))}
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
{COMPARISON.map((row, i) => (
|
|||
|
|
<tr key={row.feature} className={cn(
|
|||
|
|
"border-b border-white/[0.04] last:border-0",
|
|||
|
|
i % 2 === 0 ? "bg-transparent" : "bg-white/[0.015]"
|
|||
|
|
)}>
|
|||
|
|
<td className="px-5 py-3.5 text-sm text-white/60">{row.feature}</td>
|
|||
|
|
<td className="px-4 py-3.5 text-center"><Cell val={row.starter} /></td>
|
|||
|
|
<td className="px-4 py-3.5 text-center bg-indigo-500/[0.04]"><Cell val={row.pro} /></td>
|
|||
|
|
<td className="px-4 py-3.5 text-center"><Cell val={row.landlord} /></td>
|
|||
|
|
<td className="px-4 py-3.5 text-center"><Cell val={row.lifetime} /></td>
|
|||
|
|
</tr>
|
|||
|
|
))}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</motion.div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
)
|
|||
|
|
}
|