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
|
||||
return (
|
||||
<Wrapper>
|
||||
{/* Header - app selector */}
|
||||
<div className="p-4 border-b border-dark-800">
|
||||
<div className="flex items-center gap-3 px-4 pt-4 pb-2">
|
||||
<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
|
||||
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"
|
||||
|
||||
@@ -273,6 +273,7 @@ export function ThemeBentoPicker({
|
||||
const [hexInput, setHexInput] = useState(currentColors.accent)
|
||||
const [hasChanges, setHasChanges] = useState(false)
|
||||
|
||||
const [isPresetsOpen, setIsPresetsOpen] = useState(false)
|
||||
const [isAccentOpen, setIsAccentOpen] = useState(false)
|
||||
const [isDarkOpen, setIsDarkOpen] = useState(false)
|
||||
const [isLightOpen, setIsLightOpen] = useState(false)
|
||||
@@ -357,11 +358,14 @@ export function ThemeBentoPicker({
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<h3 className="text-xs font-medium text-dark-400 uppercase tracking-wide mb-3">
|
||||
{t('admin.theme.quickPresets', 'Quick Presets')}
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 auto-rows-fr gap-4 p-2">
|
||||
<CollapsibleSection
|
||||
title={t('admin.theme.quickPresets', 'Quick Presets')}
|
||||
icon={<PaletteIcon />}
|
||||
badge={`${COLOR_PRESETS.length}`}
|
||||
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) => (
|
||||
<div key={preset.id} className="min-h-[100px]" style={{ '--stagger': index } as React.CSSProperties}>
|
||||
<PresetCard
|
||||
@@ -372,7 +376,7 @@ export function ThemeBentoPicker({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<div className="space-y-2">
|
||||
<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 { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils/rateLimit'
|
||||
import type { PaymentMethod } from '../types'
|
||||
import BentoCard from './ui/BentoCard'
|
||||
|
||||
const TELEGRAM_LINK_REGEX = /^https?:\/\/t\.me\//i
|
||||
const isTelegramPaymentLink = (url: string): boolean => TELEGRAM_LINK_REGEX.test(url)
|
||||
@@ -172,7 +173,7 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
|
||||
|
||||
const modalContent = (
|
||||
<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={{
|
||||
paddingBottom: `max(1rem, env(safe-area-inset-bottom, 0px))`,
|
||||
}}
|
||||
@@ -180,99 +181,153 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
|
||||
>
|
||||
<div
|
||||
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()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 bg-dark-800/50">
|
||||
<span className="font-semibold text-dark-100">{methodName}</span>
|
||||
<button onClick={onClose} className="p-1.5 rounded-lg hover:bg-dark-700 text-dark-400">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<div className="flex items-center justify-between px-6 py-5 bg-dark-800/30 border-b border-dark-700/30">
|
||||
<span className="text-lg font-bold text-dark-50 tracking-tight">{methodName}</span>
|
||||
<button
|
||||
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" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
{/* 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 */}
|
||||
<div className="p-6 space-y-6 overflow-y-auto custom-scrollbar">
|
||||
{quickAmounts.length > 0 && (
|
||||
<div className="flex gap-2">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{quickAmounts.map((a) => {
|
||||
const val = getQuickValue(a)
|
||||
const isSelected = amount === val
|
||||
|
||||
return (
|
||||
<button
|
||||
<BentoCard
|
||||
key={a}
|
||||
as="button"
|
||||
type="button"
|
||||
onClick={() => { setAmount(val); inputRef.current?.blur() }}
|
||||
className={`flex-1 py-2 rounded-lg text-sm font-medium ${
|
||||
amount === val ? 'bg-accent-500 text-white' : 'bg-dark-800 text-dark-300'
|
||||
}`}
|
||||
className={`flex flex-col items-center justify-center py-4 px-2 transition-all duration-300 group
|
||||
${isSelected
|
||||
? 'border-accent-500 bg-accent-500/10 shadow-glow'
|
||||
: 'hover:bg-dark-800/80 hover:border-dark-600'
|
||||
}`}
|
||||
>
|
||||
{formatAmount(a, 0)}
|
||||
</button>
|
||||
<span className={`text-xl font-extrabold mb-0.5 ${isSelected ? 'text-accent-400' : 'text-dark-100 group-hover:text-white'}`}>
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="text-error-400 text-sm text-center py-1">{error}</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<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
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
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 ? (
|
||||
<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>
|
||||
) : (
|
||||
<>
|
||||
{t('balance.topUp')}
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span>{t('balance.topUp')}</span>
|
||||
{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>
|
||||
</div>
|
||||
@@ -285,3 +340,4 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
|
||||
}
|
||||
return modalContent
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface ColorPreset {
|
||||
description: string
|
||||
descriptionRu: string
|
||||
colors: ThemeColors
|
||||
// Preview colors for the card
|
||||
preview: {
|
||||
background: string
|
||||
accent: string
|
||||
@@ -28,10 +27,10 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
||||
darkSurface: '#0f172a',
|
||||
darkText: '#f1f5f9',
|
||||
darkTextSecondary: '#94a3b8',
|
||||
lightBackground: '#f8fafc',
|
||||
lightBackground: '#f1f5f9',
|
||||
lightSurface: '#ffffff',
|
||||
lightText: '#0f172a',
|
||||
lightTextSecondary: '#64748b',
|
||||
lightTextSecondary: '#475569',
|
||||
success: '#22c55e',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
@@ -56,7 +55,7 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
||||
darkTextSecondary: '#64748b',
|
||||
lightBackground: '#f0fdf4',
|
||||
lightSurface: '#ffffff',
|
||||
lightText: '#052e16',
|
||||
lightText: '#14532d',
|
||||
lightTextSecondary: '#166534',
|
||||
success: '#22c55e',
|
||||
warning: '#eab308',
|
||||
@@ -82,8 +81,8 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
||||
darkTextSecondary: '#a1a1aa',
|
||||
lightBackground: '#faf5ff',
|
||||
lightSurface: '#ffffff',
|
||||
lightText: '#1e1b29',
|
||||
lightTextSecondary: '#6b21a8',
|
||||
lightText: '#3b0764',
|
||||
lightTextSecondary: '#581c87',
|
||||
success: '#22c55e',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
@@ -108,8 +107,8 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
||||
darkTextSecondary: '#a3a3a3',
|
||||
lightBackground: '#fff7ed',
|
||||
lightSurface: '#ffffff',
|
||||
lightText: '#1c1917',
|
||||
lightTextSecondary: '#c2410c',
|
||||
lightText: '#7c2d12',
|
||||
lightTextSecondary: '#9a3412',
|
||||
success: '#22c55e',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
@@ -135,7 +134,7 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
||||
lightBackground: '#f0fdfa',
|
||||
lightSurface: '#ffffff',
|
||||
lightText: '#134e4a',
|
||||
lightTextSecondary: '#0f766e',
|
||||
lightTextSecondary: '#115e59',
|
||||
success: '#22c55e',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
@@ -158,18 +157,122 @@ export const COLOR_PRESETS: ColorPreset[] = [
|
||||
darkSurface: '#0f172a',
|
||||
darkText: '#f1f5f9',
|
||||
darkTextSecondary: '#94a3b8',
|
||||
lightBackground: '#F7E7CE',
|
||||
lightSurface: '#FEF9F0',
|
||||
lightText: '#1F1A12',
|
||||
lightTextSecondary: '#7D6B48',
|
||||
lightBackground: '#fefce8',
|
||||
lightSurface: '#ffffff',
|
||||
lightText: '#713f12',
|
||||
lightTextSecondary: '#92400e',
|
||||
success: '#22c55e',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
},
|
||||
preview: {
|
||||
background: '#F7E7CE',
|
||||
background: '#0a0f1a',
|
||||
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] =
|
||||
useState<{ message: string; amount: number } | null>(null)
|
||||
const [transactionsPage, setTransactionsPage] = useState(1)
|
||||
const [isHistoryOpen, setIsHistoryOpen] = useState(false)
|
||||
|
||||
const { data: transactions, isLoading } = useQuery<PaginatedResponse<Transaction>>({
|
||||
queryKey: ['transactions', transactionsPage],
|
||||
@@ -201,88 +202,104 @@ export default function Balance() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Transaction History */}
|
||||
<div className="bento-card">
|
||||
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('balance.transactionHistory')}</h2>
|
||||
<div className="bento-card overflow-hidden">
|
||||
<button
|
||||
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 ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="w-8 h-8 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : transactions?.items && transactions.items.length > 0 ? (
|
||||
<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>
|
||||
{isHistoryOpen && (
|
||||
<div className="mt-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="w-8 h-8 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-dark-800 flex items-center justify-center">
|
||||
<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?.items && transactions.items.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{transactions.items.map((tx) => {
|
||||
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'
|
||||
|
||||
{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>
|
||||
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>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-dark-800 flex items-center justify-center">
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user