This commit is contained in:
root
2025-12-31 08:19:59 +00:00
parent 0d8c2ea3a3
commit d138dae2ca
8 changed files with 785 additions and 8 deletions

View File

@@ -0,0 +1,411 @@
'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>
)
}