add component CoinsDisplay and rework CustomTooltip

This commit is contained in:
2025-12-07 18:35:14 +05:00
parent 52336f8960
commit a456925a08
4 changed files with 402 additions and 171 deletions

View File

@ -0,0 +1,233 @@
// CoinsDisplay.tsx
import { Box, Typography } from '@mui/material';
import CustomTooltip from './CustomTooltip';
import { useEffect, useState } from 'react';
import { fetchCoins } from '../api';
interface CoinsDisplayProps {
// Основные пропсы
value?: number; // Передаем значение напрямую
username?: string; // Или получаем по username из API
// Опции отображения
size?: 'small' | 'medium' | 'large';
showTooltip?: boolean;
tooltipText?: string;
showIcon?: boolean;
iconColor?: string;
// Опции обновления
autoUpdate?: boolean; // Автоматическое обновление из API
updateInterval?: number; // Интервал обновления в миллисекундах
// Стилизация
backgroundColor?: string;
textColor?: string;
}
export default function CoinsDisplay({
// Основные пропсы
value: externalValue,
username,
// Опции отображения
size = 'medium',
showTooltip = true,
tooltipText = 'Попы — внутриигровая валюта, начисляемая за время игры на серверах.',
showIcon = true,
iconColor = '#2bff00ff',
// Опции обновления
autoUpdate = false,
updateInterval = 60000,
// Стилизация
backgroundColor = 'rgba(0, 0, 0, 0.2)',
textColor = 'white',
}: CoinsDisplayProps) {
const [coins, setCoins] = useState<number>(externalValue || 0);
const [isLoading, setIsLoading] = useState<boolean>(false);
// Определяем размеры в зависимости от параметра size
const getSizes = () => {
switch (size) {
case 'small':
return {
containerPadding: '4px 8px',
iconSize: '16px',
fontSize: '12px',
borderRadius: '12px',
gap: '6px',
};
case 'large':
return {
containerPadding: '8px 16px',
iconSize: '28px',
fontSize: '18px',
borderRadius: '20px',
gap: '10px',
};
case 'medium':
default:
return {
containerPadding: '6px 12px',
iconSize: '24px',
fontSize: '16px',
borderRadius: '16px',
gap: '8px',
};
}
};
const sizes = getSizes();
// Функция для получения количества монет из API
const fetchCoinsData = async () => {
if (!username) return;
setIsLoading(true);
try {
const coinsData = await fetchCoins(username);
setCoins(coinsData.coins);
} catch (error) {
console.error('Ошибка при получении количества монет:', error);
} finally {
setIsLoading(false);
}
};
// Эффект для внешнего значения
useEffect(() => {
if (externalValue !== undefined) {
setCoins(externalValue);
}
}, [externalValue]);
// Эффект для API обновлений
useEffect(() => {
if (username && autoUpdate) {
fetchCoinsData();
// Создаем интервалы для периодического обновления данных
const coinsInterval = setInterval(fetchCoinsData, updateInterval);
return () => {
clearInterval(coinsInterval);
};
}
}, [username, autoUpdate, updateInterval]);
// Ручное обновление данных
const handleRefresh = () => {
if (username) {
fetchCoinsData();
}
};
// Форматирование числа с разделителями тысяч
const formatNumber = (num: number): string => {
return num.toLocaleString('ru-RU');
};
const coinsDisplay = (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: sizes.gap,
backgroundColor,
borderRadius: sizes.borderRadius,
padding: sizes.containerPadding,
border: '1px solid rgba(255, 255, 255, 0.1)',
cursor: showTooltip ? 'help' : 'default',
opacity: isLoading ? 0.7 : 1,
transition: 'opacity 0.2s ease',
}}
onClick={username ? handleRefresh : undefined}
title={username ? 'Нажмите для обновления' : undefined}
>
{showIcon && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: sizes.iconSize,
height: sizes.iconSize,
borderRadius: '50%',
backgroundColor: 'rgba(255, 255, 255, 0.1)',
}}
>
<Typography
sx={{
color: iconColor,
fontWeight: 'bold',
fontSize: `calc(${sizes.fontSize} * 0.8)`,
}}
>
P
</Typography>
</Box>
)}
<Typography
variant="body1"
sx={{
color: textColor,
fontWeight: 'bold',
fontSize: sizes.fontSize,
lineHeight: 1,
fontFamily: 'Benzin-Bold, sans-serif',
}}
>
{isLoading ? '...' : formatNumber(coins)}
</Typography>
</Box>
);
if (showTooltip) {
return (
<CustomTooltip
title={tooltipText}
arrow
placement="bottom"
TransitionProps={{ timeout: 300 }}
>
{coinsDisplay}
</CustomTooltip>
);
}
return coinsDisplay;
}
// Примеры использования в комментариях для разработчика:
/*
// Пример 1: Простое отображение числа
<CoinsDisplay value={1500} />
// Пример 2: Получение данных по username с автообновлением
<CoinsDisplay
username="player123"
autoUpdate={true}
updateInterval={30000} // обновлять каждые 30 секунд
/>
// Пример 3: Кастомная стилизация без иконки
<CoinsDisplay
value={9999}
size="small"
showIcon={false}
showTooltip={false}
backgroundColor="rgba(255, 100, 100, 0.2)"
textColor="#ffcc00"
/>
// Пример 4: Большой отображение для профиля
<CoinsDisplay
username="player123"
size="large"
tooltipText="Ваш текущий баланс"
iconColor="#00ffaa"
/>
*/

