diff --git a/.gitignore b/.gitignore index 52b5bce..d3b91dc 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ coverage/ tmp/ temp/ miniapp/ + +# AI & Agents context +.ai/ diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index 3b5ccb4..a0a10dc 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -145,23 +145,16 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { queryFn: () => subscriptionApi.getAppConfig(), }) - // Detect platform ONCE on mount (stable reference) const detectedPlatform = useMemo(() => detectPlatform(), []) - // Set initial app based on detected platform - AFTER appConfig loads useEffect(() => { if (!appConfig?.platforms || selectedApp) return - - // Priority: detected platform > first available platform let platform = detectedPlatform if (!platform || !appConfig.platforms[platform]?.length) { platform = platformOrder.find(p => appConfig.platforms[p]?.length > 0) || null } - if (!platform || !appConfig.platforms[platform]?.length) return - const apps = appConfig.platforms[platform] - // Select featured app or first app for the detected platform const app = apps.find(a => a.isFeatured) || apps[0] if (app) setSelectedApp(app) }, [appConfig, detectedPlatform, selectedApp]) @@ -174,7 +167,6 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { setShowAppSelector(false) }, []) - // Keyboard: Escape to close (PC) useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { @@ -187,7 +179,6 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { return () => document.removeEventListener('keydown', handleKeyDown) }, [handleClose, handleBack, showAppSelector]) - // Telegram back button (Android) useEffect(() => { if (!webApp?.BackButton) return const handler = showAppSelector ? handleBack : handleClose @@ -199,7 +190,6 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { } }, [webApp, handleClose, handleBack, showAppSelector]) - // Scroll lock useEffect(() => { document.body.style.overflow = 'hidden' return () => { document.body.style.overflow = '' } @@ -214,7 +204,6 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { const availablePlatforms = useMemo(() => { if (!appConfig?.platforms) return [] const available = platformOrder.filter(key => appConfig.platforms[key]?.length > 0) - // Put detected platform first if (detectedPlatform && available.includes(detectedPlatform)) { return [detectedPlatform, ...available.filter(p => p !== detectedPlatform)] } @@ -256,7 +245,6 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { // Wrapper component const Wrapper = ({ children }: { children: React.ReactNode }) => { if (isMobile) { - // Mobile fullscreen const content = (
- {/* Header */}

{t('subscription.connection.selectApp')}

