forked from admin/zeropost-tool
feat: promo codes UI + apply on /billing
AdminPromos.js: создание/список/toggle/удаление промокодов
auto-generated code, type (credits/%), max_uses, expires, description
AdminPanel: раздел Промокоды между Тарифами и Пользователями
/billing page: кнопка '🎁 Есть промокод?' → форма ввода → apply-promo API
API routes: /api/admin/promos, /api/admin/promos/[id], /api/billing/apply-promo
This commit is contained in:
@@ -95,6 +95,9 @@ export default function BillingPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Промокод */}
|
||||
<PromoForm onApplied={() => load(0)} />
|
||||
|
||||
{/* Стоимость операций */}
|
||||
<div className="card p-4 mb-6">
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wide mb-3">Стоимость операций</div>
|
||||
@@ -155,3 +158,59 @@ export default function BillingPage() {
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function PromoForm({ onApplied }) {
|
||||
const [code, setCode] = useState('');
|
||||
const [msg, setMsg] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
async function apply() {
|
||||
if (!code.trim()) return;
|
||||
setBusy(true);
|
||||
const res = await fetch('/api/billing/apply-promo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code: code.trim().toUpperCase() }),
|
||||
}).then(r => r.json());
|
||||
setBusy(false);
|
||||
if (res.ok) {
|
||||
setMsg(res.message);
|
||||
setCode('');
|
||||
setShow(false);
|
||||
onApplied?.();
|
||||
} else {
|
||||
setMsg('Ошибка: ' + (res.error || 'неизвестно'));
|
||||
}
|
||||
setTimeout(() => setMsg(''), 4000);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
{!show ? (
|
||||
<button onClick={() => setShow(true)} className="text-sm text-gray-500 hover:text-accent transition-colors">
|
||||
🎁 Есть промокод?
|
||||
</button>
|
||||
) : (
|
||||
<div className="card p-4">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={code}
|
||||
onChange={e => setCode(e.target.value.toUpperCase())}
|
||||
onKeyDown={e => e.key === 'Enter' && apply()}
|
||||
placeholder="ВВЕДИТЕ КОД"
|
||||
className="input flex-1 font-mono text-sm py-1.5 tracking-widest"
|
||||
autoFocus
|
||||
maxLength={32}
|
||||
/>
|
||||
<button onClick={apply} disabled={busy || !code.trim()} className="btn-primary px-4 text-sm">
|
||||
{busy ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Применить'}
|
||||
</button>
|
||||
<button onClick={() => { setShow(false); setCode(''); }} className="btn-ghost px-3 text-sm">✕</button>
|
||||
</div>
|
||||
{msg && <p className={`text-xs mt-2 ${msg.startsWith('Ошибка') ? 'text-red-400' : 'text-green-400'}`}>{msg}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user