View File

@ -8,16 +8,28 @@ const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} /> <Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({ ))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: { [`& .${tooltipClasses.tooltip}`]: {
backgroundColor: 'rgba(0, 0, 0, 1)', backgroundColor: 'rgba(0, 0, 0, 0.9)',
color: 'white', color: '#fff',
maxWidth: 300, maxWidth: 300,
fontSize: '0.9vw', fontSize: '0.9vw',
border: '1px solid rgba(255, 77, 77, 0.5)', border: '1px solid rgba(242, 113, 33, 0.5)',
borderRadius: '1vw', borderRadius: '1vw',
padding: '1vw', padding: '1vw',
boxShadow: boxShadow: `
'0 0 1vw rgba(255, 77, 77, 0.3), inset 0.8vw -0.8vw 2vw rgba(255, 77, 77, 0.15)', 0 0 1.5vw rgba(242, 113, 33, 0.4),
0 0 0.5vw rgba(233, 64, 87, 0.3),
inset 0 0 0.5vw rgba(138, 35, 135, 0.2)
`,
fontFamily: 'Benzin-Bold', fontFamily: 'Benzin-Bold',
background: `
linear-gradient(
135deg,
rgba(0, 0, 0, 0.95) 0%,
rgba(20, 20, 20, 0.95) 100%
)
`,
position: 'relative',
zIndex: 1,
'&::before': { '&::before': {
content: '""', content: '""',
position: 'absolute', position: 'absolute',
@ -26,12 +38,37 @@ const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
right: 0, right: 0,
bottom: 0, bottom: 0,
borderRadius: '1vw', borderRadius: '1vw',
// background: 'linear-gradient(45deg, rgba(255, 77, 77, 0.1), transparent)', padding: '2px',
background: `
linear-gradient(
135deg,
rgba(242, 113, 33, 0.8) 0%,
rgba(233, 64, 87, 0.6) 50%,
rgba(138, 35, 135, 0.4) 100%
)
`,
WebkitMask: `
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0)
`,
WebkitMaskComposite: 'xor',
maskComposite: 'exclude',
zIndex: -1, zIndex: -1,
}, },
}, },
[`& .${tooltipClasses.arrow}`]: { [`& .${tooltipClasses.arrow}`]: {
color: 'rgba(255, 77, 77, 0.5)', color: 'rgba(242, 113, 33, 0.9)',
'&::before': {
background: `
linear-gradient(
135deg,
rgba(242, 113, 33, 0.9) 0%,
rgba(233, 64, 87, 0.7) 50%,
rgba(138, 35, 135, 0.5) 100%
)
`,
border: '1px solid rgba(242, 113, 33, 0.5)',
},
}, },
})); }));

View File

