add secret page

This commit is contained in:
2025-12-20 19:32:47 +05:00
parent b1d369e49d
commit 5f23adc9ae
6 changed files with 554 additions and 7 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -26,6 +26,7 @@ import DailyReward from './pages/DailyReward';
import DailyQuests from './pages/DailyQuests'; import DailyQuests from './pages/DailyQuests';
import Settings from './pages/Settings'; import Settings from './pages/Settings';
import Inventory from './pages/Inventory'; import Inventory from './pages/Inventory';
import FakePaymentPage from './pages/FakePaymentPage';
import { TrayBridge } from './utils/TrayBridge'; import { TrayBridge } from './utils/TrayBridge';
import { API_BASE_URL } from './api'; import { API_BASE_URL } from './api';
@ -398,6 +399,14 @@ const AppLayout = () => {
</AuthCheck> </AuthCheck>
} }
/> />
<Route
path="/fakepaymentpage"
element={
<AuthCheck>
<FakePaymentPage />
</AuthCheck>
}
/>
</Routes> </Routes>
</Box> </Box>
</Box> </Box>

View File

@ -21,6 +21,9 @@ interface CoinsDisplayProps {
backgroundColor?: string; backgroundColor?: string;
textColor?: string; textColor?: string;
onClick?: () => void;
disableRefreshOnClick?: boolean;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
} }
@ -40,6 +43,9 @@ export default function CoinsDisplay({
backgroundColor = 'rgba(0, 0, 0, 0.2)', backgroundColor = 'rgba(0, 0, 0, 0.2)',
textColor = 'white', textColor = 'white',
onClick,
disableRefreshOnClick = false,
sx, sx,
}: CoinsDisplayProps) { }: CoinsDisplayProps) {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
@ -61,6 +67,14 @@ export default function CoinsDisplay({
} }
}; };
const handleClick = () => {
// 1) если передали внешний обработчик — выполняем его
if (onClick) onClick();
// 2) опционально оставляем обновление баланса по клику
if (!disableRefreshOnClick && username) fetchCoinsData();
};
const [coins, setCoins] = useState<number>(() => { const [coins, setCoins] = useState<number>(() => {
// 1) если пришло значение извне — оно приоритетнее // 1) если пришло значение извне — оно приоритетнее
if (externalValue !== undefined) return externalValue; if (externalValue !== undefined) return externalValue;
@ -76,7 +90,8 @@ export default function CoinsDisplay({
useEffect(() => { useEffect(() => {
const handler = () => setSettingsVersion((v) => v + 1); const handler = () => setSettingsVersion((v) => v + 1);
window.addEventListener('settings-updated', handler as EventListener); 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(() => { const isTooltipDisabledBySettings = useMemo(() => {
@ -191,7 +206,7 @@ export default function CoinsDisplay({
borderRadius: sizes.borderRadius, borderRadius: sizes.borderRadius,
padding: sizes.containerPadding, padding: sizes.containerPadding,
border: '1px solid rgba(255, 255, 255, 0.1)', border: '1px solid rgba(255, 255, 255, 0.1)',
cursor: tooltipEnabled ? 'help' : 'default', cursor: onClick ? 'pointer' : tooltipEnabled ? 'help' : 'default',
// можно оставить лёгкий намёк на загрузку, но без "пульса" текста // можно оставить лёгкий намёк на загрузку, но без "пульса" текста
opacity: isLoading ? 0.85 : 1, opacity: isLoading ? 0.85 : 1,
@ -199,7 +214,7 @@ export default function CoinsDisplay({
...sx, ...sx,
}} }}
onClick={username ? handleRefresh : undefined} onClick={handleClick}
title={username ? 'Нажмите для обновления' : undefined} title={username ? 'Нажмите для обновления' : undefined}
> >
{showIcon && ( {showIcon && (

View File

@ -34,6 +34,7 @@ export default function PageHeader() {
path === '/marketplace' || path === '/marketplace' ||
path === '/profile' || path === '/profile' ||
path === '/inventory' || path === '/inventory' ||
path === '/fakepaymentpage' ||
path.startsWith('/launch') path.startsWith('/launch')
) { ) {
return { title: '', subtitle: '', hidden: true }; return { title: '', subtitle: '', hidden: true };
@ -43,7 +44,7 @@ export default function PageHeader() {
return { return {
title: 'Настройки', title: 'Настройки',
subtitle: 'Персонализация интерфейса и поведения лаунчера', subtitle: 'Персонализация интерфейса и поведения лаунчера',
} };
} }
if (path === '/news') { if (path === '/news') {
@ -63,7 +64,8 @@ export default function PageHeader() {
if (path.startsWith('/daily')) { if (path.startsWith('/daily')) {
return { return {
title: 'Ежедневные награды', title: 'Ежедневные награды',
subtitle: 'Ежедневный вход на сервер приносит бонусы и полезные награды!', subtitle:
'Ежедневный вход на сервер приносит бонусы и полезные награды!',
}; };
} }

View File

@ -262,12 +262,13 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
}; };
window.addEventListener('settings-updated', handler as EventListener); window.addEventListener('settings-updated', handler as EventListener);
return () => window.removeEventListener('settings-updated', handler as EventListener); return () =>
window.removeEventListener('settings-updated', handler as EventListener);
}, [updateGradientVars]); }, [updateGradientVars]);
return ( return (
<Box <Box
className={isAuthPage ? undefined : 'glass-ui'} className={isAuthPage ? undefined : 'glass-ui'}
sx={[ sx={[
{ {
display: 'flex', display: 'flex',
@ -454,6 +455,8 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
size="medium" size="medium"
autoUpdate={true} autoUpdate={true}
showTooltip={true} showTooltip={true}
onClick={() => navigate('/fakepaymentpage')}
disableRefreshOnClick={true} // чтобы клик не дёргал fetchCoins
/> />
)} )}

View File

@ -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<number>(100);
const [method, setMethod] = useState<PayMethod>('sbp');
const [stage, setStage] = useState<Stage>('form');
const [stepText, setStepText] = useState<string>('Обработка платежа…');
const [progress, setProgress] = useState<number>(0);
const doneTimerRef = useRef<number | null>(null);
const stepIntervalRef = useRef<number | null>(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<number>();
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.63.2
const stepsCount = 3 + Math.floor(Math.random() * 4); // 36
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 }) => (
<Box
sx={{
height: 'calc(100vh - 8vh)',
pt: '8vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
px: 2,
}}
>
{children}
</Box>
);
// ===== DONE =====
if (stage === 'done') {
return (
<PageCenter>
<GlassPaper sx={{ width: 'min(680px, 92vw)', p: 3 }}>
<Glow />
<Stack
spacing={2.2}
alignItems="center"
sx={{ position: 'relative' }}
>
<Box
component="img"
src={fakePaymentImg}
alt="payment"
sx={{
width: 'min(440px, 82vw)',
height: 'auto',
borderRadius: '24px',
boxShadow: '0 20px 60px rgba(0,0,0,0.55)',
}}
/>
<GradientTitle variant="h5" sx={{ textAlign: 'center' }}>
Че реально думал донат добавили?
</GradientTitle>
<Typography sx={{ opacity: 0.8, textAlign: 'center' }}>
Хуй тебе а не донат
</Typography>
<Button
variant="outlined"
onClick={() => {
navigate('/');
}}
sx={{
borderRadius: 999,
px: 3,
borderColor: 'rgba(255,255,255,0.18)',
color: 'rgba(255,255,255,0.85)',
textTransform: 'none',
'&:hover': {
borderColor: 'rgba(255,255,255,0.30)',
background: 'rgba(255,255,255,0.06)',
},
}}
>
Вернуться назад
</Button>
</Stack>
</GlassPaper>
</PageCenter>
);
}
// ===== PROCESSING =====
if (stage === 'processing') {
return (
<PageCenter>
<GlassPaper sx={{ width: 'min(680px, 92vw)', p: 3 }}>
<Glow />
<Stack
spacing={2.2}
alignItems="center"
sx={{ position: 'relative' }}
>
<Box
sx={{
width: 72,
height: 72,
borderRadius: '50%',
display: 'grid',
placeItems: 'center',
background: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.10)',
}}
>
<CircularProgress />
</Box>
<GradientTitle variant="h6" sx={{ textAlign: 'center' }}>
{stepText}
</GradientTitle>
<Box sx={{ width: '100%' }}>
<LinearProgress
variant="determinate"
value={progress}
sx={{
height: 10,
borderRadius: 999,
backgroundColor: 'rgba(255,255,255,0.08)',
'& .MuiLinearProgress-bar': {
borderRadius: 999,
backgroundImage: GRADIENT,
},
}}
/>
<Typography sx={{ fontSize: 12, opacity: 0.75, mt: 1 }}>
{Math.round(progress)}%
</Typography>
</Box>
<Box
sx={{
width: '100%',
display: 'flex',
gap: 1,
flexWrap: 'wrap',
justifyContent: 'center',
}}
>
<Box
sx={{
px: 1.5,
py: 0.8,
borderRadius: 999,
background: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.10)',
}}
>
<Typography sx={{ opacity: 0.85, fontSize: 13 }}>
{rubles.toLocaleString('ru-RU')}
</Typography>
</Box>
<Box
sx={{
px: 1.5,
py: 0.8,
borderRadius: 999,
background: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.10)',
}}
>
<Typography sx={{ opacity: 0.85, fontSize: 13 }}>
{rubles.toLocaleString('ru-RU')} монет
</Typography>
</Box>
<Box
sx={{
px: 1.5,
py: 0.8,
borderRadius: 999,
background: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.10)',
}}
>
<Typography sx={{ opacity: 0.85, fontSize: 13 }}>
{methodLabel}
</Typography>
</Box>
</Box>
<Typography
sx={{ fontSize: 12, opacity: 0.55, textAlign: 'center' }}
>
Пожалуйста, не закрывайте окно
</Typography>
</Stack>
</GlassPaper>
</PageCenter>
);
}
// ===== FORM =====
return (
<PageCenter>
<GlassPaper sx={{ width: 'min(680px, 92vw)', p: 3 }}>
<Glow />
<Stack spacing={2.2} sx={{ position: 'relative' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<GradientTitle variant="h5">Пополнение баланса</GradientTitle>
<Typography sx={{ opacity: 0.75 }}>1 = 1 монета</Typography>
</Box>
<Box
sx={{
display: 'grid',
gridTemplateColumns: { xs: '1fr', md: '1.3fr 1fr' },
gap: 2,
alignItems: 'start',
}}
>
<StyledTextField
label="Сколько монет нужно?"
type="number"
value={coins}
onChange={(e) => setCoins(Number(e.target.value))}
inputProps={{ min: 0, step: 1 }}
fullWidth
/>
<Box
sx={{
borderRadius: 20,
background: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.10)',
p: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography sx={{ opacity: 0.75, fontSize: 12 }}>
Итого к оплате
</Typography>
<Typography
sx={{
mt: 0.5,
fontSize: 26,
fontWeight: 900,
fontFamily: 'Benzin-Bold, sans-serif',
}}
>
{rubles.toLocaleString('ru-RU')}
</Typography>
<Typography sx={{ opacity: 0.65, fontSize: 12, mt: 0.5 }}>
Начислим: {rubles.toLocaleString('ru-RU')} монет
</Typography>
</Box>
</Box>
<Box>
<Typography sx={{ mb: 1, fontWeight: 700, opacity: 0.9 }}>
Способ оплаты
</Typography>
<StyledToggleButtonGroup
value={method}
exclusive
onChange={(_, v) => v && setMethod(v)}
fullWidth
>
<ToggleButton value="sbp">СБП</ToggleButton>
<ToggleButton value="card">Карта</ToggleButton>
<ToggleButton value="crypto">Crypto</ToggleButton>
<ToggleButton value="other">Другое</ToggleButton>
</StyledToggleButtonGroup>
<Typography sx={{ mt: 1, fontSize: 12, opacity: 0.55 }}>
Выбрано: {methodLabel}
</Typography>
</Box>
<GradientButton
variant="contained"
size="large"
onClick={handlePay}
disabled={rubles <= 0}
sx={{
height: 52,
fontSize: 16,
}}
>
Оплатить
</GradientButton>
</Stack>
</GlassPaper>
</PageCenter>
);
}