282 lines
10 KiB
TypeScript
282 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import {
|
|
Card,
|
|
CardHeader,
|
|
CardTitle,
|
|
CardDescription,
|
|
CardContent,
|
|
CardFooter,
|
|
} from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { CheckIcon, Loader2 } from "lucide-react";
|
|
import { Dictionary } from "@/lib/IDict";
|
|
import { pb } from "@/lib/pb";
|
|
import { CyberButton } from "../ui/cyber-button";
|
|
|
|
interface PBPricingPlan {
|
|
id: string;
|
|
name: string;
|
|
level: string;
|
|
tagline: string;
|
|
description: string;
|
|
price1m: number;
|
|
price6m: number;
|
|
price1y: number;
|
|
features: string;
|
|
popular: boolean;
|
|
cta: string;
|
|
lang: string;
|
|
order: number;
|
|
}
|
|
|
|
type Period = "1m" | "6m" | "1y";
|
|
|
|
interface PeriodOption {
|
|
id: Period;
|
|
label: string;
|
|
badge?: string;
|
|
}
|
|
|
|
export function Pricing({
|
|
dict,
|
|
lang,
|
|
}: {
|
|
dict: Dictionary["pricing"];
|
|
lang: string;
|
|
}) {
|
|
const [period, setPeriod] = useState<Period>("1y");
|
|
const [plans, setPlans] = useState<PBPricingPlan[]>([]);
|
|
const [loading, setLoading] = useState(!!lang);
|
|
|
|
useEffect(() => {
|
|
if (!lang) return;
|
|
async function fetchPlans() {
|
|
try {
|
|
setLoading(true);
|
|
const records = await pb
|
|
.collection(`pricing_${lang}`)
|
|
.getFullList<PBPricingPlan>({
|
|
sort: "order",
|
|
requestKey: null,
|
|
});
|
|
setPlans(records);
|
|
} catch (error) {
|
|
console.error("Ошибка загрузки тарифов:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
fetchPlans();
|
|
}, [lang]);
|
|
|
|
const currencySign = lang === "ru" ? "₽" : "$";
|
|
|
|
const periods: PeriodOption[] = [
|
|
{ id: "1m", label: lang === "ru" ? "1 месяц" : "1 month" },
|
|
{ id: "6m", label: lang === "ru" ? "6 месяцев" : "6 months" },
|
|
{
|
|
id: "1y",
|
|
label: lang === "ru" ? "1 год" : "1 year",
|
|
badge: dict.recommend,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<section
|
|
id="pricing"
|
|
className="py-32 px-4 container mx-auto max-w-7xl relative min-h-[600px]"
|
|
>
|
|
<div className="absolute inset-0 bg-[radial-gradient(circle_at_bottom_left,_var(--tw-gradient-stops))] from-secondary/10 via-transparent to-transparent -z-10" />
|
|
|
|
<div className="text-center mb-20 flex flex-col items-center">
|
|
<Badge
|
|
variant="outline"
|
|
className="mb-4 border-primary/30 text-primary uppercase tracking-widest font-mono text-[10px]"
|
|
>
|
|
{dict.badge}
|
|
</Badge>
|
|
<h2 className="text-4xl md:text-5xl font-bold tracking-tighter mb-8">
|
|
{dict.title}
|
|
</h2>
|
|
|
|
<div className="inline-flex items-center justify-center p-1.5 bg-card/50 border border-border/50 rounded-2xl backdrop-blur-sm relative">
|
|
{periods.map((p: PeriodOption) => (
|
|
<button
|
|
key={p.id}
|
|
onClick={() => setPeriod(p.id)}
|
|
className={`relative flex flex-col items-center justify-center gap-0.5 px-6 min-w-[120px] h-14 text-sm font-bold rounded-xl transition-all duration-300 ${
|
|
period === p.id
|
|
? "bg-primary text-primary-foreground shadow-lg shadow-primary/25"
|
|
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
|
}`}
|
|
>
|
|
<span>{p.label}</span>
|
|
{p.badge && (
|
|
<span
|
|
className={`text-[9px] uppercase tracking-widest italic leading-none font-black ${
|
|
period === p.id
|
|
? "text-primary-foreground/80"
|
|
: "text-primary/70"
|
|
}`}
|
|
>
|
|
{p.badge}
|
|
</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex flex-col items-center justify-center py-20 text-primary opacity-50">
|
|
<Loader2 className="animate-spin mb-4" />
|
|
<span className="text-sm uppercase font-mono tracking-widest">
|
|
{dict.syncing}
|
|
</span>
|
|
</div>
|
|
) : plans.length === 0 ? (
|
|
<p className="text-center text-muted-foreground font-mono">
|
|
{dict.empty}
|
|
</p>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 items-stretch">
|
|
{plans.map((plan) => {
|
|
const months = period === "1m" ? 1 : period === "6m" ? 6 : 12;
|
|
const totalPrice =
|
|
period === "1m"
|
|
? plan.price1m
|
|
: period === "6m"
|
|
? plan.price6m
|
|
: plan.price1y;
|
|
const baseMonthlyPrice = plan.price1m;
|
|
const monthlyEquivalent = Math.round(totalPrice / months);
|
|
const isDiscounted = monthlyEquivalent < baseMonthlyPrice;
|
|
const formatPrice = (price: number) =>
|
|
lang === "ru" ? price.toLocaleString("ru-RU") : price.toString();
|
|
|
|
return (
|
|
<Card
|
|
key={plan.id}
|
|
className={`relative flex flex-col h-full transition-all duration-300 ${
|
|
plan.popular
|
|
? "border-primary/50 shadow-[0_0_30px_-10px_rgba(var(--primary),0.3)] scale-[1.02] z-10"
|
|
: "border-border/50"
|
|
}`}
|
|
>
|
|
{plan.popular && (
|
|
<div className="absolute top-0 right-10 -translate-y-1/2">
|
|
<Badge className="bg-primary text-primary-foreground rounded-full px-4 py-1 uppercase text-[10px] font-black tracking-widest shadow-lg shadow-primary/30">
|
|
{dict.recommend}
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
|
|
<CardHeader className="pb-0 flex-none">
|
|
<div className="text-[10px] font-bold text-primary uppercase tracking-[0.2em] mb-2 h-4">
|
|
{plan.level}
|
|
</div>
|
|
<CardTitle className="text-3xl font-black text-foreground uppercase tracking-tight h-10 flex items-center">
|
|
{plan.name}
|
|
</CardTitle>
|
|
<div className="text-sm font-medium italic text-muted-foreground mt-4 border-l-2 border-primary/30 pl-3 min-h-[40px] flex items-center">
|
|
«{plan.tagline}»
|
|
</div>
|
|
<CardDescription className="mt-4 text-sm leading-relaxed min-h-[60px]">
|
|
{plan.description}
|
|
</CardDescription>
|
|
|
|
<div className="mt-6 mb-4 flex flex-col min-h-[110px] justify-center">
|
|
<div className="flex items-center gap-2 h-6 mb-1">
|
|
{isDiscounted && (
|
|
<>
|
|
<span className="text-sm text-muted-foreground/50 line-through decoration-primary/50 font-mono">
|
|
{lang === "ru" ? "" : currencySign}
|
|
{formatPrice(baseMonthlyPrice)}
|
|
{lang === "ru" ? currencySign : ""}
|
|
</span>
|
|
<Badge
|
|
variant="outline"
|
|
className="bg-primary/10 text-primary border-primary/30 text-[10px] px-2 py-0.5 font-bold tracking-wider"
|
|
>
|
|
{dict.discount}{" "}
|
|
{Math.round(
|
|
(1 - monthlyEquivalent / baseMonthlyPrice) * 100,
|
|
)}
|
|
%
|
|
</Badge>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="flex items-baseline gap-1">
|
|
<span
|
|
className={`text-5xl font-black tracking-tighter font-mono ${plan.popular ? "text-foreground drop-shadow-[0_0_15px_rgba(var(--primary),0.3)]" : "text-foreground"}`}
|
|
>
|
|
{lang === "ru" ? "" : currencySign}
|
|
{formatPrice(monthlyEquivalent)}
|
|
</span>
|
|
{lang === "ru" && (
|
|
<span className="text-3xl font-bold ml-1 text-muted-foreground">
|
|
{currencySign}
|
|
</span>
|
|
)}
|
|
<span className="text-muted-foreground text-sm font-bold ml-2">
|
|
{dict.mo}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="h-8 mt-4 flex items-center">
|
|
{period !== "1m" && (
|
|
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-muted/40 border border-border/50 text-[11px] text-muted-foreground font-mono uppercase tracking-wider">
|
|
<span>{dict.billedOnce}</span>
|
|
<span className="font-bold text-foreground">
|
|
{lang === "ru" ? "" : currencySign}
|
|
{formatPrice(totalPrice)}
|
|
{lang === "ru" ? currencySign : ""}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="flex-1 pb-8">
|
|
<ul className="space-y-4 pt-6 border-t border-border/20">
|
|
{plan.features
|
|
.split("\n")
|
|
.filter((f) => f.trim())
|
|
.map((feature, i) => (
|
|
<li
|
|
key={i}
|
|
className="flex items-start gap-3 text-sm text-foreground/80 group"
|
|
>
|
|
<div className="bg-primary/10 w-5 h-5 rounded-full flex items-center justify-center mt-0.5 shrink-0 transition-colors group-hover:bg-primary/20">
|
|
<CheckIcon className="w-3 h-3 text-primary stroke-[3]" />
|
|
</div>
|
|
<span className="leading-tight font-medium">
|
|
{feature.trim()}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</CardContent>
|
|
|
|
<CardFooter className="pt-0">
|
|
<CyberButton
|
|
href="#"
|
|
variant={plan.popular ? "primary" : "secondary"}
|
|
className="w-full"
|
|
>
|
|
{plan.cta}
|
|
</CyberButton>
|
|
</CardFooter>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|