- - {/* Apps grouped by platform */}
{availablePlatforms.map(platform => { const apps = appConfig.platforms[platform] if (!apps?.length) return null const isCurrentPlatform = platform === detectedPlatform - return (
- {/* Platform header */}
{platformNames[platform] || platform} @@ -358,8 +341,6 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { )}
- - {/* Apps for this platform */}
{apps.map(app => (
- - {/* App selector button */}
- {/* Steps */}
- {/* Step 1: Install */} {selectedApp?.installationStep && (
@@ -449,7 +425,6 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
)} - {/* Step 2: Add subscription */} {selectedApp?.addSubscriptionStep && (
@@ -457,9 +432,7 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) { {t('subscription.connection.addSubscription')}

{getLocalizedText(selectedApp.addSubscriptionStep.description)}

-
- {/* Connect button */} {selectedApp.deepLink && ( )} - - {/* Copy link */} + ) +} + +function HSLSlider({ + label, + value, + onChange, + max, + gradient, + suffix = '', +}: { + label: string + value: number + onChange: (value: number) => void + max: number + gradient: string + suffix?: string +}) { + return ( +
+
+ + + {value} + {suffix} + +
+ onChange(parseInt(e.target.value))} + className="w-full h-2.5 rounded-full appearance-none cursor-pointer" + style={{ background: gradient }} + /> +
+ ) +} + +function CompactColorInput({ + label, + value, + onChange, +}: { + label: string + value: string + onChange: (color: string) => void +}) { + const [localValue, setLocalValue] = useState(value) + const [isEditing, setIsEditing] = useState(false) + + useEffect(() => { + setLocalValue(value) + }, [value]) + + const handleChange = (newValue: string) => { + let formatted = newValue.toUpperCase() + if (!formatted.startsWith('#')) { + formatted = '#' + formatted + } + setLocalValue(formatted) + if (isValidHex(formatted)) { + onChange(formatted) + } + } + + const handleBlur = () => { + setIsEditing(false) + if (!isValidHex(localValue)) { + setLocalValue(value) + } + } + + return ( +
+ + )} +
+
+ ) +} + +function CollapsibleSection({ + title, + icon, + isOpen, + onToggle, + children, + badge, +}: { + title: string + icon: React.ReactNode + isOpen: boolean + onToggle: () => void + children: React.ReactNode + badge?: string +}) { + return ( +
+ + +
+
+
{children}
+
+
+
+ ) +} + +export function ThemeBentoPicker({ + currentColors, + onColorsChange, + onSave, + isSaving, +}: ThemeBentoPickerProps) { + const { t } = useTranslation() + + const [hsl, setHsl] = useState(() => hexToHsl(currentColors.accent)) + 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) + const [isStatusOpen, setIsStatusOpen] = useState(false) + + const selectedPresetId = useMemo(() => { + const match = COLOR_PRESETS.find( + (p) => + p.colors.accent.toLowerCase() === currentColors.accent.toLowerCase() && + p.colors.darkBackground.toLowerCase() === currentColors.darkBackground.toLowerCase() && + p.colors.lightBackground.toLowerCase() === currentColors.lightBackground.toLowerCase() + ) + return match?.id ?? null + }, [currentColors.accent, currentColors.darkBackground, currentColors.lightBackground]) + + useEffect(() => { + setHsl(hexToHsl(currentColors.accent)) + setHexInput(currentColors.accent) + }, [currentColors.accent]) + + const updateColor = useCallback( + (key: keyof ThemeColors, value: string) => { + const newColors = { ...currentColors, [key]: value } + onColorsChange(newColors) + applyThemeColors(newColors) + setHasChanges(true) + }, + [currentColors, onColorsChange] + ) + + const updateAccentFromHsl = useCallback( + (newHsl: HSLColor) => { + setHsl(newHsl) + const newHex = hslToHex(newHsl.h, newHsl.s, newHsl.l) + setHexInput(newHex) + updateColor('accent', newHex) + }, + [updateColor] + ) + + const handleHexInputChange = (value: string) => { + setHexInput(value) + if (isValidHex(value)) { + const newHsl = hexToHsl(value) + setHsl(newHsl) + updateColor('accent', value) + } + } + + const handlePresetSelect = (preset: ColorPreset) => { + onColorsChange(preset.colors) + applyThemeColors(preset.colors) + setHasChanges(true) + } + + const hueGradient = useMemo(() => { + return `linear-gradient(to right, + hsl(0, ${hsl.s}%, ${hsl.l}%), + hsl(60, ${hsl.s}%, ${hsl.l}%), + hsl(120, ${hsl.s}%, ${hsl.l}%), + hsl(180, ${hsl.s}%, ${hsl.l}%), + hsl(240, ${hsl.s}%, ${hsl.l}%), + hsl(300, ${hsl.s}%, ${hsl.l}%), + hsl(360, ${hsl.s}%, ${hsl.l}%) + )` + }, [hsl.s, hsl.l]) + + const saturationGradient = useMemo(() => { + return `linear-gradient(to right, + hsl(${hsl.h}, 0%, ${hsl.l}%), + hsl(${hsl.h}, 100%, ${hsl.l}%) + )` + }, [hsl.h, hsl.l]) + + const lightnessGradient = useMemo(() => { + return `linear-gradient(to right, + hsl(${hsl.h}, ${hsl.s}%, 0%), + hsl(${hsl.h}, ${hsl.s}%, 50%), + hsl(${hsl.h}, ${hsl.s}%, 100%) + )` + }, [hsl.h, hsl.s]) + + return ( +
+ } + badge={`${COLOR_PRESETS.length}`} + isOpen={isPresetsOpen} + onToggle={() => setIsPresetsOpen(!isPresetsOpen)} + > +
+ {COLOR_PRESETS.map((preset, index) => ( +
+ handlePresetSelect(preset)} + /> +
+ ))} +
+
+ +
+

+ {t('admin.theme.customizeColors', 'Customize Colors')} +

+ + } + badge={hexInput.toUpperCase()} + isOpen={isAccentOpen} + onToggle={() => setIsAccentOpen(!isAccentOpen)} + > +
+
+
+
+ {hexInput.toUpperCase()} +
+
+ + updateAccentFromHsl({ ...hsl, h })} + max={360} + gradient={hueGradient} + suffix="°" + /> + + updateAccentFromHsl({ ...hsl, s })} + max={100} + gradient={saturationGradient} + suffix="%" + /> + + updateAccentFromHsl({ ...hsl, l })} + max={100} + gradient={lightnessGradient} + suffix="%" + /> + +
+ + handleHexInputChange(e.target.value)} + placeholder="#3b82f6" + maxLength={7} + className="input w-full text-sm font-mono uppercase" + /> +
+
+ + + } + isOpen={isDarkOpen} + onToggle={() => setIsDarkOpen(!isDarkOpen)} + > +
+ updateColor('darkBackground', c)} + /> + updateColor('darkSurface', c)} + /> + updateColor('darkText', c)} + /> + updateColor('darkTextSecondary', c)} + /> +
+
+ + } + isOpen={isLightOpen} + onToggle={() => setIsLightOpen(!isLightOpen)} + > +
+ updateColor('lightBackground', c)} + /> + updateColor('lightSurface', c)} + /> + updateColor('lightText', c)} + /> + updateColor('lightTextSecondary', c)} + /> +
+
+ + } + isOpen={isStatusOpen} + onToggle={() => setIsStatusOpen(!isStatusOpen)} + > +
+ updateColor('success', c)} + /> + updateColor('warning', c)} + /> + updateColor('error', c)} + /> +
+
+
+ +
+

