Files
zeropost-tool/app/plans/page.js
T
Ник (Claude) 1fbdc9f9b9 feat: unified admin panel + back buttons everywhere
AdminPanel.js: sidebar nav с 4 разделами (Настройки API, ЮKassa, Расходы AI, Пользователи)
  Встроены: SettingsSection (API-ключи), SpendingSection (расходы), AdminBilling
  Breadcrumb навигация
/system/page.js: теперь рендерит AdminPanel
Header: 'Расходы' → 'Админ' (ссылка на /system), убран TrendingUp
BackButton.js: переиспользуемая кнопка назад
  Добавлена на /drafts, /billing, /plans
2026-06-12 23:57:38 +03:00

166 lines
8.7 KiB
JavaScript
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 { Check, Zap, Loader2 } from 'lucide-react';
import Link from 'next/link';
import BackButton from '@/components/BackButton';
const PLAN_STYLE = {
free: { color: 'border-border', badge: null, btnClass: 'btn-ghost' },
starter: { color: 'border-blue-500/50', badge: null, btnClass: 'btn-primary' },
pro: { color: 'border-purple-500/60', badge: 'Популярный', btnClass: 'bg-purple-600 hover:bg-purple-500 text-white px-4 py-2 rounded-lg font-medium transition-colors' },
business: { color: 'border-yellow-500/40', badge: 'Для агентств', btnClass: 'bg-yellow-600 hover:bg-yellow-500 text-white px-4 py-2 rounded-lg font-medium transition-colors' },
};
const FEATURES = {
free: ['1 канал', '50 кредитов/мес', 'TG и VK публикация', 'Ручная генерация'],
starter: ['2 канала', '500 кредитов/мес', 'Автогенерация постов', 'Календарь публикаций', 'Аналитика канала'],
pro: ['5 каналов', '2000 кредитов/мес', 'Всё из Starter', 'Приоритетная генерация', 'История контента'],
business: ['Без ограничений', 'Безлимит кредитов', 'Всё из Pro', 'Поддержка 24/7', 'API доступ'],
};
export default function PlansPage() {
const [plans, setPlans] = useState([]);
const [costs, setCosts] = useState({});
const [balance, setBalance] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
Promise.all([
fetch('/api/billing/plans').then(r => r.json()),
fetch('/api/billing/balance').then(r => r.json()).catch(() => null),
]).then(([pd, bd]) => {
setPlans(pd.plans || []);
setCosts(Object.fromEntries((pd.costs || []).map(c => [c.operation, c.credits])));
setBalance(bd);
setLoading(false);
});
}, []);
if (loading) return (
<main className="max-w-5xl mx-auto p-6 text-center py-20">
<Loader2 className="w-6 h-6 animate-spin mx-auto text-accent" />
</main>
);
return (
<main className="max-w-5xl mx-auto p-4 sm:p-6">
<BackButton />
<div className="text-center mb-10">
<h1 className="text-3xl font-bold mb-2">Тарифы</h1>
<p className="text-gray-400">Выберите план под ваши задачи. Все планы включают публикацию в TG и VK.</p>
{balance && (
<p className="text-sm text-accent mt-2">
Сейчас у вас: <strong>{balance.planName}</strong> · {balance.isUnlimited ? '' : balance.credits} кредитов
</p>
)}
</div>
{/* Карточки планов */}
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-10">
{plans.map(plan => {
const style = PLAN_STYLE[plan.code] || PLAN_STYLE.free;
const features = FEATURES[plan.code] || [];
const isCurrent = balance?.plan === plan.code;
const isUnlimited = plan.credits_month === -1;
return (
<div key={plan.code} className={`card p-5 flex flex-col border-2 ${style.color} ${plan.code === 'pro' ? 'relative' : ''}`}>
{style.badge && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 text-xs px-3 py-1 rounded-full bg-purple-600 text-white font-medium whitespace-nowrap">
{style.badge}
</div>
)}
<div className="mb-4">
<div className="text-lg font-bold">{plan.name}</div>
<div className="mt-1">
{plan.price_rub === 0
? <span className="text-2xl font-bold">Бесплатно</span>
: <><span className="text-2xl font-bold">{plan.price_rub}</span><span className="text-gray-400 text-sm">/мес</span></>
}
</div>
<div className="text-sm text-accent mt-1 font-medium">
{isUnlimited ? '∞ кредитов' : `${plan.credits_month} кредитов/мес`}
</div>
</div>
<ul className="space-y-2 flex-1 mb-5">
{features.map(f => (
<li key={f} className="flex items-start gap-2 text-sm text-gray-300">
<Check className="w-4 h-4 text-green-400 mt-0.5 shrink-0" />
{f}
</li>
))}
</ul>
{isCurrent ? (
<div className="w-full text-center py-2 rounded-lg bg-surface2 text-gray-400 text-sm">Текущий план</div>
) : plan.price_rub === 0 ? (
<Link href="/register" className={`w-full text-center py-2 rounded-lg text-sm ${style.btnClass}`}>Начать бесплатно</Link>
) : (
<button className={`w-full text-center py-2 text-sm ${style.btnClass}`}
onClick={async () => {
try {
const res = await fetch('/api/billing/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan_code: plan.code }),
}).then(r => r.json());
if (res.confirmationUrl) window.location.href = res.confirmationUrl;
else alert(res.error || 'Ошибка создания платежа');
} catch { alert('Ошибка соединения'); }
}}>
Подключить
</button>
)}
</div>
);
})}
</div>
{/* Стоимость операций */}
<div className="card p-5 mb-6">
<h2 className="font-semibold mb-4 flex items-center gap-2">
<Zap className="w-4 h-4 text-accent" /> Стоимость генерации
</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 text-sm">
{[
{ label: 'Картинка', op: 'image', icon: '🖼', note: 'gpt-5-image-mini' },
{ label: 'Пост', op: 'text_post', icon: '✍️', note: 'aiprimetech' },
{ label: 'Статья', op: 'article', icon: '📝', note: 'Claude Sonnet' },
{ label: 'Публикация', op: 'autopublish',icon: '📤', note: 'TG / VK / MAX' },
].map(op => (
<div key={op.op} className="p-3 rounded-lg bg-surface2 text-center">
<div className="text-2xl mb-1">{op.icon}</div>
<div className="font-medium">{op.label}</div>
<div className="text-accent font-bold mt-1">
{(costs[op.op] || 0) === 0 ? 'бесплатно' : `${costs[op.op]} кр`}
</div>
<div className="text-xs text-gray-500 mt-0.5">{op.note}</div>
</div>
))}
</div>
</div>
{/* FAQ */}
<div className="card p-5">
<h2 className="font-semibold mb-4">Часто спрашивают</h2>
<div className="space-y-3 text-sm text-gray-400">
{[
['Что такое кредиты?', '1 кредит = 1 рубль. Кредиты списываются при каждой AI-генерации. Публикация постов — всегда бесплатна.'],
['Что будет если кредиты закончатся?', 'Генерация будет заблокирована до пополнения. Уже опубликованные посты и автопостинг продолжают работать.'],
['Переносятся ли кредиты на следующий месяц?', 'Нет, кредиты по тарифу сбрасываются раз в 30 дней. Дополнительно купленные кредиты не сгорают.'],
['Можно ли купить кредиты отдельно?', 'Скоро. Сейчас кредиты начисляются только по тарифному плану.'],
].map(([q, a]) => (
<details key={q} className="group">
<summary className="cursor-pointer font-medium text-gray-200 hover:text-white list-none flex items-center justify-between">
{q} <span className="text-gray-500 group-open:rotate-180 transition-transform"></span>
</summary>
<p className="mt-2 pl-1">{a}</p>
</details>
))}
</div>
</div>
</main>
);
}