diff --git a/assets/images/fake-payment.png b/assets/images/fake-payment.png new file mode 100644 index 0000000..9db467d Binary files /dev/null and b/assets/images/fake-payment.png differ diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 98022e5..43472ac 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -26,6 +26,7 @@ import DailyReward from './pages/DailyReward'; import DailyQuests from './pages/DailyQuests'; import Settings from './pages/Settings'; import Inventory from './pages/Inventory'; +import FakePaymentPage from './pages/FakePaymentPage'; import { TrayBridge } from './utils/TrayBridge'; import { API_BASE_URL } from './api'; @@ -398,6 +399,14 @@ const AppLayout = () => { } /> + + + + } + /> diff --git a/src/renderer/components/CoinsDisplay.tsx b/src/renderer/components/CoinsDisplay.tsx index cd2b920..ba1bbc9 100644 --- a/src/renderer/components/CoinsDisplay.tsx +++ b/src/renderer/components/CoinsDisplay.tsx @@ -21,6 +21,9 @@ interface CoinsDisplayProps { backgroundColor?: string; textColor?: string; + onClick?: () => void; + disableRefreshOnClick?: boolean; + sx?: SxProps; } @@ -40,6 +43,9 @@ export default function CoinsDisplay({ backgroundColor = 'rgba(0, 0, 0, 0.2)', textColor = 'white', + onClick, + disableRefreshOnClick = false, + sx, }: CoinsDisplayProps) { const [isLoading, setIsLoading] = useState(false); @@ -61,6 +67,14 @@ export default function CoinsDisplay({ } }; + const handleClick = () => { + // 1) если передали внешний обработчик — выполняем его + if (onClick) onClick(); + + // 2) опционально оставляем обновление баланса по клику + if (!disableRefreshOnClick && username) fetchCoinsData(); + }; + const [coins, setCoins] = useState(() => { // 1) если пришло значение извне — оно приоритетнее if (externalValue !== undefined) return externalValue; @@ -76,7 +90,8 @@ export default function CoinsDisplay({ useEffect(() => { const handler = () => setSettingsVersion((v) => v + 1); window.addEventListener('settings-updated', handler as EventListener); - return () => window.removeEventListener('settings-updated', handler as EventListener); + return () => + window.removeEventListener('settings-updated', handler as EventListener); }, []); const isTooltipDisabledBySettings = useMemo(() => { @@ -191,7 +206,7 @@ export default function CoinsDisplay({ borderRadius: sizes.borderRadius, padding: sizes.containerPadding, border: '1px solid rgba(255, 255, 255, 0.1)', - cursor: tooltipEnabled ? 'help' : 'default', + cursor: onClick ? 'pointer' : tooltipEnabled ? 'help' : 'default', // можно оставить лёгкий намёк на загрузку, но без "пульса" текста opacity: isLoading ? 0.85 : 1, @@ -199,7 +214,7 @@ export default function CoinsDisplay({ ...sx, }} - onClick={username ? handleRefresh : undefined} + onClick={handleClick} title={username ? 'Нажмите для обновления' : undefined} > {showIcon && ( diff --git a/src/renderer/components/PageHeader.tsx b/src/renderer/components/PageHeader.tsx index b689b27..db83526 100644 --- a/src/renderer/components/PageHeader.tsx +++ b/src/renderer/components/PageHeader.tsx @@ -34,6 +34,7 @@ export default function PageHeader() { path === '/marketplace' || path === '/profile' || path === '/inventory' || + path === '/fakepaymentpage' || path.startsWith('/launch') ) { return { title: '', subtitle: '', hidden: true }; @@ -43,7 +44,7 @@ export default function PageHeader() { return { title: 'Настройки', subtitle: 'Персонализация интерфейса и поведения лаунчера', - } + }; } if (path === '/news') { @@ -63,7 +64,8 @@ export default function PageHeader() { if (path.startsWith('/daily')) { return { title: 'Ежедневные награды', - subtitle: 'Ежедневный вход на сервер приносит бонусы и полезные награды!', + subtitle: + 'Ежедневный вход на сервер приносит бонусы и полезные награды!', }; } diff --git a/src/renderer/components/TopBar.tsx b/src/renderer/components/TopBar.tsx index ed37ee3..312b737 100644 --- a/src/renderer/components/TopBar.tsx +++ b/src/renderer/components/TopBar.tsx @@ -262,12 +262,13 @@ export default function TopBar({ onRegister, username }: TopBarProps) { }; window.addEventListener('settings-updated', handler as EventListener); - return () => window.removeEventListener('settings-updated', handler as EventListener); + return () => + window.removeEventListener('settings-updated', handler as EventListener); }, [updateGradientVars]); return ( navigate('/fakepaymentpage')} + disableRefreshOnClick={true} // чтобы клик не дёргал fetchCoins /> )} diff --git a/src/renderer/pages/FakePaymentPage.tsx b/src/renderer/pages/FakePaymentPage.tsx new file mode 100644 index 0000000..960ad61 --- /dev/null +++ b/src/renderer/pages/FakePaymentPage.tsx @@ -0,0 +1,518 @@ +// pages/TopUpPage.tsx +import { + Box, + Button, + Paper, + Stack, + TextField, + Typography, + ToggleButton, + ToggleButtonGroup, + CircularProgress, + LinearProgress, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import fakePaymentImg from '../../../assets/images/fake-payment.png'; +import { useNavigate } from 'react-router-dom'; + +type PayMethod = 'sbp' | 'card' | 'crypto' | 'other'; +type Stage = 'form' | 'processing' | 'done'; + +const STEPS: string[] = [ + 'Создаём счёт…', + 'Проверяем данные…', + 'Подключаем платёжный шлюз…', + 'Ожидаем подтверждение…', + 'Подписываем запрос…', + 'Проверяем лимиты…', + 'Синхронизируем баланс…', + 'Завершаем операцию…', + 'Почти готово…', +]; + +// ===== Styles “как Registration” ===== +const GRADIENT = + 'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)'; + +const GlassPaper = styled(Paper)(() => ({ + position: 'relative', + overflow: 'hidden', + borderRadius: 28, + background: 'rgba(0,0,0,0.35)', + border: '1px solid rgba(255,255,255,0.10)', + backdropFilter: 'blur(14px)', + boxShadow: '0 20px 60px rgba(0,0,0,0.45)', +})); + +const Glow = styled('div')(() => ({ + position: 'absolute', + inset: -2, + background: + 'radial-gradient(800px 300px at 20% 10%, rgba(242,113,33,0.22), transparent 60%),' + + 'radial-gradient(800px 300px at 80% 0%, rgba(233,64,205,0.18), transparent 55%),' + + 'radial-gradient(900px 420px at 50% 110%, rgba(138,35,135,0.20), transparent 60%)', + pointerEvents: 'none', +})); + +const GradientTitle = styled(Typography)(() => ({ + fontWeight: 900, + backgroundImage: + 'linear-gradient(136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + fontFamily: 'Benzin-Bold, sans-serif', +})); + +const GradientButton = styled(Button)(() => ({ + background: GRADIENT, + fontFamily: 'Benzin-Bold, sans-serif', + borderRadius: 999, + textTransform: 'none', + transition: 'transform 0.25s ease, filter 0.25s ease, box-shadow 0.25s ease', + boxShadow: '0 12px 30px rgba(0,0,0,0.35)', + '&:hover': { + transform: 'scale(1.04)', + filter: 'brightness(1.06)', + boxShadow: '0 16px 42px rgba(0,0,0,0.48)', + background: GRADIENT, + }, + '&:disabled': { + background: 'rgba(255,255,255,0.08)', + color: 'rgba(255,255,255,0.35)', + boxShadow: 'none', + }, +})); + +const StyledToggleButtonGroup = styled(ToggleButtonGroup)(() => ({ + borderRadius: 999, + overflow: 'hidden', + border: '1px solid rgba(255,255,255,0.10)', + background: 'rgba(255,255,255,0.06)', + '& .MuiToggleButton-root': { + border: 'none', + color: 'rgba(255,255,255,0.75)', + fontFamily: 'Benzin-Bold, sans-serif', + letterSpacing: '0.02em', + paddingTop: 10, + paddingBottom: 10, + transition: 'transform 0.2s ease, background 0.2s ease, color 0.2s ease', + }, + '& .MuiToggleButton-root:hover': { + background: 'rgba(255,255,255,0.08)', + transform: 'scale(1.02)', + }, + '& .MuiToggleButton-root.Mui-selected': { + color: '#fff', + background: GRADIENT, + }, + '& .MuiToggleButton-root.Mui-selected:hover': { + background: GRADIENT, + }, +})); + +const StyledTextField = styled(TextField)(() => ({ + '& .MuiInputLabel-root': { + color: 'rgba(255,255,255,0.65)', + }, + '& .MuiInputLabel-root.Mui-focused': { + color: 'rgba(255,255,255,0.9)', + }, + '& .MuiOutlinedInput-root': { + borderRadius: 20, + background: 'rgba(255,255,255,0.06)', + color: '#fff', + fontFamily: 'Benzin-Bold, sans-serif', + }, + '& .MuiOutlinedInput-notchedOutline': { + borderColor: 'rgba(255,255,255,0.10)', + }, + '& .MuiOutlinedInput-root:hover .MuiOutlinedInput-notchedOutline': { + borderColor: 'rgba(255,255,255,0.18)', + }, + '& .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: 'rgba(233,64,205,0.55)', + boxShadow: '0 0 0 6px rgba(233,64,205,0.12)', + }, + '& input': { + color: '#fff', + }, +})); + +export default function TopUpPage() { + const [coins, setCoins] = useState(100); + const [method, setMethod] = useState('sbp'); + const [stage, setStage] = useState('form'); + + const [stepText, setStepText] = useState('Обработка платежа…'); + const [progress, setProgress] = useState(0); + + const doneTimerRef = useRef(null); + const stepIntervalRef = useRef(null); + const navigate = useNavigate(); + + const rubles = useMemo(() => { + const safe = Number.isFinite(coins) ? coins : 0; + return Math.max(0, Math.floor(safe)); + }, [coins]); + + const methodLabel = useMemo(() => { + switch (method) { + case 'sbp': + return 'СБП'; + case 'card': + return 'Карта'; + case 'crypto': + return 'Crypto'; + default: + return 'Другое'; + } + }, [method]); + + const clearTimers = () => { + if (doneTimerRef.current !== null) { + window.clearTimeout(doneTimerRef.current); + doneTimerRef.current = null; + } + if (stepIntervalRef.current !== null) { + window.clearInterval(stepIntervalRef.current); + stepIntervalRef.current = null; + } + }; + + const startProcessing = () => { + clearTimers(); + setStage('processing'); + setProgress(0); + + const used = new Set(); + const pickStep = () => { + if (used.size >= STEPS.length) used.clear(); + let idx = Math.floor(Math.random() * STEPS.length); + while (used.has(idx)) idx = Math.floor(Math.random() * STEPS.length); + used.add(idx); + return STEPS[idx]; + }; + + setStepText(pickStep()); + + const totalMs = 1600 + Math.floor(Math.random() * 1600); // 1.6–3.2 + const stepsCount = 3 + Math.floor(Math.random() * 4); // 3–6 + + let ticks = 0; + stepIntervalRef.current = window.setInterval( + () => { + ticks += 1; + + setStepText(pickStep()); + setProgress((p) => { + const bump = 8 + Math.floor(Math.random() * 18); // 8..25 + return Math.min(95, p + bump); + }); + + if (ticks >= stepsCount && stepIntervalRef.current !== null) { + window.clearInterval(stepIntervalRef.current); + stepIntervalRef.current = null; + } + }, + 400 + Math.floor(Math.random() * 500), + ); // 400..900 + + doneTimerRef.current = window.setTimeout(() => { + setProgress(100); + setStepText('Готово!'); + window.setTimeout(() => setStage('done'), 250); + doneTimerRef.current = null; + }, totalMs); + }; + + const handlePay = () => { + if (rubles <= 0) return; + startProcessing(); + }; + + useEffect(() => { + return () => clearTimers(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // ===== Layout wrapper ===== + const PageCenter = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ); + + // ===== DONE ===== + if (stage === 'done') { + return ( + + + + + + + + Че реально думал донат добавили? + + + + Хуй тебе а не донат + + + + + + + ); + } + + // ===== PROCESSING ===== + if (stage === 'processing') { + return ( + + + + + + + + + + {stepText} + + + + + + {Math.round(progress)}% + + + + + + + {rubles.toLocaleString('ru-RU')} ₽ + + + + + + {rubles.toLocaleString('ru-RU')} монет + + + + + + {methodLabel} + + + + + + Пожалуйста, не закрывайте окно + + + + + ); + } + + // ===== FORM ===== + return ( + + + + + + Пополнение баланса + 1 ₽ = 1 монета + + + + setCoins(Number(e.target.value))} + inputProps={{ min: 0, step: 1 }} + fullWidth + /> + + + + Итого к оплате + + + {rubles.toLocaleString('ru-RU')} ₽ + + + Начислим: {rubles.toLocaleString('ru-RU')} монет + + + + + + + Способ оплаты + + + v && setMethod(v)} + fullWidth + > + СБП + Карта + Crypto + Другое + + + + Выбрано: {methodLabel} + + + + + Оплатить + + + + + ); +}