218 lines
10 KiB
React
218 lines
10 KiB
React
import React, { useState, useEffect } from 'react';
|
||
import { useApp } from '../../context/AppContext';
|
||
import { getToken } from '../../services/auth';
|
||
import { Check, Sparkles, CreditCard, Loader2, Info, ShieldCheck, XCircle } from 'lucide-react';
|
||
import { CancelSubscriptionModal } from '../CancelSubscriptionModal';
|
||
|
||
export const Pricing = () => {
|
||
const { authUser, plan, planYenile } = useApp();
|
||
const [data, setData] = useState(null);
|
||
const [busy, setBusy] = useState(null);
|
||
const [error, setError] = useState(null);
|
||
const [yillik, setYillik] = useState(true); // yıllık varsayılan (indirimli)
|
||
const [iptalAcik, setIptalAcik] = useState(false);
|
||
const [iptalSonuc, setIptalSonuc] = useState(null);
|
||
|
||
useEffect(() => {
|
||
fetch('/api/billing/plans')
|
||
.then((r) => r.json())
|
||
.then(setData)
|
||
.catch(() => setData({ configured: false, plans: [] }));
|
||
}, []);
|
||
|
||
const upgrade = async (planId) => {
|
||
setBusy(planId);
|
||
setError(null);
|
||
try {
|
||
const res = await fetch('/api/billing/checkout', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
|
||
body: JSON.stringify({ planId, periyot: yillik ? 'yillik' : 'aylik' }),
|
||
});
|
||
const d = await res.json();
|
||
if (d.success && d.url) window.location.href = d.url;
|
||
else setError(d.error || 'Ödeme başlatılamadı');
|
||
} catch (e) {
|
||
setError(e.message);
|
||
} finally {
|
||
setBusy(null);
|
||
}
|
||
};
|
||
|
||
const plans = data?.plans || [];
|
||
// Yıllık tutar Stripe'taki fiyatla birebir aynı olsun diye plandan gelir
|
||
const fiyatla = (p) => (yillik ? (p.fiyatYillik ?? Math.round(p.fiyat * 12 * 0.83)) : p.fiyat);
|
||
const aylikKarsiligi = (p) => (p.fiyatYillik ? Math.round(p.fiyatYillik / 12) : p.fiyat);
|
||
|
||
return (
|
||
<div className="space-y-6 animate-fade-in">
|
||
|
||
<div className="bg-linear-to-r from-slate-900 via-blue-950 to-indigo-900 text-white p-6 rounded-2xl shadow-lg border border-blue-800/40">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 rounded-2xl bg-blue-600 flex items-center justify-center shadow-md">
|
||
<CreditCard className="w-6 h-6" />
|
||
</div>
|
||
<div>
|
||
<h2 className="font-extrabold text-xl">Planlar & Fiyatlandırma</h2>
|
||
<p className="text-xs text-blue-200 mt-0.5">İhtiyacınıza uygun planı seçin. İstediğiniz zaman iptal edebilirsiniz.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Aylık / Yıllık */}
|
||
<div className="flex justify-center">
|
||
<div className="bg-white border border-slate-200 rounded-xl p-1 inline-flex text-xs font-bold shadow-xs">
|
||
<button onClick={() => setYillik(true)} className={`px-4 py-2 rounded-lg transition-colors flex items-center gap-1.5 ${yillik ? 'bg-blue-600 text-white' : 'text-slate-600 hover:bg-slate-50'}`}>
|
||
Yıllık <span className={`text-[10px] px-1.5 py-0.5 rounded-full ${yillik ? 'bg-amber-400 text-slate-900' : 'bg-emerald-100 text-emerald-800'}`}>%17 indirim</span>
|
||
</button>
|
||
<button onClick={() => setYillik(false)} className={`px-4 py-2 rounded-lg transition-colors ${!yillik ? 'bg-blue-600 text-white' : 'text-slate-600 hover:bg-slate-50'}`}>
|
||
Aylık
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{error && (
|
||
<div className="bg-red-50 border border-red-200 rounded-xl p-3 text-xs text-red-800 text-center">{error}</div>
|
||
)}
|
||
|
||
{data && !data.configured && (
|
||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 flex gap-3">
|
||
<Info className="w-4 h-4 text-amber-700 shrink-0 mt-0.5" />
|
||
<p className="text-xs text-amber-900 leading-relaxed">
|
||
<strong>Ödeme altyapısı henüz bağlanmadı.</strong> Satın alma için sunucuya <code className="bg-amber-100 px-1 rounded">STRIPE_SECRET_KEY</code> ve
|
||
plan fiyat kimlikleri (<code className="bg-amber-100 px-1 rounded">STRIPE_PRICE_PRO</code>, <code className="bg-amber-100 px-1 rounded">STRIPE_PRICE_KURUMSAL</code>) eklenmeli.
|
||
Planlar aşağıda görüntülenebilir.
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Mevcut abonelik — gerçek /api/me/plan verisi */}
|
||
{plan && (
|
||
<div className="bg-white border border-slate-200 rounded-2xl p-5 shadow-xs">
|
||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 rounded-xl bg-emerald-50 border border-emerald-200 flex items-center justify-center">
|
||
<ShieldCheck className="w-5 h-5 text-emerald-600" />
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-slate-500">Mevcut aboneliğiniz</p>
|
||
<p className="font-black text-slate-900">
|
||
{plan.abonelikVar ? plan.abonelikAdi : 'Ücretsiz'}
|
||
{plan.yonetici && (
|
||
<span className="ml-2 text-[10px] font-bold px-1.5 py-0.5 rounded-full bg-indigo-50 text-indigo-700 border border-indigo-200">
|
||
yönetici erişimi: {plan.planAdi}
|
||
</span>
|
||
)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{plan.iptalEdilebilir && (
|
||
<button
|
||
onClick={() => setIptalAcik(true)}
|
||
className="text-xs font-bold text-red-600 hover:text-red-700 hover:bg-red-50 border border-red-200 px-3 py-2 rounded-xl flex items-center gap-1.5 transition-colors"
|
||
>
|
||
<XCircle className="w-4 h-4" /> Aboneliği iptal et
|
||
</button>
|
||
)}
|
||
</div>
|
||
{plan.yonetici && (
|
||
<p className="text-[11px] text-slate-400 mt-2 leading-relaxed">
|
||
Yönetici hesapları tüm özelliklere erişir; bu, satın aldığınız aboneliğin yerine geçmez.
|
||
</p>
|
||
)}
|
||
{iptalSonuc && (
|
||
<div className="mt-3 bg-amber-50 border border-amber-200 rounded-xl p-3 text-xs text-amber-900">
|
||
Aboneliğiniz iptal edildi. Erişiminiz{' '}
|
||
<strong>{iptalSonuc.bitis ? new Date(iptalSonuc.bitis).toLocaleDateString('tr-TR') : 'dönem sonuna'}</strong>{' '}
|
||
kadar açık kalacak, sonrasında yenileme yapılmayacak.
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||
{plans.map((p) => (
|
||
<div
|
||
key={p.id}
|
||
className={`rounded-2xl border p-6 flex flex-col gap-4 shadow-xs ${
|
||
p.vurgu ? 'border-blue-500 border-2 bg-linear-to-b from-blue-50 to-white shadow-lg' : 'border-slate-200 bg-white'
|
||
}`}
|
||
>
|
||
<div>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<h3 className="font-black text-lg text-slate-900">{p.ad}</h3>
|
||
{p.rozet && (
|
||
<span className="bg-blue-600 text-white text-[10px] font-black px-2 py-0.5 rounded-full uppercase tracking-wide flex items-center gap-1">
|
||
<Sparkles className="w-3 h-3" /> {p.rozet}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<p className="text-xs text-slate-500 mt-1">{p.ozet}</p>
|
||
</div>
|
||
|
||
<div>
|
||
<span className="text-3xl font-black text-slate-900">
|
||
{p.fiyat === 0 ? 'Ücretsiz' : `₺${fiyatla(p).toLocaleString('tr-TR')}`}
|
||
</span>
|
||
{p.fiyat > 0 && <span className="text-xs text-slate-500 font-medium"> /{yillik ? 'yıl' : 'ay'}</span>}
|
||
{p.fiyat > 0 && (
|
||
<p className="text-[11px] text-slate-400 mt-0.5">
|
||
KDV hariç
|
||
{yillik && (
|
||
<> · ayda ₺{aylikKarsiligi(p).toLocaleString('tr-TR')} · yılda{' '}
|
||
<span className="text-emerald-600 font-bold">
|
||
₺{(p.fiyat * 12 - fiyatla(p)).toLocaleString('tr-TR')} tasarruf
|
||
</span>
|
||
</>
|
||
)}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<ul className="space-y-2 text-xs text-slate-700 flex-1">
|
||
{p.ozellikler.map((o, i) => (
|
||
<li key={i} className="flex items-start gap-2">
|
||
<Check className="w-3.5 h-3.5 text-emerald-600 shrink-0 mt-0.5" />
|
||
<span>{o}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
|
||
{p.fiyat === 0 ? (
|
||
<div className="text-center text-xs font-bold text-slate-500 bg-slate-50 border border-slate-200 rounded-xl py-2.5">
|
||
Mevcut planınız
|
||
</div>
|
||
) : (
|
||
<button
|
||
onClick={() => upgrade(p.id)}
|
||
disabled={!(yillik ? p.satinAlinabilirYillik : p.satinAlinabilir) || busy === p.id || !authUser}
|
||
title={!authUser ? 'Önce giriş yapın' : !(yillik ? p.satinAlinabilirYillik : p.satinAlinabilir) ? 'Ödeme altyapısı bağlanmadı' : ''}
|
||
className={`w-full font-extrabold text-xs py-3 rounded-xl flex items-center justify-center gap-2 transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
|
||
p.vurgu ? 'bg-blue-600 hover:bg-blue-500 text-white' : 'bg-slate-900 hover:bg-slate-800 text-white'
|
||
}`}
|
||
>
|
||
{busy === p.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <CreditCard className="w-4 h-4" />}
|
||
{busy === p.id ? 'Yönlendiriliyor…' : `${p.ad}'a Geç`}
|
||
</button>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{iptalAcik && (
|
||
<CancelSubscriptionModal
|
||
planAdi={plan?.abonelikAdi || 'Mevcut'}
|
||
nedenler={data?.iptalNedenleri || []}
|
||
onKapat={() => setIptalAcik(false)}
|
||
onIptalEdildi={(out) => { setIptalAcik(false); setIptalSonuc(out); planYenile(); }}
|
||
/>
|
||
)}
|
||
|
||
<p className="text-[11px] text-slate-400 text-center leading-relaxed max-w-2xl mx-auto">
|
||
Ödemeler Stripe üzerinden güvenli şekilde alınır; kart bilgileriniz İhaleist sunucularında saklanmaz.
|
||
Aboneliğinizi istediğiniz zaman iptal edebilirsiniz.
|
||
</p>
|
||
</div>
|
||
);
|
||
};
|