Files
netrunner-landing/components/sections/Pricing.tsx
T
2026-07-02 23:13:24 +07:00

291 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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";
import { cn } from "@/lib/utils";
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;
}
/**
* Секция тарифов. Тарифные планы приходят из PocketBase (`pricing_{lang}`) с ценами
* сразу на три периода (price1m/price6m/price1y) — компонент на клиенте пересчитывает
* "цену в месяц" для выбранного периода и подсвечивает скидку, если помесячная цена
* при длинном периоде ниже базовой месячной ставки.
*/
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={cn(
"relative flex flex-col h-full transition-all duration-300",
"cursor-net-grab active:cursor-net-grabbing", // <-- Новые курсоры
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>
);
}