mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
bento-grids v1
в целом все хорошо, немного доправить и переходить к light theme
This commit is contained in:
@@ -386,8 +386,11 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
|||||||
// Main view
|
// Main view
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
{/* Header - app selector */}
|
<div className="flex items-center gap-3 px-4 pt-4 pb-2">
|
||||||
<div className="p-4 border-b border-dark-800">
|
<h2 className="text-lg font-bold text-dark-100 flex-1">{t('subscription.connection.title')}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 pb-4 border-b border-dark-800">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAppSelector(true)}
|
onClick={() => setShowAppSelector(true)}
|
||||||
className="w-full flex items-center gap-4 p-3 rounded-2xl bg-dark-800/50 hover:bg-dark-800 transition-colors"
|
className="w-full flex items-center gap-4 p-3 rounded-2xl bg-dark-800/50 hover:bg-dark-800 transition-colors"
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ export function ThemeBentoPicker({
|
|||||||
const [hexInput, setHexInput] = useState(currentColors.accent)
|
const [hexInput, setHexInput] = useState(currentColors.accent)
|
||||||
const [hasChanges, setHasChanges] = useState(false)
|
const [hasChanges, setHasChanges] = useState(false)
|
||||||
|
|
||||||
|
const [isPresetsOpen, setIsPresetsOpen] = useState(false)
|
||||||
const [isAccentOpen, setIsAccentOpen] = useState(false)
|
const [isAccentOpen, setIsAccentOpen] = useState(false)
|
||||||
const [isDarkOpen, setIsDarkOpen] = useState(false)
|
const [isDarkOpen, setIsDarkOpen] = useState(false)
|
||||||
const [isLightOpen, setIsLightOpen] = useState(false)
|
const [isLightOpen, setIsLightOpen] = useState(false)
|
||||||
@@ -357,11 +358,14 @@ export function ThemeBentoPicker({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div>
|
<CollapsibleSection
|
||||||
<h3 className="text-xs font-medium text-dark-400 uppercase tracking-wide mb-3">
|
title={t('admin.theme.quickPresets', 'Quick Presets')}
|
||||||
{t('admin.theme.quickPresets', 'Quick Presets')}
|
icon={<PaletteIcon />}
|
||||||
</h3>
|
badge={`${COLOR_PRESETS.length}`}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 auto-rows-fr gap-4 p-2">
|
isOpen={isPresetsOpen}
|
||||||
|
onToggle={() => setIsPresetsOpen(!isPresetsOpen)}
|
||||||
|
>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 auto-rows-fr gap-4 p-1">
|
||||||
{COLOR_PRESETS.map((preset, index) => (
|
{COLOR_PRESETS.map((preset, index) => (
|
||||||
<div key={preset.id} className="min-h-[100px]" style={{ '--stagger': index } as React.CSSProperties}>
|
<div key={preset.id} className="min-h-[100px]" style={{ '--stagger': index } as React.CSSProperties}>
|
||||||
<PresetCard
|
<PresetCard
|
||||||
@@ -372,7 +376,7 @@ export function ThemeBentoPicker({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CollapsibleSection>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-xs font-medium text-dark-400 uppercase tracking-wide">
|
<h3 className="text-xs font-medium text-dark-400 uppercase tracking-wide">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { balanceApi } from '../api/balance'
|
|||||||
import { useCurrency } from '../hooks/useCurrency'
|
import { useCurrency } from '../hooks/useCurrency'
|
||||||
import { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils/rateLimit'
|
import { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils/rateLimit'
|
||||||
import type { PaymentMethod } from '../types'
|
import type { PaymentMethod } from '../types'
|
||||||
|
import BentoCard from './ui/BentoCard'
|
||||||
|
|
||||||
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)
|
||||||
@@ -172,7 +173,7 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
|
|||||||
|
|
||||||
const modalContent = (
|
const modalContent = (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black/70 backdrop-blur-sm z-[60] flex items-center justify-center p-4 overflow-hidden"
|
className="fixed inset-0 bg-dark-950/80 backdrop-blur-sm z-[60] flex items-center justify-center p-4 overflow-hidden animate-in fade-in duration-200"
|
||||||
style={{
|
style={{
|
||||||
paddingBottom: `max(1rem, env(safe-area-inset-bottom, 0px))`,
|
paddingBottom: `max(1rem, env(safe-area-inset-bottom, 0px))`,
|
||||||
}}
|
}}
|
||||||
@@ -180,99 +181,153 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-modal-content
|
data-modal-content
|
||||||
className="w-full max-w-sm bg-dark-900/95 backdrop-blur-xl rounded-3xl border border-dark-700/50 shadow-2xl overflow-hidden animate-scale-in"
|
className="w-full max-w-sm bg-dark-900/90 backdrop-blur-xl rounded-[32px] border border-dark-700/50 shadow-2xl overflow-hidden animate-scale-in flex flex-col max-h-[90vh]"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 bg-dark-800/50">
|
<div className="flex items-center justify-between px-6 py-5 bg-dark-800/30 border-b border-dark-700/30">
|
||||||
<span className="font-semibold text-dark-100">{methodName}</span>
|
<span className="text-lg font-bold text-dark-50 tracking-tight">{methodName}</span>
|
||||||
<button onClick={onClose} className="p-1.5 rounded-lg hover:bg-dark-700 text-dark-400">
|
<button
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
onClick={onClose}
|
||||||
|
className="p-2 -mr-2 rounded-xl hover:bg-dark-700/50 text-dark-400 hover:text-dark-100 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
||||||
<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>
|
||||||
|
|
||||||
<div className="p-4 space-y-3">
|
<div className="p-6 space-y-6 overflow-y-auto custom-scrollbar">
|
||||||
{/* Payment options */}
|
|
||||||
{hasOptions && method.options && (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{method.options.map((opt) => (
|
|
||||||
<button
|
|
||||||
key={opt.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedOption(opt.id)}
|
|
||||||
className={`flex-1 py-2 px-3 rounded-lg text-sm font-medium transition-all ${
|
|
||||||
selectedOption === opt.id
|
|
||||||
? 'bg-accent-500 text-white'
|
|
||||||
: 'bg-dark-800 text-dark-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{opt.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Amount input */}
|
|
||||||
<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)}`}
|
|
||||||
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"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-dark-500 font-medium">
|
|
||||||
{currencySymbol}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick amounts */}
|
|
||||||
{quickAmounts.length > 0 && (
|
{quickAmounts.length > 0 && (
|
||||||
<div className="flex gap-2">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{quickAmounts.map((a) => {
|
{quickAmounts.map((a) => {
|
||||||
const val = getQuickValue(a)
|
const val = getQuickValue(a)
|
||||||
|
const isSelected = amount === val
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<BentoCard
|
||||||
key={a}
|
key={a}
|
||||||
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setAmount(val); inputRef.current?.blur() }}
|
onClick={() => { setAmount(val); inputRef.current?.blur() }}
|
||||||
className={`flex-1 py-2 rounded-lg text-sm font-medium ${
|
className={`flex flex-col items-center justify-center py-4 px-2 transition-all duration-300 group
|
||||||
amount === val ? 'bg-accent-500 text-white' : 'bg-dark-800 text-dark-300'
|
${isSelected
|
||||||
}`}
|
? 'border-accent-500 bg-accent-500/10 shadow-glow'
|
||||||
|
: 'hover:bg-dark-800/80 hover:border-dark-600'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{formatAmount(a, 0)}
|
<span className={`text-xl font-extrabold mb-0.5 ${isSelected ? 'text-accent-400' : 'text-dark-100 group-hover:text-white'}`}>
|
||||||
</button>
|
{formatAmount(a, 0)}
|
||||||
|
</span>
|
||||||
|
<span className={`text-xs font-semibold uppercase tracking-wider ${isSelected ? 'text-accent-300/80' : 'text-dark-500 group-hover:text-dark-400'}`}>
|
||||||
|
{currencySymbol}
|
||||||
|
</span>
|
||||||
|
</BentoCard>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error */}
|
<div className="space-y-3">
|
||||||
{error && (
|
<div className="flex items-center justify-between px-1">
|
||||||
<div className="text-error-400 text-sm text-center py-1">{error}</div>
|
<label className="text-xs font-semibold text-dark-400 uppercase tracking-wider">
|
||||||
|
{t('balance.amount') || 'Amount'}
|
||||||
|
</label>
|
||||||
|
<span className="text-xs text-dark-500 font-medium">
|
||||||
|
{formatAmount(minRubles, 0)} – {formatAmount(maxRubles, 0)} {currencySymbol}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative group">
|
||||||
|
<div className="absolute -inset-0.5 bg-gradient-to-r from-accent-500/20 to-accent-600/20 rounded-2xl blur opacity-0 group-focus-within:opacity-100 transition duration-500" />
|
||||||
|
<div className="relative flex items-center bg-dark-800/50 border border-dark-700/50 rounded-2xl p-1 focus-within:bg-dark-800 focus-within:border-accent-500/50 transition-all duration-300">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="number"
|
||||||
|
inputMode="decimal"
|
||||||
|
value={amount}
|
||||||
|
onChange={(e) => setAmount(e.target.value)}
|
||||||
|
placeholder="0"
|
||||||
|
className="w-full h-14 pl-5 pr-12 text-2xl font-bold bg-transparent text-dark-50 placeholder:text-dark-600 focus:outline-none"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<div className="absolute right-5 pointer-events-none">
|
||||||
|
<span className="text-lg font-bold text-dark-400 group-focus-within:text-accent-400 transition-colors">
|
||||||
|
{currencySymbol}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasOptions && method.options && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="text-xs font-semibold text-dark-400 uppercase tracking-wider px-1">
|
||||||
|
Method Type
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-2 gap-2 bg-dark-800/30 p-1.5 rounded-2xl border border-dark-700/30">
|
||||||
|
{method.options.map((opt) => {
|
||||||
|
const isSelected = selectedOption === opt.id
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={opt.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedOption(opt.id)}
|
||||||
|
className={`relative py-2.5 px-3 rounded-xl text-sm font-bold transition-all duration-300 ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-dark-700 text-white shadow-lg'
|
||||||
|
: 'text-dark-400 hover:text-dark-200 hover:bg-dark-800/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isSelected && (
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-tr from-accent-500/10 to-transparent rounded-xl pointer-events-none" />
|
||||||
|
)}
|
||||||
|
{opt.name}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-error-500/10 border border-error-500/20 rounded-xl p-3 flex items-start gap-3 animate-in fade-in slide-in-from-top-2">
|
||||||
|
<div className="text-error-400 mt-0.5">
|
||||||
|
<svg className="w-5 h-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium text-error-200 leading-snug">{error}</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Submit */}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isPending || !amount}
|
disabled={isPending || !amount}
|
||||||
className="btn-primary w-full h-11 text-base font-semibold"
|
className={`
|
||||||
|
w-full h-14 rounded-2xl text-base font-bold tracking-wide transition-all duration-300
|
||||||
|
${isPending || !amount
|
||||||
|
? 'bg-dark-800 text-dark-500 cursor-not-allowed border border-dark-700'
|
||||||
|
: 'bg-accent-500 text-white shadow-lg shadow-accent-500/30 hover:bg-accent-400 hover:scale-[1.02] active:scale-[0.98]'
|
||||||
|
}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
<span>Processing...</span>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="flex items-center justify-center gap-2">
|
||||||
{t('balance.topUp')}
|
<span>{t('balance.topUp')}</span>
|
||||||
{amount && parseFloat(amount) > 0 && (
|
{amount && parseFloat(amount) > 0 && (
|
||||||
<span className="ml-2 opacity-80">{formatAmount(parseFloat(amount), currencyDecimals)} {currencySymbol}</span>
|
<>
|
||||||
|
<span className="w-1 h-1 bg-white/40 rounded-full" />
|
||||||
|
<span>{formatAmount(parseFloat(amount), currencyDecimals)} {currencySymbol}</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,3 +340,4 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
|
|||||||
}
|
}
|
||||||
return modalContent
|
return modalContent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export interface ColorPreset {
|
|||||||
description: string
|
description: string
|
||||||
descriptionRu: string
|
descriptionRu: string
|
||||||
colors: ThemeColors
|
colors: ThemeColors
|
||||||
// Preview colors for the card
|
|
||||||
preview: {
|
preview: {
|
||||||
background: string
|
background: string
|
||||||
accent: string
|
accent: string
|
||||||
@@ -28,10 +27,10 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
|||||||
darkSurface: '#0f172a',
|
darkSurface: '#0f172a',
|
||||||
darkText: '#f1f5f9',
|
darkText: '#f1f5f9',
|
||||||
darkTextSecondary: '#94a3b8',
|
darkTextSecondary: '#94a3b8',
|
||||||
lightBackground: '#f8fafc',
|
lightBackground: '#f1f5f9',
|
||||||
lightSurface: '#ffffff',
|
lightSurface: '#ffffff',
|
||||||
lightText: '#0f172a',
|
lightText: '#0f172a',
|
||||||
lightTextSecondary: '#64748b',
|
lightTextSecondary: '#475569',
|
||||||
success: '#22c55e',
|
success: '#22c55e',
|
||||||
warning: '#f59e0b',
|
warning: '#f59e0b',
|
||||||
error: '#ef4444',
|
error: '#ef4444',
|
||||||
@@ -56,7 +55,7 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
|||||||
darkTextSecondary: '#64748b',
|
darkTextSecondary: '#64748b',
|
||||||
lightBackground: '#f0fdf4',
|
lightBackground: '#f0fdf4',
|
||||||
lightSurface: '#ffffff',
|
lightSurface: '#ffffff',
|
||||||
lightText: '#052e16',
|
lightText: '#14532d',
|
||||||
lightTextSecondary: '#166534',
|
lightTextSecondary: '#166534',
|
||||||
success: '#22c55e',
|
success: '#22c55e',
|
||||||
warning: '#eab308',
|
warning: '#eab308',
|
||||||
@@ -82,8 +81,8 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
|||||||
darkTextSecondary: '#a1a1aa',
|
darkTextSecondary: '#a1a1aa',
|
||||||
lightBackground: '#faf5ff',
|
lightBackground: '#faf5ff',
|
||||||
lightSurface: '#ffffff',
|
lightSurface: '#ffffff',
|
||||||
lightText: '#1e1b29',
|
lightText: '#3b0764',
|
||||||
lightTextSecondary: '#6b21a8',
|
lightTextSecondary: '#581c87',
|
||||||
success: '#22c55e',
|
success: '#22c55e',
|
||||||
warning: '#f59e0b',
|
warning: '#f59e0b',
|
||||||
error: '#ef4444',
|
error: '#ef4444',
|
||||||
@@ -108,8 +107,8 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
|||||||
darkTextSecondary: '#a3a3a3',
|
darkTextSecondary: '#a3a3a3',
|
||||||
lightBackground: '#fff7ed',
|
lightBackground: '#fff7ed',
|
||||||
lightSurface: '#ffffff',
|
lightSurface: '#ffffff',
|
||||||
lightText: '#1c1917',
|
lightText: '#7c2d12',
|
||||||
lightTextSecondary: '#c2410c',
|
lightTextSecondary: '#9a3412',
|
||||||
success: '#22c55e',
|
success: '#22c55e',
|
||||||
warning: '#f59e0b',
|
warning: '#f59e0b',
|
||||||
error: '#ef4444',
|
error: '#ef4444',
|
||||||
@@ -135,7 +134,7 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
|||||||
lightBackground: '#f0fdfa',
|
lightBackground: '#f0fdfa',
|
||||||
lightSurface: '#ffffff',
|
lightSurface: '#ffffff',
|
||||||
lightText: '#134e4a',
|
lightText: '#134e4a',
|
||||||
lightTextSecondary: '#0f766e',
|
lightTextSecondary: '#115e59',
|
||||||
success: '#22c55e',
|
success: '#22c55e',
|
||||||
warning: '#f59e0b',
|
warning: '#f59e0b',
|
||||||
error: '#ef4444',
|
error: '#ef4444',
|
||||||
@@ -158,18 +157,122 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
|||||||
darkSurface: '#0f172a',
|
darkSurface: '#0f172a',
|
||||||
darkText: '#f1f5f9',
|
darkText: '#f1f5f9',
|
||||||
darkTextSecondary: '#94a3b8',
|
darkTextSecondary: '#94a3b8',
|
||||||
lightBackground: '#F7E7CE',
|
lightBackground: '#fefce8',
|
||||||
lightSurface: '#FEF9F0',
|
lightSurface: '#ffffff',
|
||||||
lightText: '#1F1A12',
|
lightText: '#713f12',
|
||||||
lightTextSecondary: '#7D6B48',
|
lightTextSecondary: '#92400e',
|
||||||
success: '#22c55e',
|
success: '#22c55e',
|
||||||
warning: '#f59e0b',
|
warning: '#f59e0b',
|
||||||
error: '#ef4444',
|
error: '#ef4444',
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
background: '#F7E7CE',
|
background: '#0a0f1a',
|
||||||
accent: '#b8860b',
|
accent: '#b8860b',
|
||||||
text: '#1F1A12',
|
text: '#f1f5f9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'quantum-teal',
|
||||||
|
name: 'Quantum Teal',
|
||||||
|
nameRu: 'Квантовый бирюзовый',
|
||||||
|
description: 'Bio-synthetic, futuristic fintech',
|
||||||
|
descriptionRu: 'Био-синтетический, футуристичный финтех',
|
||||||
|
colors: {
|
||||||
|
accent: '#0d9488',
|
||||||
|
darkBackground: '#0f172a',
|
||||||
|
darkSurface: '#1e293b',
|
||||||
|
darkText: '#f1f5f9',
|
||||||
|
darkTextSecondary: '#94a3b8',
|
||||||
|
lightBackground: '#f0fdfa',
|
||||||
|
lightSurface: '#ffffff',
|
||||||
|
lightText: '#134e4a',
|
||||||
|
lightTextSecondary: '#0f766e',
|
||||||
|
success: '#22c55e',
|
||||||
|
warning: '#f59e0b',
|
||||||
|
error: '#ef4444',
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
background: '#0f172a',
|
||||||
|
accent: '#0d9488',
|
||||||
|
text: '#f1f5f9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cosmic-violet',
|
||||||
|
name: 'Cosmic Violet',
|
||||||
|
nameRu: 'Космический фиолет',
|
||||||
|
description: 'Digital lavender, wellness vibes',
|
||||||
|
descriptionRu: 'Цифровая лаванда, атмосфера спокойствия',
|
||||||
|
colors: {
|
||||||
|
accent: '#7c3aed',
|
||||||
|
darkBackground: '#0b0d10',
|
||||||
|
darkSurface: '#18181b',
|
||||||
|
darkText: '#f4f4f5',
|
||||||
|
darkTextSecondary: '#a1a1aa',
|
||||||
|
lightBackground: '#faf5ff',
|
||||||
|
lightSurface: '#ffffff',
|
||||||
|
lightText: '#3b0764',
|
||||||
|
lightTextSecondary: '#5b21b6',
|
||||||
|
success: '#22c55e',
|
||||||
|
warning: '#f59e0b',
|
||||||
|
error: '#ef4444',
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
background: '#0b0d10',
|
||||||
|
accent: '#7c3aed',
|
||||||
|
text: '#f4f4f5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'solar-coral',
|
||||||
|
name: 'Solar Coral',
|
||||||
|
nameRu: 'Солнечный коралл',
|
||||||
|
description: 'Hyper-coral, high energy social',
|
||||||
|
descriptionRu: 'Гипер-коралловый, энергия соцсетей',
|
||||||
|
colors: {
|
||||||
|
accent: '#ea580c',
|
||||||
|
darkBackground: '#18181b',
|
||||||
|
darkSurface: '#27272a',
|
||||||
|
darkText: '#fafafa',
|
||||||
|
darkTextSecondary: '#a1a1aa',
|
||||||
|
lightBackground: '#fff7ed',
|
||||||
|
lightSurface: '#ffffff',
|
||||||
|
lightText: '#7c2d12',
|
||||||
|
lightTextSecondary: '#9a3412',
|
||||||
|
success: '#22c55e',
|
||||||
|
warning: '#f59e0b',
|
||||||
|
error: '#ef4444',
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
background: '#18181b',
|
||||||
|
accent: '#ea580c',
|
||||||
|
text: '#fafafa',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'frost-blue',
|
||||||
|
name: 'Frost Blue',
|
||||||
|
nameRu: 'Морозный синий',
|
||||||
|
description: 'Liquid chrome, enterprise trust',
|
||||||
|
descriptionRu: 'Жидкий хром, корпоративное доверие',
|
||||||
|
colors: {
|
||||||
|
accent: '#0284c7',
|
||||||
|
darkBackground: '#0b0d10',
|
||||||
|
darkSurface: '#1e293b',
|
||||||
|
darkText: '#f1f5f9',
|
||||||
|
darkTextSecondary: '#94a3b8',
|
||||||
|
lightBackground: '#f0f9ff',
|
||||||
|
lightSurface: '#ffffff',
|
||||||
|
lightText: '#0c4a6e',
|
||||||
|
lightTextSecondary: '#075985',
|
||||||
|
success: '#22c55e',
|
||||||
|
warning: '#f59e0b',
|
||||||
|
error: '#ef4444',
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
background: '#0b0d10',
|
||||||
|
accent: '#0284c7',
|
||||||
|
text: '#f1f5f9',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default function Balance() {
|
|||||||
const [promocodeSuccess, setPromocodeSuccess] =
|
const [promocodeSuccess, setPromocodeSuccess] =
|
||||||
useState<{ message: string; amount: number } | null>(null)
|
useState<{ message: string; amount: number } | null>(null)
|
||||||
const [transactionsPage, setTransactionsPage] = useState(1)
|
const [transactionsPage, setTransactionsPage] = useState(1)
|
||||||
|
const [isHistoryOpen, setIsHistoryOpen] = useState(false)
|
||||||
|
|
||||||
const { data: transactions, isLoading } = useQuery<PaginatedResponse<Transaction>>({
|
const { data: transactions, isLoading } = useQuery<PaginatedResponse<Transaction>>({
|
||||||
queryKey: ['transactions', transactionsPage],
|
queryKey: ['transactions', transactionsPage],
|
||||||
@@ -201,88 +202,104 @@ export default function Balance() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Transaction History */}
|
<div className="bento-card overflow-hidden">
|
||||||
<div className="bento-card">
|
<button
|
||||||
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('balance.transactionHistory')}</h2>
|
onClick={() => setIsHistoryOpen(!isHistoryOpen)}
|
||||||
|
className="w-full flex items-center justify-between text-left"
|
||||||
|
>
|
||||||
|
<h2 className="text-lg font-semibold text-dark-100">{t('balance.transactionHistory')}</h2>
|
||||||
|
<svg
|
||||||
|
className={`w-5 h-5 text-dark-400 transition-transform duration-200 ${isHistoryOpen ? 'rotate-180' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
{isLoading ? (
|
{isHistoryOpen && (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="mt-4">
|
||||||
<div className="w-8 h-8 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" />
|
{isLoading ? (
|
||||||
</div>
|
<div className="flex items-center justify-center py-12">
|
||||||
) : transactions?.items && transactions.items.length > 0 ? (
|
<div className="w-8 h-8 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" />
|
||||||
<div className="space-y-3">
|
|
||||||
{transactions.items.map((tx) => {
|
|
||||||
// API returns negative values for debits, positive for credits
|
|
||||||
const isPositive = tx.amount_rubles >= 0
|
|
||||||
const displayAmount = Math.abs(tx.amount_rubles)
|
|
||||||
const sign = isPositive ? '+' : '-'
|
|
||||||
const colorClass = isPositive ? 'text-success-400' : 'text-error-400'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={tx.id}
|
|
||||||
className="flex items-center justify-between p-4 rounded-xl bg-dark-800/30 border border-dark-700/30"
|
|
||||||
>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-3 mb-1">
|
|
||||||
<span className={getTypeBadge(tx.type)}>
|
|
||||||
{getTypeLabel(tx.type)}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-dark-500">
|
|
||||||
{new Date(tx.created_at).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{tx.description && (
|
|
||||||
<div className="text-sm text-dark-400">{tx.description}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={`text-lg font-semibold ${colorClass}`}>
|
|
||||||
{sign}{formatAmount(displayAmount)} {currencySymbol}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
) : transactions?.items && transactions.items.length > 0 ? (
|
||||||
})}
|
<div className="space-y-3">
|
||||||
</div>
|
{transactions.items.map((tx) => {
|
||||||
) : (
|
const isPositive = tx.amount_rubles >= 0
|
||||||
<div className="text-center py-12">
|
const displayAmount = Math.abs(tx.amount_rubles)
|
||||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-dark-800 flex items-center justify-center">
|
const sign = isPositive ? '+' : '-'
|
||||||
<svg className="w-8 h-8 text-dark-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
const colorClass = isPositive ? 'text-success-400' : 'text-error-400'
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div className="text-dark-400">{t('balance.noTransactions')}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{transactions && transactions.pages > 1 && (
|
return (
|
||||||
<div className="mt-4 flex items-center gap-3 flex-wrap text-sm text-dark-500">
|
<div
|
||||||
<button
|
key={tx.id}
|
||||||
type="button"
|
className="flex items-center justify-between p-4 rounded-xl bg-dark-800/30 border border-dark-700/30"
|
||||||
onClick={() => setTransactionsPage((prev) => Math.max(1, prev - 1))}
|
>
|
||||||
disabled={transactions.page <= 1}
|
<div className="flex-1">
|
||||||
className={`btn-secondary text-xs sm:text-sm flex-1 sm:flex-none min-w-[120px] ${
|
<div className="flex items-center gap-3 mb-1">
|
||||||
transactions.page <= 1 ? 'opacity-50 cursor-not-allowed' : ''
|
<span className={getTypeBadge(tx.type)}>
|
||||||
}`}
|
{getTypeLabel(tx.type)}
|
||||||
>
|
</span>
|
||||||
{t('common.back')}
|
<span className="text-xs text-dark-500">
|
||||||
</button>
|
{new Date(tx.created_at).toLocaleDateString()}
|
||||||
<div className="flex-1 text-center">
|
</span>
|
||||||
{t('balance.page', { current: transactions.page, total: transactions.pages })}
|
</div>
|
||||||
</div>
|
{tx.description && (
|
||||||
<button
|
<div className="text-sm text-dark-400">{tx.description}</div>
|
||||||
type="button"
|
)}
|
||||||
onClick={() =>
|
</div>
|
||||||
setTransactionsPage((prev) =>
|
<div className={`text-lg font-semibold ${colorClass}`}>
|
||||||
transactions.pages ? Math.min(transactions.pages, prev + 1) : prev + 1
|
{sign}{formatAmount(displayAmount)} {currencySymbol}
|
||||||
)
|
</div>
|
||||||
}
|
</div>
|
||||||
disabled={transactions.page >= transactions.pages}
|
)
|
||||||
className={`btn-secondary text-xs sm:text-sm flex-1 sm:flex-none min-w-[120px] ${
|
})}
|
||||||
transactions.page >= transactions.pages ? 'opacity-50 cursor-not-allowed' : ''
|
</div>
|
||||||
}`}
|
) : (
|
||||||
>
|
<div className="text-center py-12">
|
||||||
{t('common.next')}
|
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-dark-800 flex items-center justify-center">
|
||||||
</button>
|
<svg className="w-8 h-8 text-dark-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="text-dark-400">{t('balance.noTransactions')}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{transactions && transactions.pages > 1 && (
|
||||||
|
<div className="mt-4 flex items-center gap-3 flex-wrap text-sm text-dark-500">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTransactionsPage((prev) => Math.max(1, prev - 1))}
|
||||||
|
disabled={transactions.page <= 1}
|
||||||
|
className={`btn-secondary text-xs sm:text-sm flex-1 sm:flex-none min-w-[120px] ${
|
||||||
|
transactions.page <= 1 ? 'opacity-50 cursor-not-allowed' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t('common.back')}
|
||||||
|
</button>
|
||||||
|
<div className="flex-1 text-center">
|
||||||
|
{t('balance.page', { current: transactions.page, total: transactions.pages })}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setTransactionsPage((prev) =>
|
||||||
|
transactions.pages ? Math.min(transactions.pages, prev + 1) : prev + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
disabled={transactions.page >= transactions.pages}
|
||||||
|
className={`btn-secondary text-xs sm:text-sm flex-1 sm:flex-none min-w-[120px] ${
|
||||||
|
transactions.page >= transactions.pages ? 'opacity-50 cursor-not-allowed' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t('common.next')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user