274 lines
7.6 KiB
TypeScript
274 lines
7.6 KiB
TypeScript
// CoinsDisplay.tsx
|
||
import { Box, Typography } from '@mui/material';
|
||
import CustomTooltip from './Notifications/CustomTooltip';
|
||
import { useEffect, useMemo, useState } from 'react';
|
||
import { fetchCoins } from '../api';
|
||
import type { SxProps, Theme } from '@mui/material/styles';
|
||
|
||
interface CoinsDisplayProps {
|
||
value?: number;
|
||
username?: string;
|
||
|
||
size?: 'small' | 'medium' | 'large';
|
||
showTooltip?: boolean;
|
||
tooltipText?: string;
|
||
showIcon?: boolean;
|
||
iconColor?: string;
|
||
|
||
autoUpdate?: boolean;
|
||
updateInterval?: number;
|
||
|
||
backgroundColor?: string;
|
||
textColor?: string;
|
||
|
||
onClick?: () => void;
|
||
disableRefreshOnClick?: boolean;
|
||
|
||
sx?: SxProps<Theme>;
|
||
}
|
||
|
||
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',
|
||
|
||
onClick,
|
||
disableRefreshOnClick = false,
|
||
|
||
sx,
|
||
}: CoinsDisplayProps) {
|
||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||
const [settingsVersion, setSettingsVersion] = useState(0);
|
||
const storageKey = useMemo(() => {
|
||
// ключ под конкретного пользователя
|
||
return username ? `coins:${username}` : 'coins:anonymous';
|
||
}, [username]);
|
||
|
||
const readCachedCoins = (): number | null => {
|
||
if (typeof window === 'undefined') return null;
|
||
try {
|
||
const raw = localStorage.getItem(storageKey);
|
||
if (!raw) return null;
|
||
const parsed = Number(raw);
|
||
return Number.isFinite(parsed) ? parsed : null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
const handleClick = () => {
|
||
// 1) если передали внешний обработчик — выполняем его
|
||
if (onClick) onClick();
|
||
|
||
// 2) опционально оставляем обновление баланса по клику
|
||
if (!disableRefreshOnClick && username) fetchCoinsData();
|
||
};
|
||
|
||
const [coins, setCoins] = useState<number>(() => {
|
||
// 1) если пришло значение извне — оно приоритетнее
|
||
if (externalValue !== undefined) return externalValue;
|
||
|
||
// 2) иначе пробуем localStorage
|
||
const cached = readCachedCoins();
|
||
if (cached !== null) return cached;
|
||
|
||
// 3) иначе 0
|
||
return 0;
|
||
});
|
||
|
||
useEffect(() => {
|
||
const handler = () => setSettingsVersion((v) => v + 1);
|
||
window.addEventListener('settings-updated', handler as EventListener);
|
||
return () =>
|
||
window.removeEventListener('settings-updated', handler as EventListener);
|
||
}, []);
|
||
|
||
const isTooltipDisabledBySettings = useMemo(() => {
|
||
try {
|
||
const raw = localStorage.getItem('launcher_settings');
|
||
if (!raw) return false;
|
||
const s = JSON.parse(raw);
|
||
return Boolean(s?.disableToolTip);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}, [settingsVersion]);
|
||
|
||
const tooltipEnabled = showTooltip && !isTooltipDisabledBySettings;
|
||
|
||
const getSizes = () => {
|
||
switch (size) {
|
||
case 'small':
|
||
return {
|
||
containerPadding: '0.4vw 0.8vw',
|
||
iconSize: '1.4vw',
|
||
fontSize: '1vw',
|
||
borderRadius: '2vw',
|
||
gap: '0.6vw',
|
||
};
|
||
case 'large':
|
||
return {
|
||
containerPadding: '0.4vw 1.2vw',
|
||
iconSize: '2.2vw',
|
||
fontSize: '1.6vw',
|
||
borderRadius: '1.8vw',
|
||
gap: '0.8vw',
|
||
};
|
||
case 'medium':
|
||
default:
|
||
return {
|
||
containerPadding: '0.4vw 1vw',
|
||
iconSize: '2vw',
|
||
fontSize: '1.4vw',
|
||
borderRadius: '1.6vw',
|
||
gap: '0.6vw',
|
||
};
|
||
}
|
||
};
|
||
|
||
const sizes = getSizes();
|
||
|
||
const formatNumber = (num: number): string => {
|
||
return num.toLocaleString('ru-RU');
|
||
};
|
||
|
||
// Сохраняем актуальный баланс в localStorage при любом изменении coins
|
||
useEffect(() => {
|
||
if (typeof window === 'undefined') return;
|
||
try {
|
||
localStorage.setItem(storageKey, String(coins));
|
||
} catch {
|
||
// игнорируем (private mode, quota и т.п.)
|
||
}
|
||
}, [coins, storageKey]);
|
||
|
||
// Если пришло внешнее значение — обновляем и оно же попадёт в localStorage через эффект выше
|
||
useEffect(() => {
|
||
if (externalValue !== undefined) {
|
||
setCoins(externalValue);
|
||
}
|
||
}, [externalValue]);
|
||
|
||
// При смене username можно сразу подхватить кэш, чтобы не мигало при первом fetch
|
||
useEffect(() => {
|
||
if (externalValue !== undefined) return; // внешнее значение важнее
|
||
const cached = readCachedCoins();
|
||
if (cached !== null) setCoins(cached);
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [storageKey]);
|
||
|
||
const fetchCoinsData = async () => {
|
||
if (!username) return;
|
||
|
||
setIsLoading(true);
|
||
try {
|
||
const coinsData = await fetchCoins(username);
|
||
// ВАЖНО: не показываем "..." — просто меняем число, когда пришёл ответ
|
||
setCoins(coinsData.coins);
|
||
} catch (error) {
|
||
console.error('Ошибка при получении количества монет:', error);
|
||
// оставляем старое значение (из state/localStorage)
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (username && autoUpdate) {
|
||
fetchCoinsData();
|
||
const coinsInterval = setInterval(fetchCoinsData, updateInterval);
|
||
return () => clearInterval(coinsInterval);
|
||
}
|
||
}, [username, autoUpdate, updateInterval]);
|
||
|
||
const handleRefresh = () => {
|
||
if (username) fetchCoinsData();
|
||
};
|
||
|
||
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: onClick ? 'pointer' : tooltipEnabled ? 'help' : 'default',
|
||
|
||
// можно оставить лёгкий намёк на загрузку, но без "пульса" текста
|
||
opacity: isLoading ? 0.85 : 1,
|
||
transition: 'opacity 0.2s ease',
|
||
|
||
...sx,
|
||
}}
|
||
onClick={handleClick}
|
||
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',
|
||
}}
|
||
>
|
||
{formatNumber(coins)}
|
||
</Typography>
|
||
</Box>
|
||
);
|
||
|
||
if (tooltipEnabled) {
|
||
return (
|
||
<CustomTooltip
|
||
title={tooltipText}
|
||
arrow
|
||
placement="bottom"
|
||
TransitionProps={{ timeout: 300 }}
|
||
>
|
||
{coinsDisplay}
|
||
</CustomTooltip>
|
||
);
|
||
}
|
||
|
||
return coinsDisplay;
|
||
}
|