@ -6,6 +6,7 @@ import { useEffect, useRef, useState } from 'react';
import { Tooltip } from '@mui/material'; import { Tooltip } from '@mui/material';
import { fetchCoins } from '../api'; import { fetchCoins } from '../api';
import CustomTooltip from './CustomTooltip'; import CustomTooltip from './CustomTooltip';
import CoinsDisplay from './CoinsDisplay';
declare global { declare global {
interface Window { interface Window {
electron: { electron: {
@ -208,111 +209,118 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
}, },
}} }}
> >
<Tabs <CustomTooltip
value={value} title={'Покрути колесиком мыши чтобы увидеть больше кнопок'}
onChange={handleChange} arrow
aria-label="basic tabs example" placement="bottom"
variant="scrollable" TransitionProps={{ timeout: 100 }}
scrollButtons={false}
disableRipple={true}
sx={{ maxWidth: '42vw' }}
> >
<Tab <Tabs
label="Новости" value={value}
onChange={handleChange}
aria-label="basic tabs example"
variant="scrollable"
scrollButtons={false}
disableRipple={true} disableRipple={true}
onClick={() => { sx={{ maxWidth: '42vw' }}
setActivePage('news'); >
}} <Tab
sx={{ label="Новости"
color: 'white', disableRipple={true}
fontFamily: 'Benzin-Bold', onClick={() => {
fontSize: '0.7em', setActivePage('news');
'&.Mui-selected': { }}
color: 'rgba(255, 77, 77, 1)', sx={{
}, color: 'white',
'&:hover': { fontFamily: 'Benzin-Bold',
color: 'rgb(177, 52, 52)', fontSize: '0.7em',
}, '&.Mui-selected': {
transition: 'all 0.3s ease', color: 'rgba(255, 77, 77, 1)',
}} },
/> '&:hover': {
<Tab color: 'rgb(177, 52, 52)',
label="Версии" },
disableRipple={true} transition: 'all 0.3s ease',
onClick={() => { }}
setActivePage('versions'); />
}} <Tab
sx={{ label="Версии"
color: 'white', disableRipple={true}
fontFamily: 'Benzin-Bold', onClick={() => {
fontSize: '0.7em', setActivePage('versions');
'&.Mui-selected': { }}
color: 'rgba(255, 77, 77, 1)', sx={{
}, color: 'white',
'&:hover': { fontFamily: 'Benzin-Bold',
color: 'rgb(177, 52, 52)', fontSize: '0.7em',
}, '&.Mui-selected': {
transition: 'all 0.3s ease', color: 'rgba(255, 77, 77, 1)',
}} },
/> '&:hover': {
<Tab color: 'rgb(177, 52, 52)',
label="Профиль" },
disableRipple={true} transition: 'all 0.3s ease',
onClick={() => { }}
setActivePage('profile'); />
}} <Tab
sx={{ label="Профиль"
color: 'white', disableRipple={true}
fontFamily: 'Benzin-Bold', onClick={() => {
fontSize: '0.7em', setActivePage('profile');
'&.Mui-selected': { }}
color: 'rgba(255, 77, 77, 1)', sx={{
}, color: 'white',
'&:hover': { fontFamily: 'Benzin-Bold',
color: 'rgb(177, 52, 52)', fontSize: '0.7em',
}, '&.Mui-selected': {
transition: 'all 0.3s ease', color: 'rgba(255, 77, 77, 1)',
}} },
/> '&:hover': {
<Tab color: 'rgb(177, 52, 52)',
label="Магазин" },
disableRipple={true} transition: 'all 0.3s ease',
onClick={() => { }}
setActivePage('shop'); />
}} <Tab
sx={{ label="Магазин"
color: 'white', disableRipple={true}
fontFamily: 'Benzin-Bold', onClick={() => {
fontSize: '0.7em', setActivePage('shop');
'&.Mui-selected': { }}
color: 'rgba(255, 77, 77, 1)', sx={{
}, color: 'white',
'&:hover': { fontFamily: 'Benzin-Bold',
color: 'rgb(177, 52, 52)', fontSize: '0.7em',
}, '&.Mui-selected': {
transition: 'all 0.3s ease', color: 'rgba(255, 77, 77, 1)',
}} },
/> '&:hover': {
<Tab color: 'rgb(177, 52, 52)',
label="Рынок" },
disableRipple={true} transition: 'all 0.3s ease',
onClick={() => { }}
setActivePage('marketplace'); />
}} <Tab
sx={{ label="Рынок"
color: 'white', disableRipple={true}
fontFamily: 'Benzin-Bold', onClick={() => {
fontSize: '0.7em', setActivePage('marketplace');
'&.Mui-selected': { }}
color: 'rgba(255, 77, 77, 1)', sx={{
}, color: 'white',
'&:hover': { fontFamily: 'Benzin-Bold',
color: 'rgb(177, 52, 52)', fontSize: '0.7em',
}, '&.Mui-selected': {
transition: 'all 0.3s ease', color: 'rgba(255, 77, 77, 1)',
}} },
/> '&:hover': {
</Tabs> color: 'rgb(177, 52, 52)',
},
transition: 'all 0.3s ease',
}}
/>
</Tabs>
</CustomTooltip>
</Box> </Box>
)} )}
</Box> </Box>
@ -376,46 +384,12 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
)} )}
{/* Кнопка регистрации, если на странице логина */} {/* Кнопка регистрации, если на странице логина */}
{!isLoginPage && !isRegistrationPage && username && ( {!isLoginPage && !isRegistrationPage && username && (
<CustomTooltip <CoinsDisplay
title="Попы — внутриигровая валюта, начисляемая за время игры на серверах." username={username}
arrow size="medium"
placement="bottom" autoUpdate={true}
TransitionProps={{ timeout: 300 }} showTooltip={true}
> />
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '8px',
backgroundColor: 'rgba(0, 0, 0, 0.2)',
borderRadius: '16px',
padding: '6px 12px',
border: '1px solid rgba(255, 255, 255, 0.1)',
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
width: '24px',
height: '24px',
}}
>
<Typography sx={{ color: '#2bff00ff' }}>P</Typography>
</Box>
<Typography
variant="body1"
sx={{
color: 'white',
fontWeight: 'bold',
fontSize: '16px',
lineHeight: 1,
}}
>
{coins}
</Typography>
</Box>
</CustomTooltip>
)} )}
{isLoginPage && ( {isLoginPage && (
<Button <Button

View File

@ -29,6 +29,7 @@ import { useEffect, useState } from 'react';
import { FullScreenLoader } from '../components/FullScreenLoader'; import { FullScreenLoader } from '../components/FullScreenLoader';
import { getPlayerServer } from '../utils/playerOnlineCheck'; import { getPlayerServer } from '../utils/playerOnlineCheck';
import CaseRoulette from '../components/CaseRoulette'; import CaseRoulette from '../components/CaseRoulette';
import CoinsDisplay from '../components/CoinsDisplay';
function getRarityByWeight( function getRarityByWeight(
weight?: number, weight?: number,
@ -91,8 +92,6 @@ export default function Shop() {
type: 'success', type: 'success',
}); });
const ITEM_WIDTH = 110; // ширина "карточки" предмета в рулетке
const ITEM_GAP = 8;
const VISIBLE_ITEMS = 21; // сколько элементов в линии const VISIBLE_ITEMS = 21; // сколько элементов в линии
const CENTER_INDEX = Math.floor(VISIBLE_ITEMS / 2); const CENTER_INDEX = Math.floor(VISIBLE_ITEMS / 2);
@ -327,11 +326,7 @@ export default function Shop() {
</Typography> </Typography>
{!isOnline && ( {!isOnline && (
<Typography <Typography variant="body1" color="error" sx={{ mb: 2 }}>
variant="body1"
color="error"
sx={{ mb: 2, maxWidth: '600px' }}
>
Для открытия кейсов вам необходимо находиться на одном из серверов Для открытия кейсов вам необходимо находиться на одном из серверов
игры. Зайдите в игру и обновите страницу. игры. Зайдите в игру и обновите страницу.
</Typography> </Typography>
@ -475,20 +470,12 @@ export default function Shop() {
> >
Цена Цена
</Typography> </Typography>
<Box <CoinsDisplay
sx={{ value={c.price}
px: 1.6, size="small"
py: 0.4, autoUpdate={false}
borderRadius: '999px', showTooltip={true}
fontSize: '0.8rem', />
bgcolor: 'rgba(255, 77, 77, 0.16)',
border: '1px solid rgba(255, 77, 77, 0.85)',
color: 'white',
fontFamily: 'Benzin-Bold',
}}
>
{c.price} поп
</Box>
</Box> </Box>
{typeof c.items_count === 'number' && ( {typeof c.items_count === 'number' && (