mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
@@ -41,7 +41,6 @@ interface TopUpModalProps {
|
|||||||
export default function TopUpModal({ method, onClose, initialAmountRubles }: TopUpModalProps) {
|
export default function TopUpModal({ method, onClose, initialAmountRubles }: TopUpModalProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { formatAmount, currencySymbol, convertAmount, convertToRub, targetCurrency } = useCurrency()
|
const { formatAmount, currencySymbol, convertAmount, convertToRub, targetCurrency } = useCurrency()
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const getInitialAmount = (): string => {
|
const getInitialAmount = (): string => {
|
||||||
@@ -58,7 +57,7 @@ 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 [isKeyboardOpen, setIsKeyboardOpen] = useState(false)
|
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
|
||||||
@@ -69,31 +68,6 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
|
|||||||
const methodName = t(`balance.paymentMethods.${methodKey}.name`, { defaultValue: '' }) || method.name
|
const methodName = t(`balance.paymentMethods.${methodKey}.name`, { defaultValue: '' }) || method.name
|
||||||
const isTelegramMiniApp = typeof window !== 'undefined' && Boolean(window.Telegram?.WebApp?.initData)
|
const isTelegramMiniApp = typeof window !== 'undefined' && Boolean(window.Telegram?.WebApp?.initData)
|
||||||
|
|
||||||
// Detect keyboard open/close
|
|
||||||
useEffect(() => {
|
|
||||||
const handleFocus = () => setIsKeyboardOpen(true)
|
|
||||||
const handleBlur = () => setTimeout(() => setIsKeyboardOpen(false), 100)
|
|
||||||
|
|
||||||
const input = inputRef.current
|
|
||||||
if (input) {
|
|
||||||
input.addEventListener('focus', handleFocus)
|
|
||||||
input.addEventListener('blur', handleBlur)
|
|
||||||
return () => {
|
|
||||||
input.removeEventListener('focus', handleFocus)
|
|
||||||
input.removeEventListener('blur', handleBlur)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Scroll to input when keyboard opens
|
|
||||||
useEffect(() => {
|
|
||||||
if (isKeyboardOpen && containerRef.current && inputRef.current) {
|
|
||||||
setTimeout(() => {
|
|
||||||
inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
||||||
}, 150)
|
|
||||||
}
|
|
||||||
}, [isKeyboardOpen])
|
|
||||||
|
|
||||||
const starsPaymentMutation = useMutation({
|
const starsPaymentMutation = useMutation({
|
||||||
mutationFn: (amountKopeks: number) => balanceApi.createStarsInvoice(amountKopeks),
|
mutationFn: (amountKopeks: number) => balanceApi.createStarsInvoice(amountKopeks),
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -172,25 +146,23 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
|
|||||||
const inputStep = currencyDecimals === 0 ? 1 : 0.01
|
const inputStep = currencyDecimals === 0 ? 1 : 0.01
|
||||||
const isPending = topUpMutation.isPending || starsPaymentMutation.isPending
|
const isPending = topUpMutation.isPending || starsPaymentMutation.isPending
|
||||||
|
|
||||||
return (
|
// When input is focused in Telegram Mini App, position modal at top
|
||||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-end sm:items-center justify-center">
|
const modalPosition = isInputFocused && isTelegramMiniApp ? 'items-start pt-2' : 'items-end sm:items-center'
|
||||||
<div className="absolute inset-0" onClick={onClose} />
|
|
||||||
|
|
||||||
<div
|
return (
|
||||||
ref={containerRef}
|
<div className={`fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex justify-center ${modalPosition}`}>
|
||||||
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 max-h-[85vh] overflow-hidden flex flex-col"
|
<div className="absolute inset-0" onClick={() => { inputRef.current?.blur(); onClose() }} />
|
||||||
>
|
|
||||||
{/* Compact Header */}
|
<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="flex-shrink-0 flex justify-between items-center px-4 py-3 border-b border-dark-800">
|
{/* Header - hide when keyboard open on mobile */}
|
||||||
|
<div className={`flex justify-between items-center px-4 py-3 border-b border-dark-800 ${isInputFocused && isTelegramMiniApp ? 'hidden' : ''}`}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-8 h-8 rounded-lg bg-accent-500/20 flex items-center justify-center">
|
<div className="w-8 h-8 rounded-lg bg-accent-500/20 flex items-center justify-center">
|
||||||
<svg className="w-4 h-4 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<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="M12 4.5v15m7.5-7.5h-15" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<h2 className="text-base font-semibold text-dark-100">{methodName}</h2>
|
||||||
<h2 className="text-base font-semibold text-dark-100">{methodName}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<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="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}>
|
<svg className="w-4 h-4 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
@@ -199,90 +171,88 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-y-auto overscroll-contain">
|
<div className="p-4 space-y-3">
|
||||||
<div className="p-4 space-y-3">
|
{/* Payment options - hide when keyboard open */}
|
||||||
{/* Payment options - compact grid */}
|
{hasOptions && method.options && !isInputFocused && (
|
||||||
{hasOptions && method.options && (
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="grid grid-cols-2 gap-2">
|
{method.options.map((option) => (
|
||||||
{method.options.map((option) => (
|
<button
|
||||||
|
key={option.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedOption(option.id)}
|
||||||
|
className={`p-2.5 rounded-xl text-sm font-medium transition-all text-left ${
|
||||||
|
selectedOption === option.id
|
||||||
|
? 'bg-accent-500 text-white'
|
||||||
|
: 'bg-dark-800 text-dark-300 active:bg-dark-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{option.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Amount input */}
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="number"
|
||||||
|
inputMode="decimal"
|
||||||
|
value={amount}
|
||||||
|
onChange={(e) => setAmount(e.target.value)}
|
||||||
|
onFocus={() => setIsInputFocused(true)}
|
||||||
|
onBlur={() => setTimeout(() => setIsInputFocused(false), 150)}
|
||||||
|
placeholder={`${formatAmount(minRubles, 0)} – ${formatAmount(maxRubles, 0)}`}
|
||||||
|
step={inputStep}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-dark-400 font-medium text-lg">
|
||||||
|
{currencySymbol}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick amounts - always show, they help dismiss keyboard */}
|
||||||
|
{quickAmounts.length > 0 && (
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{quickAmounts.map((a) => {
|
||||||
|
const quickValue = getQuickAmountValue(a)
|
||||||
|
const isSelected = amount === quickValue
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={option.id}
|
key={a}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedOption(option.id)}
|
onClick={() => {
|
||||||
className={`p-2.5 rounded-xl text-sm font-medium transition-all text-left ${
|
setAmount(quickValue)
|
||||||
selectedOption === option.id
|
inputRef.current?.blur()
|
||||||
|
}}
|
||||||
|
className={`py-2.5 rounded-xl text-sm font-medium transition-all ${
|
||||||
|
isSelected
|
||||||
? '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 active:bg-dark-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{option.name}
|
{formatAmount(a, 0)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
)
|
||||||
</div>
|
})}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Amount input with currency */}
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="number"
|
|
||||||
inputMode="decimal"
|
|
||||||
value={amount}
|
|
||||||
onChange={(e) => setAmount(e.target.value)}
|
|
||||||
placeholder={`${formatAmount(minRubles, 0)} – ${formatAmount(maxRubles, 0)}`}
|
|
||||||
step={inputStep}
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-dark-400 font-medium text-lg">
|
|
||||||
{currencySymbol}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Quick amounts - compact */}
|
{/* Error - hide when keyboard open */}
|
||||||
{quickAmounts.length > 0 && (
|
{error && !isInputFocused && (
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="bg-error-500/10 border border-error-500/30 text-error-400 px-3 py-2 rounded-xl text-sm">
|
||||||
{quickAmounts.map((a) => {
|
{error}
|
||||||
const quickValue = getQuickAmountValue(a)
|
</div>
|
||||||
const isSelected = amount === quickValue
|
)}
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={a}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setAmount(quickValue)
|
|
||||||
inputRef.current?.blur()
|
|
||||||
}}
|
|
||||||
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)}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Error */}
|
{/* Submit button - always visible */}
|
||||||
{error && (
|
|
||||||
<div className="bg-error-500/10 border border-error-500/30 text-error-400 px-3 py-2 rounded-xl text-sm">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Fixed Footer with Submit */}
|
|
||||||
<div className="flex-shrink-0 p-4 pt-3 border-t border-dark-800 bg-dark-900 safe-area-inset-bottom">
|
|
||||||
<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"
|
className="btn-primary w-full h-12 text-base font-semibold mt-2"
|
||||||
>
|
>
|
||||||
{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" />
|
||||||
@@ -295,14 +265,21 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
{/* Cancel - hide when keyboard open */}
|
||||||
onClick={onClose}
|
{!isInputFocused && (
|
||||||
className="w-full mt-2 py-2 text-sm text-dark-400 hover:text-dark-200"
|
<button
|
||||||
>
|
type="button"
|
||||||
{t('common.cancel')}
|
onClick={onClose}
|
||||||
</button>
|
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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user