Files
cbd420/app/components/RedeemPointsModal.tsx
2026-01-03 06:06:54 +00:00

420 lines
14 KiB
TypeScript
Raw Permalink 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 Image from 'next/image'
import { useI18n } from '@/lib/i18n'
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] = useState<string>('usdtsol') // Fixed to USDT (SOL) only
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('')
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 changes
// USDT (SOL) rate is approximately 0.9 CHF per USDT
if (pointsToRedeem && !isNaN(parseFloat(pointsToRedeem))) {
const points = parseFloat(pointsToRedeem)
const chfValue = points / pointsToCryptoChf
const usdtRate = 0.9 // CHF per USDT (SOL)
setEstimatedCrypto(chfValue / usdtRate)
} else {
setEstimatedCrypto(0)
}
}, [pointsToRedeem, 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)} USDT</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', display: 'flex', alignItems: 'center', gap: '8px' }}>
<Image
src="/icon_ref_points.png"
alt="Referral Points"
width={24}
height={24}
style={{ display: 'inline-block', verticalAlign: 'middle' }}
/>
{currentPoints.toFixed(2)} {t('redeemPoints.points')}
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: 500 }}>
{t('redeemPoints.cryptoCurrency')}
</label>
<div style={{
width: '100%',
padding: '12px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '14px',
color: 'var(--text)',
}}>
USDT (SOL)
</div>
</div>
<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>
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<input
type="number"
value={pointsToRedeem}
onChange={(e) => setPointsToRedeem(e.target.value)}
min={minRedemptionPoints}
max={currentPoints}
step="1"
placeholder={minRedemptionPoints.toString()}
style={{
flex: 1,
padding: '12px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '14px',
color: 'var(--text)',
}}
required
/>
<button
type="button"
onClick={() => setPointsToRedeem(currentPoints.toString())}
style={{
padding: '12px 20px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '14px',
color: 'var(--text)',
cursor: 'pointer',
fontWeight: 500,
whiteSpace: 'nowrap',
}}
>
{t('drop.max')}
</button>
</div>
{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)} USDT
</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>
)
}