+ {t('theme.preview', 'Preview')} +

+
+ + + {t('theme.success', 'Success')} + {t('theme.warning', 'Warning')} + {t('theme.error', 'Error')} +
+
+ + {hasChanges && ( +
+ +
+ )} +
+ ) +} diff --git a/src/components/TicketNotificationBell.tsx b/src/components/TicketNotificationBell.tsx index 57f9f61..f270530 100644 --- a/src/components/TicketNotificationBell.tsx +++ b/src/components/TicketNotificationBell.tsx @@ -198,7 +198,7 @@ export default function TicketNotificationBell({ isAdmin = false }: TicketNotifi {/* Bell button */} + + {formatAmount(a, 0)} + + + {currencySymbol} + + ) })}
diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 4333c07..0b7fdd6 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -353,7 +353,7 @@ export default function Layout({ children }: LayoutProps) { {/* Header */}
{/* Logo */} setMobileMenuOpen(false)} className={`flex items-center gap-2.5 flex-shrink-0 ${!appName ? 'lg:mr-4' : ''}`}> -
+
{/* Always show letter as fallback */} - + {logoLetter} {/* Logo image with smooth fade-in */} @@ -423,7 +423,7 @@ export default function Layout({ children }: LayoutProps) { {/* Right side */} -
+
{/* Theme toggle button - only show if both themes are enabled */} {canToggle && ( + ) + } + + const { onClick } = props as BentoCardDivProps + return ( +
+ {children} +
+ ) +}) + +BentoCard.displayName = 'BentoCard' + +export default BentoCard diff --git a/src/components/ui/BentoSkeleton.tsx b/src/components/ui/BentoSkeleton.tsx new file mode 100644 index 0000000..d7e47a0 --- /dev/null +++ b/src/components/ui/BentoSkeleton.tsx @@ -0,0 +1,28 @@ +interface BentoSkeletonProps { + className?: string + count?: number +} + +export default function BentoSkeleton({ className = '', count = 1 }: BentoSkeletonProps) { + const baseClasses = `animate-pulse bg-dark-800/50 border border-dark-700/30 rounded-[var(--bento-radius,24px)] min-h-[160px] w-full ${className}` + + if (count > 1) { + return ( + <> + {Array.from({ length: count }).map((_, i) => ( +
+ ))} + + ) + } + + return ( +
+ ) +} diff --git a/src/data/colorPresets.ts b/src/data/colorPresets.ts new file mode 100644 index 0000000..0cee321 --- /dev/null +++ b/src/data/colorPresets.ts @@ -0,0 +1,282 @@ +import { ThemeColors } from '../types/theme' + +export interface ColorPreset { + id: string + name: string + nameRu: string + description: string + descriptionRu: string + colors: ThemeColors + preview: { + background: string + accent: string + text: string + } +} + +export const COLOR_PRESETS: ColorPreset[] = [ + { + id: 'electric-blue', + name: 'Electric Blue', + nameRu: 'Электрик', + description: 'Classic tech blue, reliable and clean', + descriptionRu: 'Классический технологичный синий', + colors: { + accent: '#3b82f6', + darkBackground: '#0a0f1a', + darkSurface: '#0f172a', + darkText: '#f1f5f9', + darkTextSecondary: '#94a3b8', + lightBackground: '#f1f5f9', + lightSurface: '#ffffff', + lightText: '#0f172a', + lightTextSecondary: '#475569', + success: '#22c55e', + warning: '#f59e0b', + error: '#ef4444', + }, + preview: { + background: '#0a0f1a', + accent: '#3b82f6', + text: '#f1f5f9', + }, + }, + { + id: 'toxic-neon', + name: 'Toxic Neon', + nameRu: 'Токсичный неон', + description: 'Cyberpunk vibes, high energy', + descriptionRu: 'Киберпанк атмосфера, высокая энергия', + colors: { + accent: '#22c55e', + darkBackground: '#030712', + darkSurface: '#0a0f14', + darkText: '#e2e8f0', + darkTextSecondary: '#64748b', + lightBackground: '#f0fdf4', + lightSurface: '#ffffff', + lightText: '#14532d', + lightTextSecondary: '#166534', + success: '#22c55e', + warning: '#eab308', + error: '#ef4444', + }, + preview: { + background: '#030712', + accent: '#22c55e', + text: '#e2e8f0', + }, + }, + { + id: 'royal-purple', + name: 'Royal Purple', + nameRu: 'Королевский пурпур', + description: 'Premium, sophisticated, Stripe-like', + descriptionRu: 'Премиальный, утончённый, как Stripe', + colors: { + accent: '#8b5cf6', + darkBackground: '#0c0a14', + darkSurface: '#13111c', + darkText: '#f1f0f5', + darkTextSecondary: '#a1a1aa', + lightBackground: '#faf5ff', + lightSurface: '#ffffff', + lightText: '#3b0764', + lightTextSecondary: '#581c87', + success: '#22c55e', + warning: '#f59e0b', + error: '#ef4444', + }, + preview: { + background: '#0c0a14', + accent: '#8b5cf6', + text: '#f1f0f5', + }, + }, + { + id: 'sunset-orange', + name: 'Sunset Orange', + nameRu: 'Закатный оранж', + description: 'Warm, energetic, action-oriented', + descriptionRu: 'Тёплый, энергичный, призыв к действию', + colors: { + accent: '#f97316', + darkBackground: '#0f0906', + darkSurface: '#1a120d', + darkText: '#fef3e2', + darkTextSecondary: '#a3a3a3', + lightBackground: '#fff7ed', + lightSurface: '#ffffff', + lightText: '#7c2d12', + lightTextSecondary: '#9a3412', + success: '#22c55e', + warning: '#f59e0b', + error: '#ef4444', + }, + preview: { + background: '#0f0906', + accent: '#f97316', + text: '#fef3e2', + }, + }, + { + id: 'ocean-teal', + name: 'Ocean Teal', + nameRu: 'Океанский бирюзовый', + description: 'Calm, trustworthy, health-tech', + descriptionRu: 'Спокойный, надёжный, медтех', + colors: { + accent: '#14b8a6', + darkBackground: '#042f2e', + darkSurface: '#0d3d3b', + darkText: '#f0fdfa', + darkTextSecondary: '#5eead4', + lightBackground: '#f0fdfa', + lightSurface: '#ffffff', + lightText: '#134e4a', + lightTextSecondary: '#115e59', + success: '#22c55e', + warning: '#f59e0b', + error: '#ef4444', + }, + preview: { + background: '#042f2e', + accent: '#14b8a6', + text: '#f0fdfa', + }, + }, + { + id: 'champagne-gold', + name: 'Champagne Gold', + nameRu: 'Шампанское золото', + description: 'Luxury, premium, elegant', + descriptionRu: 'Роскошный, премиальный, элегантный', + colors: { + accent: '#b8860b', + darkBackground: '#0a0f1a', + darkSurface: '#0f172a', + darkText: '#f1f5f9', + darkTextSecondary: '#94a3b8', + lightBackground: '#fefce8', + lightSurface: '#ffffff', + lightText: '#713f12', + lightTextSecondary: '#92400e', + success: '#22c55e', + warning: '#f59e0b', + error: '#ef4444', + }, + preview: { + background: '#0a0f1a', + accent: '#b8860b', + 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', + }, + }, +] + +export function getPresetById(id: string): ColorPreset | undefined { + return COLOR_PRESETS.find((preset) => preset.id === id) +} diff --git a/src/pages/Balance.tsx b/src/pages/Balance.tsx index 4f3b5ee..6e0c10e 100644 --- a/src/pages/Balance.tsx +++ b/src/pages/Balance.tsx @@ -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>({ queryKey: ['transactions', transactionsPage], @@ -122,7 +123,7 @@ export default function Balance() {

{t('balance.title')}

{/* Balance Card */} -
+
{t('balance.currentBalance')}
{formatAmount(balanceData?.balance_rubles || 0)} @@ -131,7 +132,7 @@ export default function Balance() {
{/* Promo Code Section */} -
+

{t('balance.promocode.title')}

0 && ( -
+

{t('balance.topUpBalance')}

{paymentMethods.map((method) => { @@ -181,10 +182,10 @@ export default function Balance() { key={method.id} disabled={!method.is_available} onClick={() => method.is_available && setSelectedMethod(method)} - className={`p-4 rounded-xl border text-left transition-all ${ + className={`bento-card-hover p-4 text-left transition-all ${ method.is_available - ? 'border-dark-700/50 hover:border-accent-500/50 bg-dark-800/30 cursor-pointer' - : 'border-dark-800/30 bg-dark-900/30 opacity-50 cursor-not-allowed' + ? 'cursor-pointer' + : 'opacity-50 cursor-not-allowed' }`} >
{translatedName || method.name}
@@ -201,88 +202,104 @@ export default function Balance() {
)} - {/* Transaction History */} -
-

{t('balance.transactionHistory')}

+
+ - {isLoading ? ( -
-
-
- ) : transactions?.items && transactions.items.length > 0 ? ( -
- {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 ( -
-
-
- - {getTypeLabel(tx.type)} - - - {new Date(tx.created_at).toLocaleDateString()} - -
- {tx.description && ( -
{tx.description}
- )} -
-
- {sign}{formatAmount(displayAmount)} {currencySymbol} -
+ {isHistoryOpen && ( +
+ {isLoading ? ( +
+
- ) - })} -
- ) : ( -
-
- - - -
-
{t('balance.noTransactions')}
-
- )} + ) : transactions?.items && transactions.items.length > 0 ? ( +
+ {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 && ( -
- -
- {t('balance.page', { current: transactions.page, total: transactions.pages })} -
- + return ( +
+
+
+ + {getTypeLabel(tx.type)} + + + {new Date(tx.created_at).toLocaleDateString()} + +
+ {tx.description && ( +
{tx.description}
+ )} +
+
+ {sign}{formatAmount(displayAmount)} {currencySymbol} +
+
+ ) + })} +
+ ) : ( +
+
+ + + +
+
{t('balance.noTransactions')}
+
+ )} + + {transactions && transactions.pages > 1 && ( +
+ +
+ {t('balance.page', { current: transactions.page, total: transactions.pages })} +
+ +
+ )}
)}
diff --git a/src/pages/Contests.tsx b/src/pages/Contests.tsx index c0b8790..34fc3ac 100644 --- a/src/pages/Contests.tsx +++ b/src/pages/Contests.tsx @@ -86,8 +86,8 @@ export default function Contests() { {/* Game Modal */} {selectedContest && ( -
-
+
+
e.stopPropagation()}>

{selectedContest.name}