280 lines
10 KiB
TypeScript
280 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import {
|
|
Card,
|
|
CardHeader,
|
|
CardTitle,
|
|
CardDescription,
|
|
CardContent,
|
|
CardFooter,
|
|
} from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
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 isRu = lang === "ru";
|
|
const currencySign = isRu ? "₽" : "$";
|
|
|
|
const periods: PeriodOption[] = [
|
|
{ id: "1m", label: isRu ? "1 месяц" : "1 month" },
|
|
{ id: "6m", label: isRu ? "6 месяцев" : "6 months" },
|
|
{
|
|
id: "1y",
|
|
label: isRu ? "1 год" : "1 year",
|
|
badge: isRu ? "Выгодно" : "Best Value",
|
|
},
|
|
];
|
|
|
|
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"
|
|
>
|
|
{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)}
|
|
// Исправлено: фиксированная высота h-14, flex-col, элементы по центру
|
|
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/70"
|
|
: "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">
|
|
Syncing_Nodes...
|
|
</span>
|
|
</div>
|
|
) : plans.length === 0 ? (
|
|
<p className="text-center text-muted-foreground font-mono">
|
|
0 uplinks found.
|
|
</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 originalTotalPrice = plan.price1m * months;
|
|
const isDiscounted = totalPrice < originalTotalPrice;
|
|
const monthlyEquivalent = Math.round(totalPrice / months);
|
|
const formatPrice = (price: number) =>
|
|
isRu ? 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-2xl shadow-primary/10 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-xs font-bold tracking-widest shadow-lg shadow-primary/30">
|
|
{isRu ? "Рекомендуем" : "Recommended"}
|
|
</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>
|
|
{/* Описание: запас на 3 строки текста */}
|
|
<CardDescription className="mt-4 text-sm leading-relaxed min-h-[80px]">
|
|
{plan.description}
|
|
</CardDescription>
|
|
{/* Блок цены: жестко привязан к позиции */}
|
|
<div className="mt-8 mb-6">
|
|
<div className="flex items-center gap-2 h-6 mb-1">
|
|
{isDiscounted ? (
|
|
<>
|
|
<span className="text-sm text-muted-foreground/50 line-through">
|
|
{formatPrice(originalTotalPrice)}
|
|
{currencySign}
|
|
</span>
|
|
<Badge
|
|
variant="secondary"
|
|
className="bg-green-500/10 text-green-500 border-none text-[10px] px-2 py-0"
|
|
>
|
|
{Math.round(
|
|
(1 - totalPrice / originalTotalPrice) * 100,
|
|
)}
|
|
% OFF
|
|
</Badge>
|
|
</>
|
|
) : (
|
|
<div className="h-6" /> // Spacer
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-baseline gap-1">
|
|
<span className="text-5xl font-black tracking-tighter text-foreground">
|
|
{isRu ? "" : currencySign}
|
|
{formatPrice(monthlyEquivalent)}
|
|
{isRu ? currencySign : ""}
|
|
</span>
|
|
<span className="text-muted-foreground text-sm font-bold">
|
|
/mo
|
|
</span>
|
|
</div>
|
|
|
|
<div className="h-5 mt-1 text-[10px] font-mono text-muted-foreground/60 uppercase tracking-wider">
|
|
{period !== "1m" &&
|
|
`${isRu ? "Total billed:" : "Total billed:"} ${formatPrice(totalPrice)}${currencySign}`}
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
{/* flex-1 заставляет контент расширяться, толкая футер вниз */}
|
|
<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">
|
|
{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>
|
|
);
|
|
}
|