Port meetups, discussions, gigs, dating/chat, VIP pay (ZPay/XorPay), MiroTalk live, digital academy, and ebook reader. Add persistent community_store, git-first deploy docs, and env template for production secrets. Co-authored-by: Cursor <cursoragent@cursor.com>
51 lines
1.7 KiB
TypeScript
51 lines
1.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { trackEvent } from '@/lib/ebook/analytics';
|
|
import { getFeatures } from '@/lib/ebook/features';
|
|
import { useT } from '@/components/ebook/LocaleProvider';
|
|
|
|
export function BuyEbook({ compact = false }: { compact?: boolean }) {
|
|
const t = useT();
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const productName = getFeatures().payment.checkout.productName;
|
|
const buttonText = getFeatures().payment.checkout.buttonText;
|
|
|
|
const buy = async () => {
|
|
setError('');
|
|
setBusy(true);
|
|
trackEvent('buy_click', { from: compact ? 'reader' : 'cover' });
|
|
try {
|
|
const res = await fetch('/api/creem/checkout', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
const data = (await res.json()) as { ok?: boolean; checkoutUrl?: string; error?: string };
|
|
if (!res.ok || !data.checkoutUrl) {
|
|
throw new Error(data.error || t('payment.checkoutError'));
|
|
}
|
|
window.location.href = data.checkoutUrl;
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : t('payment.checkoutFailed'));
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<section className={`buy-ebook ${compact ? 'is-compact' : ''}`}>
|
|
<h2 className="buy-ebook-title">{t('payment.buyGet', { name: productName })}</h2>
|
|
<p className="buy-ebook-sub">{t('payment.buySub')}</p>
|
|
<button
|
|
type="button"
|
|
className="buy-ebook-btn"
|
|
disabled={busy}
|
|
onClick={buy}
|
|
>
|
|
{busy ? t('payment.openingCheckout') : buttonText}
|
|
</button>
|
|
{error ? <p className="buy-ebook-error">{error}</p> : null}
|
|
</section>
|
|
);
|
|
}
|