Files
cbd420/app/components/RedeemPointsModal.tsx
root d138dae2ca rc
2025-12-31 08:19:59 +00:00

412 lines
13 KiB
TypeScript
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 { useI18n } from '@/lib/i18n'
import { ALLOWED_PAYMENT_CURRENCIES } from '@/lib/payment-currencies'
interface RedeemPointsModalProps {
isOpen: boolean
onClose: () => void
currentPoints: number
onRedeemSuccess: () => void
}
export default function RedeemPointsModal({
isOpen,
onClose,
currentPoints,
onRedeemSuccess,
}: RedeemPointsModalProps) {
const { t } = useI18n()
const [pointsToRedeem, setPointsToRedeem] = useState<string>('')
const [cryptoCurrency, setCryptoCurrency] = useState<string>('btc')
const [walletAddress, setWalletAddress] = useState<string>('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string>('')
const [success, setSuccess] = useState(false)
const [redemptionDetails, setRedemptionDetails] = useState<any>(null)
const [pointsToCryptoChf, setPointsToCryptoChf] = useState<number>(100)
const [minRedemptionPoints, setMinRedemptionPoints] = useState<number>(1000)
const [estimatedCrypto, setEstimatedCrypto] = useState<number>(0)
useEffect(() => {
if (isOpen) {
fetchRedemptionSettings()
// Reset form
setPointsToRedeem('')
setWalletAddress('')
setCryptoCurrency('btc')
setError('')
setSuccess(false)
setRedemptionDetails(null)
}
}, [isOpen])
const fetchRedemptionSettings = async () => {
try {
const response = await fetch('/api/referral-points', {
credentials: 'include',
})
if (response.ok) {
const data = await response.json()
setPointsToCryptoChf(data.points_to_crypto_chf || data.points_to_chf || 100)
setMinRedemptionPoints(data.min_redemption_points || 1000)
}
} catch (error) {
console.error('Error fetching redemption settings:', error)
}
}
useEffect(() => {
// Calculate estimated crypto amount when points or currency changes
if (pointsToRedeem && !isNaN(parseFloat(pointsToRedeem))) {
const points = parseFloat(pointsToRedeem)
const chfValue = points / pointsToCryptoChf
// Mock exchange rates (should match API)
const mockRates: Record<string, number> = {
'btc': 85000,
'eth': 2500,
'sol': 100,
'xrp': 0.6,
'bnbbsc': 300,
'usdterc20': 0.9,
}
const rate = mockRates[cryptoCurrency.toLowerCase()] || 1
setEstimatedCrypto(chfValue / rate)
} else {
setEstimatedCrypto(0)
}
}, [pointsToRedeem, cryptoCurrency, pointsToCryptoChf])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setLoading(true)
try {
const points = parseFloat(pointsToRedeem)
if (isNaN(points) || points <= 0) {
setError(t('redeemPoints.invalidPoints'))
setLoading(false)
return
}
if (points < minRedemptionPoints) {
setError(t('redeemPoints.minPoints', { min: minRedemptionPoints }))
setLoading(false)
return
}
if (points > currentPoints) {
setError(t('redeemPoints.insufficientPoints'))
setLoading(false)
return
}
if (!walletAddress.trim()) {
setError(t('redeemPoints.invalidWallet'))
setLoading(false)
return
}
const response = await fetch('/api/referral-points/redeem', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
points: points,
crypto_currency: cryptoCurrency,
wallet_address: walletAddress.trim(),
}),
})
const data = await response.json()
if (!response.ok) {
setError(data.error || t('redeemPoints.error'))
setLoading(false)
return
}
setSuccess(true)
setRedemptionDetails(data)
onRedeemSuccess()
} catch (error: any) {
console.error('Error redeeming points:', error)
setError(error.message || t('redeemPoints.error'))
} finally {
setLoading(false)
}
}
if (!isOpen) return null
const pointsNum = parseFloat(pointsToRedeem) || 0
const chfValue = pointsNum / pointsToCryptoChf
return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '20px',
}}
onClick={onClose}
>
<div
style={{
background: 'var(--bg)',
borderRadius: '12px',
padding: '32px',
maxWidth: '500px',
width: '100%',
maxHeight: '90vh',
overflowY: 'auto',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
}}
onClick={(e) => e.stopPropagation()}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<h2 style={{ margin: 0, fontSize: '24px', fontWeight: 600 }}>
{t('redeemPoints.title')}
</h2>
<button
onClick={onClose}
style={{
background: 'transparent',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: 'var(--muted)',
padding: 0,
width: '32px',
height: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
×
</button>
</div>
{success ? (
<div>
<div style={{
background: '#0a7931',
color: 'white',
padding: '16px',
borderRadius: '8px',
marginBottom: '24px'
}}>
<strong> {t('redeemPoints.success')}</strong>
</div>
{redemptionDetails && (
<div style={{ marginBottom: '24px' }}>
<p><strong>{t('redeemPoints.redemptionId')}:</strong> #{redemptionDetails.redemption_id}</p>
<p><strong>{t('redeemPoints.pointsRedeemed')}:</strong> {redemptionDetails.points_redeemed.toFixed(2)}</p>
<p><strong>{t('redeemPoints.cryptoAmount')}:</strong> {redemptionDetails.crypto_amount.toFixed(8)} {redemptionDetails.crypto_currency.toUpperCase()}</p>
<p><strong>{t('redeemPoints.newBalance')}:</strong> {redemptionDetails.new_balance.toFixed(2)} {t('redeemPoints.points')}</p>
<p style={{ marginTop: '16px', fontSize: '14px', color: 'var(--muted)' }}>
{redemptionDetails.message}
</p>
</div>
)}
<button
onClick={onClose}
style={{
width: '100%',
padding: '12px',
background: 'var(--primary)',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '16px',
cursor: 'pointer',
fontWeight: 500,
}}
>
{t('common.close')}
</button>
</div>
) : (
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '20px' }}>
<div style={{
background: 'var(--bg-soft)',
padding: '16px',
borderRadius: '8px',
marginBottom: '16px'
}}>
<div style={{ fontSize: '14px', color: 'var(--muted)', marginBottom: '4px' }}>
{t('redeemPoints.currentBalance')}
</div>
<div style={{ fontSize: '24px', fontWeight: 600, color: '#0a7931' }}>
{currentPoints.toFixed(2)} {t('redeemPoints.points')}
</div>
</div>
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: 500 }}>
{t('redeemPoints.selectCrypto')} *
</label>
<select
value={cryptoCurrency}
onChange={(e) => setCryptoCurrency(e.target.value)}
style={{
width: '100%',
padding: '12px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '14px',
color: 'var(--text)',
marginBottom: '16px',
}}
required
>
{ALLOWED_PAYMENT_CURRENCIES.map((currency) => (
<option key={currency} value={currency}>
{currency.toUpperCase()}
</option>
))}
</select>
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: 500 }}>
{t('redeemPoints.walletAddress')} *
</label>
<input
type="text"
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value)}
placeholder={t('redeemPoints.walletAddressPlaceholder')}
style={{
width: '100%',
padding: '12px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '14px',
color: 'var(--text)',
marginBottom: '16px',
fontFamily: 'monospace',
}}
required
/>
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: 500 }}>
{t('redeemPoints.pointsToRedeem')} *
<span style={{ marginLeft: '8px', fontSize: '12px', color: 'var(--muted)', fontWeight: 'normal' }}>
({t('redeemPoints.min')}: {minRedemptionPoints})
</span>
</label>
<input
type="number"
value={pointsToRedeem}
onChange={(e) => setPointsToRedeem(e.target.value)}
min={minRedemptionPoints}
max={currentPoints}
step="1"
placeholder={minRedemptionPoints.toString()}
style={{
width: '100%',
padding: '12px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '14px',
color: 'var(--text)',
marginBottom: '16px',
}}
required
/>
{pointsNum > 0 && (
<div style={{
background: 'var(--bg-soft)',
padding: '12px',
borderRadius: '8px',
marginBottom: '16px',
fontSize: '14px',
}}>
<div style={{ marginBottom: '4px' }}>
<strong>{t('redeemPoints.estimatedValue')}:</strong>
</div>
<div style={{ color: 'var(--muted)' }}>
{chfValue.toFixed(2)} CHF {estimatedCrypto.toFixed(8)} {cryptoCurrency.toUpperCase()}
</div>
</div>
)}
{error && (
<div style={{
background: '#ff4444',
color: 'white',
padding: '12px',
borderRadius: '8px',
marginBottom: '16px',
fontSize: '14px',
}}>
{error}
</div>
)}
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button
type="button"
onClick={onClose}
style={{
flex: 1,
padding: '12px',
background: 'transparent',
color: 'var(--text)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '16px',
cursor: 'pointer',
fontWeight: 500,
}}
>
{t('common.cancel')}
</button>
<button
type="submit"
disabled={loading || pointsNum < minRedemptionPoints || pointsNum > currentPoints}
style={{
flex: 1,
padding: '12px',
background: loading || pointsNum < minRedemptionPoints || pointsNum > currentPoints
? 'var(--muted)'
: 'var(--primary)',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '16px',
cursor: loading || pointsNum < minRedemptionPoints || pointsNum > currentPoints
? 'not-allowed'
: 'pointer',
fontWeight: 500,
}}
>
{loading ? t('common.processing') : t('redeemPoints.redeem')}
</button>
</div>
</form>
)}
</div>
</div>
)
}