420 lines
14 KiB
TypeScript
420 lines
14 KiB
TypeScript
'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>
|
||
)
|
||
}
|
||
|