@@ -0,0 +1,153 @@
'use client' ;
import { useState , useEffect } from 'react' ;
import { Check , Zap , Loader2 } from 'lucide-react' ;
import Link from 'next/link' ;
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" / >
< / m a i n >
) ;
return (
< main className = "max-w-5xl mx-auto p-4 sm:p-6" >
< div className = "text-center mb-10" >
< h1 className = "text-3xl font-bold mb-2" > Тарифы < / h 1 >
< p className = "text-gray-400" > Выберите план под ваши задачи . Все планы включают публикацию в TG и VK . < / p >
{ balance && (
< p className = "text-sm text-accent mt-2" >
Сейчас у вас : < strong > { balance . planName } < / s t r o n g > · { b a l a n c e . i s U n l i m i t e d ? ' ∞ ' : b a l a n c e . c r e d i t s } к р е д и т о в
< / p >
) }
< / d i v >
{ /* Карточки планов */ }
< 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 }
< / d i v >
) }
< div className = "mb-4" >
< div className = "text-lg font-bold" > { plan . name } < / d i v >
< div className = "mt-1" >
{ plan . price _rub === 0
? < span className = "text-2xl font-bold" > Бесплатно < / s p a n >
: < > < span className = "text-2xl font-bold" > ₽ { plan . price _rub } < / s p a n > < s p a n c l a s s N a m e = " t e x t - g r a y - 4 0 0 t e x t - s m " > / м е с < / s p a n > < / >
}
< / d i v >
< div className = "text-sm text-accent mt-1 font-medium" >
{ isUnlimited ? '∞ кредитов' : ` ${ plan . credits _month } кредитов/мес ` }
< / d i v >
< / d i v >
< 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 }
< / l i >
) ) }
< / u l >
{ isCurrent ? (
< div className = "w-full text-center py-2 rounded-lg bg-surface2 text-gray-400 text-sm" > Текущий план < / d i v >
) : plan . price _rub === 0 ? (
< Link href = "/register" className = { ` w-full text-center py-2 rounded-lg text-sm ${ style . btnClass } ` } > Начать бесплатно < / L i n k >
) : (
< button className = { ` w-full text-center py-2 text-sm ${ style . btnClass } ` }
onClick = { ( ) => alert ( 'ЮKassa — скоро' ) } >
Подключить
< / b u t t o n >
) }
< / d i v >
) ;
} ) }
< / d i v >
{ /* Стоимость операций */ }
< 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" / > Стоимость генерации
< / h 2 >
< 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 } < / d i v >
< div className = "font-medium" > { op . label } < / d i v >
< div className = "text-accent font-bold mt-1" >
{ ( costs [ op . op ] || 0 ) === 0 ? 'бесплатно' : ` ${ costs [ op . op ] } кр ` }
< / d i v >
< div className = "text-xs text-gray-500 mt-0.5" > { op . note } < / d i v >
< / d i v >
) ) }
< / d i v >
< / d i v >
{ /* FAQ */ }
< div className = "card p-5" >
< h2 className = "font-semibold mb-4" > Часто спрашивают < / h 2 >
< 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" > ▾ < / s p a n >
< / s u m m a r y >
< p className = "mt-2 pl-1" > { a } < / p >
< / d e t a i l s >
) ) }
< / d i v >
< / d i v >
< / m a i n >
) ;
}