bento-grids v1

в целом все хорошо, немного доправить и переходить к light theme
This commit is contained in:
kinvsh
2026-01-21 01:30:14 +05:00
parent 11fd2876b2
commit 7272f0a6c1
5 changed files with 347 additions and 164 deletions

View File

@@ -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"

View File

@@ -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">

View File

@@ -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
} }

View File

@@ -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',
}, },
}, },
] ]

View File

@@ -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>