Add files via upload

This commit is contained in:
Egor
2026-01-19 01:18:02 +03:00
committed by GitHub
parent 0dff04a77e
commit 4833f51dc7
2 changed files with 97 additions and 157 deletions

View File

@@ -145,22 +145,22 @@ function PaymentMethodModal({ paymentMethods, onSelect, onClose }: PaymentMethod
const { formatAmount, currencySymbol } = useCurrency() const { formatAmount, currencySymbol } = useCurrency()
return ( return (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-end sm:items-center justify-center"> <div className="fixed inset-0 bg-black/70 z-50 flex items-start justify-center pt-12 sm:pt-0 sm:items-center px-3">
<div className="absolute inset-0" onClick={onClose} /> <div className="absolute inset-0" onClick={onClose} />
<div className="relative w-full sm:max-w-lg sm:mx-4 bg-dark-900 sm:rounded-2xl rounded-t-2xl border border-dark-700/50 shadow-2xl animate-slide-up max-h-[80vh] flex flex-col"> <div className="relative w-full max-w-sm bg-dark-900 rounded-2xl border border-dark-700/50 shadow-2xl overflow-hidden max-h-[70vh] flex flex-col">
{/* Compact Header */} {/* Header */}
<div className="flex-shrink-0 flex justify-between items-center px-4 py-3 border-b border-dark-800"> <div className="flex items-center justify-between px-4 py-3 bg-dark-800/50">
<h2 className="text-base font-semibold text-dark-100">{t('balance.selectPaymentMethod')}</h2> <span className="font-semibold text-dark-100">{t('balance.selectPaymentMethod')}</span>
<button onClick={onClose} className="w-8 h-8 rounded-lg bg-dark-800 hover:bg-dark-700 flex items-center justify-center"> <button onClick={onClose} className="p-1.5 rounded-lg hover:bg-dark-700 text-dark-400">
<svg className="w-4 h-4 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
</div> </div>
{/* Scrollable content */} {/* Content */}
<div className="flex-1 overflow-y-auto overscroll-contain p-3"> <div className="flex-1 overflow-y-auto p-3 space-y-2">
{!paymentMethods ? ( {!paymentMethods ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<div className="w-6 h-6 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" />
@@ -170,49 +170,40 @@ function PaymentMethodModal({ paymentMethods, onSelect, onClose }: PaymentMethod
{t('balance.noPaymentMethods')} {t('balance.noPaymentMethods')}
</div> </div>
) : ( ) : (
<div className="space-y-2"> paymentMethods.map((method) => {
{paymentMethods.map((method) => { const methodKey = method.id.toLowerCase().replace(/-/g, '_')
const methodKey = method.id.toLowerCase().replace(/-/g, '_') const translatedName = t(`balance.paymentMethods.${methodKey}.name`, { defaultValue: '' })
const translatedName = t(`balance.paymentMethods.${methodKey}.name`, { defaultValue: '' })
return ( return (
<button <button
key={method.id} key={method.id}
disabled={!method.is_available} disabled={!method.is_available}
onClick={() => method.is_available && onSelect(method)} onClick={() => method.is_available && onSelect(method)}
className={`w-full p-3 rounded-xl border text-left transition-all flex items-center gap-3 ${ className={`w-full p-3 rounded-xl text-left flex items-center gap-3 ${
method.is_available method.is_available
? 'border-dark-700/50 bg-dark-800/30 active:bg-dark-700/50' ? 'bg-dark-800 hover:bg-dark-700 active:bg-dark-600'
: 'border-dark-800/30 bg-dark-900/30 opacity-50' : 'bg-dark-800/50 opacity-50'
}`} }`}
> >
<div className="w-10 h-10 rounded-lg bg-dark-800 flex items-center justify-center flex-shrink-0"> <div className="w-9 h-9 rounded-lg bg-accent-500/20 flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-4 h-4 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-dark-100 text-sm">{translatedName || method.name}</div>
<div className="text-xs text-dark-500">
{formatAmount(method.min_amount_kopeks / 100, 0)} {formatAmount(method.max_amount_kopeks / 100, 0)} {currencySymbol}
</div>
</div>
<svg className="w-4 h-4 text-dark-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg> </svg>
</button> </div>
) <div className="flex-1 min-w-0">
})} <div className="font-medium text-dark-100 text-sm">{translatedName || method.name}</div>
</div> <div className="text-xs text-dark-500">
{formatAmount(method.min_amount_kopeks / 100, 0)} {formatAmount(method.max_amount_kopeks / 100, 0)} {currencySymbol}
</div>
</div>
<svg className="w-4 h-4 text-dark-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
)
})
)} )}
</div> </div>
{/* Compact Footer */}
<div className="flex-shrink-0 p-3 pt-2 border-t border-dark-800 bg-dark-900 safe-area-inset-bottom">
<button type="button" onClick={onClose} className="w-full py-2 text-sm text-dark-400 hover:text-dark-200">
{t('common.cancel')}
</button>
</div>
</div> </div>
</div> </div>
) )

View File

@@ -7,7 +7,6 @@ import { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils
import type { PaymentMethod } from '../types' import type { PaymentMethod } from '../types'
const TELEGRAM_LINK_REGEX = /^https?:\/\/t\.me\//i const TELEGRAM_LINK_REGEX = /^https?:\/\/t\.me\//i
const isTelegramPaymentLink = (url: string): boolean => TELEGRAM_LINK_REGEX.test(url) const isTelegramPaymentLink = (url: string): boolean => TELEGRAM_LINK_REGEX.test(url)
const openPaymentLink = (url: string, reservedWindow?: Window | null) => { const openPaymentLink = (url: string, reservedWindow?: Window | null) => {
@@ -17,16 +16,13 @@ const openPaymentLink = (url: string, reservedWindow?: Window | null) => {
if (isTelegramPaymentLink(url) && webApp?.openTelegramLink) { if (isTelegramPaymentLink(url) && webApp?.openTelegramLink) {
try { webApp.openTelegramLink(url); return } catch (e) { console.warn('[TopUpModal] openTelegramLink failed:', e) } try { webApp.openTelegramLink(url); return } catch (e) { console.warn('[TopUpModal] openTelegramLink failed:', e) }
} }
if (webApp?.openLink) { if (webApp?.openLink) {
try { webApp.openLink(url, { try_instant_view: false }); return } catch (e) { console.warn('[TopUpModal] webApp.openLink failed:', e) } try { webApp.openLink(url, { try_instant_view: false }); return } catch (e) { console.warn('[TopUpModal] webApp.openLink failed:', e) }
} }
if (reservedWindow && !reservedWindow.closed) { if (reservedWindow && !reservedWindow.closed) {
try { reservedWindow.location.href = url; reservedWindow.focus?.() } catch (e) { console.warn('[TopUpModal] Failed to use reserved window:', e) } try { reservedWindow.location.href = url; reservedWindow.focus?.() } catch (e) { console.warn('[TopUpModal] Failed to use reserved window:', e) }
return return
} }
const w2 = window.open(url, '_blank', 'noopener,noreferrer') const w2 = window.open(url, '_blank', 'noopener,noreferrer')
if (w2) { w2.opener = null; return } if (w2) { w2.opener = null; return }
window.location.href = url window.location.href = url
@@ -46,10 +42,9 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
const getInitialAmount = (): string => { const getInitialAmount = (): string => {
if (!initialAmountRubles || initialAmountRubles <= 0) return '' if (!initialAmountRubles || initialAmountRubles <= 0) return ''
const converted = convertAmount(initialAmountRubles) const converted = convertAmount(initialAmountRubles)
if (targetCurrency === 'IRR' || targetCurrency === 'RUB') { return (targetCurrency === 'IRR' || targetCurrency === 'RUB')
return Math.ceil(converted).toString() ? Math.ceil(converted).toString()
} : converted.toFixed(2)
return converted.toFixed(2)
} }
const [amount, setAmount] = useState(getInitialAmount) const [amount, setAmount] = useState(getInitialAmount)
@@ -57,7 +52,6 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
const [selectedOption, setSelectedOption] = useState<string | null>( const [selectedOption, setSelectedOption] = useState<string | null>(
method.options && method.options.length > 0 ? method.options[0].id : null method.options && method.options.length > 0 ? method.options[0].id : null
) )
const [isInputFocused, setIsInputFocused] = useState(false)
const popupRef = useRef<Window | null>(null) const popupRef = useRef<Window | null>(null)
const hasOptions = method.options && method.options.length > 0 const hasOptions = method.options && method.options.length > 0
@@ -78,24 +72,18 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
webApp.openInvoice(data.invoice_url, (status) => { webApp.openInvoice(data.invoice_url, (status) => {
if (status === 'paid') { setError(null); onClose() } if (status === 'paid') { setError(null); onClose() }
else if (status === 'failed') { setError(t('wheel.starsPaymentFailed')) } else if (status === 'failed') { setError(t('wheel.starsPaymentFailed')) }
else if (status === 'cancelled') { setError(null) }
}) })
} catch (e) { setError('Ошибка открытия окна оплаты: ' + String(e)) } } catch (e) { setError('Ошибка: ' + String(e)) }
}, },
onError: (error: unknown) => { onError: (err: unknown) => {
const axiosError = error as { response?: { data?: { detail?: string }, status?: number } } const axiosError = err as { response?: { data?: { detail?: string }, status?: number } }
setError(`Ошибка API (${axiosError?.response?.status || 'network'}): ${axiosError?.response?.data?.detail || 'Не удалось создать счёт'}`) setError(`Ошибка: ${axiosError?.response?.data?.detail || 'Не удалось создать счёт'}`)
}, },
}) })
const topUpMutation = useMutation<{ const topUpMutation = useMutation<{
payment_id: string payment_id: string; payment_url?: string; invoice_url?: string
payment_url?: string amount_kopeks: number; amount_rubles: number; status: string; expires_at: string | null
invoice_url?: string
amount_kopeks: number
amount_rubles: number
status: string
expires_at: string | null
}, unknown, number>({ }, unknown, number>({
mutationFn: (amountKopeks: number) => balanceApi.createTopUp(amountKopeks, method.id, selectedOption || undefined), mutationFn: (amountKopeks: number) => balanceApi.createTopUp(amountKopeks, method.id, selectedOption || undefined),
onSuccess: (data) => { onSuccess: (data) => {
@@ -104,12 +92,11 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
popupRef.current = null popupRef.current = null
onClose() onClose()
}, },
onError: (error: unknown) => { onError: (err: unknown) => {
try { if (popupRef.current && !popupRef.current.closed) popupRef.current.close() } catch {} try { if (popupRef.current && !popupRef.current.closed) popupRef.current.close() } catch {}
popupRef.current = null popupRef.current = null
const detail = (error as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '' const detail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail || ''
if (detail.includes('not yet implemented')) setError(t('balance.useBot')) setError(detail.includes('not yet implemented') ? t('balance.useBot') : (detail || t('common.error')))
else setError(detail || t('common.error'))
}, },
}) })
@@ -118,76 +105,63 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
inputRef.current?.blur() inputRef.current?.blur()
if (!checkRateLimit(RATE_LIMIT_KEYS.PAYMENT, 3, 30000)) { if (!checkRateLimit(RATE_LIMIT_KEYS.PAYMENT, 3, 30000)) {
setError(t('balance.tooManyRequests', { seconds: getRateLimitResetTime(RATE_LIMIT_KEYS.PAYMENT) }) || 'Подождите...') setError('Подождите ' + getRateLimitResetTime(RATE_LIMIT_KEYS.PAYMENT) + ' сек.')
return return
} }
if (hasOptions && !selectedOption) { setError('Выберите способ'); return }
if (hasOptions && !selectedOption) { setError(t('balance.selectPaymentOption', 'Выберите способ')); return }
const amountCurrency = parseFloat(amount) const amountCurrency = parseFloat(amount)
if (isNaN(amountCurrency) || amountCurrency <= 0) { setError(t('balance.invalidAmount', 'Введите сумму')); return } if (isNaN(amountCurrency) || amountCurrency <= 0) { setError('Введите сумму'); return }
const amountRubles = convertToRub(amountCurrency) const amountRubles = convertToRub(amountCurrency)
if (amountRubles < minRubles) { setError(t('balance.minAmountError', { amount: minRubles })); return } if (amountRubles < minRubles || amountRubles > maxRubles) {
if (amountRubles > maxRubles) { setError(t('balance.maxAmountError', { amount: maxRubles })); return } setError(`Сумма: ${minRubles} ${maxRubles}`); return
}
const amountKopeks = Math.round(amountRubles * 100) const amountKopeks = Math.round(amountRubles * 100)
if (!isTelegramMiniApp) { if (!isTelegramMiniApp) {
try { popupRef.current = window.open('', '_blank') } catch { popupRef.current = null } try { popupRef.current = window.open('', '_blank') } catch { popupRef.current = null }
} }
if (isStarsMethod) { starsPaymentMutation.mutate(amountKopeks); return } if (isStarsMethod) { starsPaymentMutation.mutate(amountKopeks) }
topUpMutation.mutate(amountKopeks) else { topUpMutation.mutate(amountKopeks) }
} }
const quickAmounts = [100, 300, 500, 1000].filter((a) => a >= minRubles && a <= maxRubles) const quickAmounts = [100, 300, 500, 1000].filter((a) => a >= minRubles && a <= maxRubles)
const currencyDecimals = targetCurrency === 'IRR' || targetCurrency === 'RUB' ? 0 : 2 const currencyDecimals = (targetCurrency === 'IRR' || targetCurrency === 'RUB') ? 0 : 2
const getQuickAmountValue = (rubAmount: number): string => { const getQuickValue = (rub: number) => (targetCurrency === 'IRR')
if (targetCurrency === 'IRR') return Math.round(convertAmount(rubAmount)).toString() ? Math.round(convertAmount(rub)).toString()
return convertAmount(rubAmount).toFixed(currencyDecimals) : convertAmount(rub).toFixed(currencyDecimals)
}
const inputStep = currencyDecimals === 0 ? 1 : 0.01
const isPending = topUpMutation.isPending || starsPaymentMutation.isPending const isPending = topUpMutation.isPending || starsPaymentMutation.isPending
// When input is focused in Telegram Mini App, position modal at top
const modalPosition = isInputFocused && isTelegramMiniApp ? 'items-start pt-2' : 'items-end sm:items-center'
return ( return (
<div className={`fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex justify-center ${modalPosition}`}> <div className="fixed inset-0 bg-black/70 z-50 flex items-start justify-center pt-12 sm:pt-0 sm:items-center px-3">
<div className="absolute inset-0" onClick={() => { inputRef.current?.blur(); onClose() }} /> <div className="absolute inset-0" onClick={onClose} />
<div className="relative w-full sm:max-w-md sm:mx-4 bg-dark-900 sm:rounded-2xl rounded-t-2xl border border-dark-700/50 shadow-2xl animate-slide-up"> <div className="relative w-full max-w-sm bg-dark-900 rounded-2xl border border-dark-700/50 shadow-2xl overflow-hidden">
{/* Header - hide when keyboard open on mobile */} {/* Header */}
<div className={`flex justify-between items-center px-4 py-3 border-b border-dark-800 ${isInputFocused && isTelegramMiniApp ? 'hidden' : ''}`}> <div className="flex items-center justify-between px-4 py-3 bg-dark-800/50">
<div className="flex items-center gap-2"> <span className="font-semibold text-dark-100">{methodName}</span>
<div className="w-8 h-8 rounded-lg bg-accent-500/20 flex items-center justify-center"> <button onClick={onClose} className="p-1.5 rounded-lg hover:bg-dark-700 text-dark-400">
<svg className="w-4 h-4 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</div>
<h2 className="text-base font-semibold text-dark-100">{methodName}</h2>
</div>
<button onClick={onClose} className="w-8 h-8 rounded-lg bg-dark-800 hover:bg-dark-700 flex items-center justify-center">
<svg className="w-4 h-4 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
</div> </div>
{/* Content */}
<div className="p-4 space-y-3"> <div className="p-4 space-y-3">
{/* Payment options - hide when keyboard open */} {/* Payment options */}
{hasOptions && method.options && !isInputFocused && ( {hasOptions && method.options && (
<div className="grid grid-cols-2 gap-2"> <div className="flex gap-2">
{method.options.map((option) => ( {method.options.map((opt) => (
<button <button
key={option.id} key={opt.id}
type="button" type="button"
onClick={() => setSelectedOption(option.id)} onClick={() => setSelectedOption(opt.id)}
className={`p-2.5 rounded-xl text-sm font-medium transition-all text-left ${ className={`flex-1 py-2 px-3 rounded-lg text-sm font-medium transition-all ${
selectedOption === option.id selectedOption === opt.id
? 'bg-accent-500 text-white' ? 'bg-accent-500 text-white'
: 'bg-dark-800 text-dark-300 active:bg-dark-700' : 'bg-dark-800 text-dark-300'
}`} }`}
> >
{option.name} {opt.name}
</button> </button>
))} ))}
</div> </div>
@@ -201,36 +175,27 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
inputMode="decimal" inputMode="decimal"
value={amount} value={amount}
onChange={(e) => setAmount(e.target.value)} onChange={(e) => setAmount(e.target.value)}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setTimeout(() => setIsInputFocused(false), 150)}
placeholder={`${formatAmount(minRubles, 0)} ${formatAmount(maxRubles, 0)}`} placeholder={`${formatAmount(minRubles, 0)} ${formatAmount(maxRubles, 0)}`}
step={inputStep} className="w-full h-12 px-4 pr-12 text-lg font-semibold bg-dark-800 border border-dark-700 rounded-xl text-dark-100 placeholder:text-dark-500 focus:outline-none focus:border-accent-500"
className="w-full h-14 px-4 pr-16 text-xl font-semibold bg-dark-800 border border-dark-700 rounded-xl text-dark-100 placeholder:text-dark-500 focus:outline-none focus:border-accent-500 focus:ring-1 focus:ring-accent-500"
autoComplete="off" autoComplete="off"
/> />
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-dark-400 font-medium text-lg"> <span className="absolute right-4 top-1/2 -translate-y-1/2 text-dark-500 font-medium">
{currencySymbol} {currencySymbol}
</span> </span>
</div> </div>
{/* Quick amounts - always show, they help dismiss keyboard */} {/* Quick amounts */}
{quickAmounts.length > 0 && ( {quickAmounts.length > 0 && (
<div className="grid grid-cols-4 gap-2"> <div className="flex gap-2">
{quickAmounts.map((a) => { {quickAmounts.map((a) => {
const quickValue = getQuickAmountValue(a) const val = getQuickValue(a)
const isSelected = amount === quickValue
return ( return (
<button <button
key={a} key={a}
type="button" type="button"
onClick={() => { onClick={() => { setAmount(val); inputRef.current?.blur() }}
setAmount(quickValue) className={`flex-1 py-2 rounded-lg text-sm font-medium ${
inputRef.current?.blur() amount === val ? 'bg-accent-500 text-white' : 'bg-dark-800 text-dark-300'
}}
className={`py-2.5 rounded-xl text-sm font-medium transition-all ${
isSelected
? 'bg-accent-500 text-white'
: 'bg-dark-800 text-dark-300 active:bg-dark-700'
}`} }`}
> >
{formatAmount(a, 0)} {formatAmount(a, 0)}
@@ -240,46 +205,30 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
</div> </div>
)} )}
{/* Error - hide when keyboard open */} {/* Error */}
{error && !isInputFocused && ( {error && (
<div className="bg-error-500/10 border border-error-500/30 text-error-400 px-3 py-2 rounded-xl text-sm"> <div className="text-error-400 text-sm text-center py-1">{error}</div>
{error}
</div>
)} )}
{/* Submit button - always visible */} {/* Submit */}
<button <button
type="button" type="button"
onClick={handleSubmit} onClick={handleSubmit}
disabled={isPending || !amount} disabled={isPending || !amount}
className="btn-primary w-full h-12 text-base font-semibold mt-2" className="btn-primary w-full h-11 text-base font-semibold"
> >
{isPending ? ( {isPending ? (
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" /> <span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : ( ) : (
<span className="flex items-center justify-center gap-2"> <>
{t('balance.topUp')} {t('balance.topUp')}
{amount && parseFloat(amount) > 0 && ( {amount && parseFloat(amount) > 0 && (
<span className="opacity-80">{formatAmount(parseFloat(amount), currencyDecimals)} {currencySymbol}</span> <span className="ml-2 opacity-80">{formatAmount(parseFloat(amount), currencyDecimals)} {currencySymbol}</span>
)} )}
</span> </>
)} )}
</button> </button>
{/* Cancel - hide when keyboard open */}
{!isInputFocused && (
<button
type="button"
onClick={onClose}
className="w-full py-2 text-sm text-dark-400 hover:text-dark-200"
>
{t('common.cancel')}
</button>
)}
</div> </div>
{/* Safe area for iPhone */}
<div className="safe-area-inset-bottom" />
</div> </div>
</div> </div>
) )