rc
This commit is contained in:
411
app/components/RedeemPointsModal.tsx
Normal file
411
app/components/RedeemPointsModal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user