ne minor, a ebat fix
This commit is contained in:
@ -7,6 +7,7 @@ import {
|
||||
Typography,
|
||||
Button,
|
||||
CardMedia,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import CoinsDisplay from './CoinsDisplay';
|
||||
|
||||
@ -249,28 +250,30 @@ export const BonusShopItem: React.FC<BonusShopItemProps> = ({
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider sx={{background: 'rgba(160, 160, 160, 0.3)', borderRadius: '2vw'}}/>
|
||||
|
||||
{!isBuyMode && onToggleActive && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={onToggleActive}
|
||||
disabled={disabled}
|
||||
sx={{
|
||||
mt: 0.5,
|
||||
borderRadius: '2.5vw',
|
||||
textTransform: 'none',
|
||||
fontSize: '0.75rem',
|
||||
px: 2,
|
||||
borderColor: 'rgba(255,255,255,0.4)',
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
'&:hover': {
|
||||
borderColor: 'rgba(255,255,255,0.8)',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
onClick={onToggleActive}
|
||||
sx={{
|
||||
mt: '1vw',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '1vw',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
cursor: 'pointer',
|
||||
backgroundImage:
|
||||
'linear-gradient(71deg, #F27121 0%, #E940CD 60%, #8A2387 100%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
textShadow: '0 0 15px rgba(0,0,0,0.9)',
|
||||
'&:hover': {
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isActive ? 'Выключить' : 'Включить'}
|
||||
</Button>
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
|
||||
@ -1,18 +1,7 @@
|
||||
// src/renderer/components/CapeCard.tsx
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardMedia,
|
||||
CardContent,
|
||||
Typography,
|
||||
CardActions,
|
||||
Button,
|
||||
Tooltip,
|
||||
Box,
|
||||
Chip,
|
||||
} from '@mui/material';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Typography, Paper, Chip, Button } from '@mui/material';
|
||||
import CustomTooltip from './Notifications/CustomTooltip';
|
||||
// Тип для плаща с необязательными полями для обоих вариантов использования
|
||||
|
||||
export interface CapeCardProps {
|
||||
cape: {
|
||||
cape_id?: string;
|
||||
@ -31,104 +20,189 @@ export interface CapeCardProps {
|
||||
actionDisabled?: boolean;
|
||||
}
|
||||
|
||||
const GRADIENT =
|
||||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)';
|
||||
|
||||
export default function CapeCard({
|
||||
cape,
|
||||
mode,
|
||||
onAction,
|
||||
actionDisabled = false,
|
||||
}: CapeCardProps) {
|
||||
// Определяем текст и цвет кнопки в зависимости от режима
|
||||
const getActionButton = () => {
|
||||
if (mode === 'shop') {
|
||||
return {
|
||||
text: 'Купить',
|
||||
color: 'primary',
|
||||
};
|
||||
} else {
|
||||
// Профиль
|
||||
return cape.is_active
|
||||
? { text: 'Снять', color: 'error' }
|
||||
: { text: 'Надеть', color: 'success' };
|
||||
}
|
||||
};
|
||||
|
||||
const actionButton = getActionButton();
|
||||
|
||||
// В функции компонента добавьте нормализацию данных
|
||||
const capeId = cape.cape_id || cape.id || '';
|
||||
const capeName = cape.cape_name || cape.name || '';
|
||||
const capeName = cape.cape_name || cape.name || 'Без названия';
|
||||
const capeDescription = cape.cape_description || cape.description || '';
|
||||
|
||||
const action = useMemo(() => {
|
||||
if (mode === 'shop') {
|
||||
return { text: 'Купить', variant: 'gradient' as const };
|
||||
}
|
||||
return cape.is_active
|
||||
? { text: 'Снять', variant: 'danger' as const }
|
||||
: { text: 'Надеть', variant: 'success' as const };
|
||||
}, [mode, cape.is_active]);
|
||||
|
||||
const topRightChip =
|
||||
mode === 'shop' && cape.price !== undefined
|
||||
? `${cape.price} коинов`
|
||||
: cape.is_active
|
||||
? 'Активен'
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<CustomTooltip arrow title={capeDescription}>
|
||||
<Card
|
||||
<CustomTooltip arrow title={capeDescription} placement="bottom">
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
bgcolor: 'rgba(255, 255, 255, 0.05)',
|
||||
width: 200,
|
||||
width: '16.5vw',
|
||||
borderRadius: '1.2vw',
|
||||
overflow: 'hidden',
|
||||
position: 'relative', // для позиционирования ценника
|
||||
borderRadius: '1vw',
|
||||
position: 'relative',
|
||||
color: 'white',
|
||||
background:
|
||||
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.14), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.12), transparent 55%), rgba(10,10,20,0.86)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
boxShadow: '0 1.2vw 3.2vw rgba(0,0,0,0.55)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
transition:
|
||||
'transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.01)',
|
||||
borderColor: 'rgba(242,113,33,0.35)',
|
||||
boxShadow: '0 1.4vw 3.8vw rgba(0,0,0,0.60)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Ценник для магазина */}
|
||||
{mode === 'shop' && cape.price !== undefined && (
|
||||
{/* градиентная полоска-акцент (как у твоих блоков) */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '0.35vw',
|
||||
background: GRADIENT,
|
||||
opacity: 0.9,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 2,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* chip справа сверху */}
|
||||
{topRightChip && (
|
||||
<Chip
|
||||
label={`${cape.price} коинов`}
|
||||
label={topRightChip}
|
||||
size="small"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 2,
|
||||
bgcolor: 'rgba(0, 0, 0, 0.7)',
|
||||
top: '0.8vw',
|
||||
right: '0.8vw',
|
||||
zIndex: 3,
|
||||
height: '1.55rem',
|
||||
fontSize: '0.72rem',
|
||||
fontWeight: 900,
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '999px',
|
||||
background:
|
||||
mode === 'shop'
|
||||
? 'rgba(0,0,0,0.65)'
|
||||
: 'linear-gradient(120deg, rgba(242,113,33,0.22), rgba(233,64,205,0.14), rgba(138,35,135,0.18))',
|
||||
border: '1px solid rgba(255,255,255,0.10)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={cape.image_url}
|
||||
alt={capeName}
|
||||
{/* preview */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
transform: 'scale(2.9) translateX(66px) translateY(32px)',
|
||||
imageRendering: 'pixelated',
|
||||
}}
|
||||
/>
|
||||
|
||||
<CardContent
|
||||
sx={{
|
||||
bgcolor: 'rgba(255, 255, 255, 0.05)',
|
||||
pt: '6vw',
|
||||
minHeight: '5vw',
|
||||
px: '1.1vw',
|
||||
pt: '1.0vw',
|
||||
pb: '0.7vw',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: 'white' }}>{capeName}</Typography>
|
||||
</CardContent>
|
||||
|
||||
<CardActions sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color={actionButton.color as 'primary' | 'success' | 'error'}
|
||||
onClick={() => onAction(capeId)}
|
||||
disabled={actionDisabled}
|
||||
<Box
|
||||
sx={{
|
||||
borderRadius: '2vw',
|
||||
p: '0.5vw 2.5vw',
|
||||
color: 'white',
|
||||
backgroundColor: 'rgb(0, 134, 0)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(0, 134, 0, 0.5)',
|
||||
},
|
||||
fontFamily: 'Benzin-Bold',
|
||||
borderRadius: '1.0vw',
|
||||
overflow: 'hidden',
|
||||
border: '1px solid rgba(255,255,255,0.10)',
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
maxHeight: '21vw',
|
||||
}}
|
||||
>
|
||||
{actionButton.text}
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
{/* Здесь показываем ЛЕВУЮ половину текстуры (лицевую часть) */}
|
||||
<Box
|
||||
sx={{
|
||||
width: '44vw',
|
||||
height: '36vw',
|
||||
minWidth: '462px',
|
||||
minHeight: '380px',
|
||||
imageRendering: 'pixelated',
|
||||
backgroundImage: `url(${cape.image_url})`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '200% 100%', // важно: режем пополам “кадром”
|
||||
backgroundPosition: 'left center',
|
||||
ml: '-2vw',
|
||||
// если нужно чуть увеличить/сдвинуть — делай через backgroundPosition
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* content */}
|
||||
<Box sx={{ px: '1.1vw', pb: '1.1vw' }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '0.95vw',
|
||||
minFontSize: 14,
|
||||
color: 'rgba(255,255,255,0.92)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{capeName}
|
||||
</Typography>
|
||||
|
||||
{/* действия */}
|
||||
<Box sx={{ mt: '0.9vw', display: 'flex', justifyContent: 'center' }}>
|
||||
<Button
|
||||
fullWidth
|
||||
disableRipple
|
||||
onClick={() => onAction(capeId)}
|
||||
disabled={actionDisabled || !capeId}
|
||||
sx={{
|
||||
borderRadius: '999px',
|
||||
py: '0.75vw',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontWeight: 900,
|
||||
color: '#fff',
|
||||
background:
|
||||
action.variant === 'gradient'
|
||||
? GRADIENT
|
||||
: action.variant === 'success'
|
||||
? 'rgba(0, 134, 0, 0.95)'
|
||||
: 'rgba(190, 35, 35, 0.95)',
|
||||
boxShadow: '0 1.0vw 2.8vw rgba(0,0,0,0.40)',
|
||||
transition: 'transform 0.18s ease, filter 0.18s ease, opacity 0.18s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.01)',
|
||||
filter: 'brightness(1.05)',
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
background: 'rgba(255,255,255,0.10)',
|
||||
color: 'rgba(255,255,255,0.55)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{action.text}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</CustomTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,27 +1,23 @@
|
||||
// CoinsDisplay.tsx
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import CustomTooltip from './Notifications/CustomTooltip';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { fetchCoins } from '../api';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
|
||||
interface CoinsDisplayProps {
|
||||
// Основные пропсы
|
||||
value?: number; // Передаем значение напрямую
|
||||
username?: string; // Или получаем по username из API
|
||||
value?: number;
|
||||
username?: string;
|
||||
|
||||
// Опции отображения
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
showTooltip?: boolean;
|
||||
tooltipText?: string;
|
||||
showIcon?: boolean;
|
||||
iconColor?: string;
|
||||
|
||||
// Опции обновления
|
||||
autoUpdate?: boolean; // Автоматическое обновление из API
|
||||
updateInterval?: number; // Интервал обновления в миллисекундах
|
||||
autoUpdate?: boolean;
|
||||
updateInterval?: number;
|
||||
|
||||
// Стилизация
|
||||
backgroundColor?: string;
|
||||
textColor?: string;
|
||||
|
||||
@ -29,31 +25,54 @@ interface CoinsDisplayProps {
|
||||
}
|
||||
|
||||
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',
|
||||
|
||||
sx,
|
||||
}: CoinsDisplayProps) {
|
||||
const [coins, setCoins] = useState<number>(externalValue || 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 [coins, setCoins] = useState<number>(() => {
|
||||
// 1) если пришло значение извне — оно приоритетнее
|
||||
if (externalValue !== undefined) return externalValue;
|
||||
|
||||
// 2) иначе пробуем localStorage
|
||||
const cached = readCachedCoins();
|
||||
if (cached !== null) return cached;
|
||||
|
||||
// 3) иначе 0
|
||||
return 0;
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
// Определяем размеры в зависимости от параметра size
|
||||
const getSizes = () => {
|
||||
switch (size) {
|
||||
case 'small':
|
||||
@ -86,52 +105,61 @@ export default function CoinsDisplay({
|
||||
|
||||
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);
|
||||
}
|
||||
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]);
|
||||
|
||||
// Эффект для API обновлений
|
||||
// При смене username можно сразу подхватить кэш, чтобы не мигало при первом fetch
|
||||
useEffect(() => {
|
||||
if (username && autoUpdate) {
|
||||
fetchCoinsData();
|
||||
if (externalValue !== undefined) return; // внешнее значение важнее
|
||||
const cached = readCachedCoins();
|
||||
if (cached !== null) setCoins(cached);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [storageKey]);
|
||||
|
||||
// Создаем интервалы для периодического обновления данных
|
||||
const coinsInterval = setInterval(fetchCoinsData, updateInterval);
|
||||
const fetchCoinsData = async () => {
|
||||
if (!username) return;
|
||||
|
||||
return () => {
|
||||
clearInterval(coinsInterval);
|
||||
};
|
||||
}
|
||||
}, [username, autoUpdate, updateInterval]);
|
||||
|
||||
// Ручное обновление данных
|
||||
const handleRefresh = () => {
|
||||
if (username) {
|
||||
fetchCoinsData();
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const coinsData = await fetchCoins(username);
|
||||
// ВАЖНО: не показываем "..." — просто меняем число, когда пришёл ответ
|
||||
setCoins(coinsData.coins);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении количества монет:', error);
|
||||
// оставляем старое значение (из state/localStorage)
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Форматирование числа с разделителями тысяч
|
||||
const formatNumber = (num: number): string => {
|
||||
return num.toLocaleString('ru-RU');
|
||||
useEffect(() => {
|
||||
if (username && autoUpdate) {
|
||||
fetchCoinsData();
|
||||
const coinsInterval = setInterval(fetchCoinsData, updateInterval);
|
||||
return () => clearInterval(coinsInterval);
|
||||
}
|
||||
}, [username, autoUpdate, updateInterval]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
if (username) fetchCoinsData();
|
||||
};
|
||||
|
||||
const coinsDisplay = (
|
||||
@ -145,7 +173,9 @@ export default function CoinsDisplay({
|
||||
padding: sizes.containerPadding,
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
cursor: showTooltip ? 'help' : 'default',
|
||||
opacity: isLoading ? 0.7 : 1,
|
||||
|
||||
// можно оставить лёгкий намёк на загрузку, но без "пульса" текста
|
||||
opacity: isLoading ? 0.85 : 1,
|
||||
transition: 'opacity 0.2s ease',
|
||||
|
||||
...sx,
|
||||
@ -187,7 +217,7 @@ export default function CoinsDisplay({
|
||||
fontFamily: 'Benzin-Bold, sans-serif',
|
||||
}}
|
||||
>
|
||||
{isLoading ? '...' : formatNumber(coins)}
|
||||
{formatNumber(coins)}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
@ -207,34 +237,3 @@ export default function CoinsDisplay({
|
||||
|
||||
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"
|
||||
/>
|
||||
*/
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Fade from '@mui/material/Fade';
|
||||
|
||||
interface FullScreenLoaderProps {
|
||||
message?: string;
|
||||
fullScreen?: boolean; // <-- новый проп
|
||||
fullScreen?: boolean;
|
||||
}
|
||||
|
||||
export const FullScreenLoader = ({
|
||||
@ -34,39 +35,76 @@ export const FullScreenLoader = ({
|
||||
|
||||
return (
|
||||
<Box sx={containerSx}>
|
||||
{/* Градиентное вращающееся кольцо */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '50%',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: 'conic-gradient(#F27121, #E940CD, #8A2387, #F27121)',
|
||||
animation: 'spin 1s linear infinite',
|
||||
WebkitMask: 'radial-gradient(circle, transparent 55%, black 56%)',
|
||||
mask: 'radial-gradient(circle, transparent 55%, black 56%)',
|
||||
'@keyframes spin': {
|
||||
'0%': { transform: 'rotate(0deg)' },
|
||||
'100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{/* Плавное появление фона */}
|
||||
{fullScreen && (
|
||||
<Fade in timeout={220} appear>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background:
|
||||
'radial-gradient(circle at 15% 20%, rgba(242,113,33,0.15), transparent 60%), radial-gradient(circle at 85% 10%, rgba(233,64,205,0.12), transparent 55%), rgba(5,5,10,0.75)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
WebkitBackdropFilter: 'blur(14px)',
|
||||
}}
|
||||
/>
|
||||
</Fade>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<Typography
|
||||
variant="h6"
|
||||
{/* Плавное появление контента */}
|
||||
<Fade in timeout={260} appear>
|
||||
<Box
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
background:
|
||||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 3,
|
||||
// небольшой "подъём" при появлении
|
||||
animation: 'popIn 260ms ease-out both',
|
||||
'@keyframes popIn': {
|
||||
from: { opacity: 0, transform: 'translateY(8px) scale(0.98)' },
|
||||
to: { opacity: 1, transform: 'translateY(0) scale(1)' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</Typography>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '50%',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background: 'conic-gradient(#F27121, #E940CD, #8A2387, #F27121)',
|
||||
animation: 'spin 1s linear infinite',
|
||||
WebkitMask: 'radial-gradient(circle, transparent 55%, black 56%)',
|
||||
mask: 'radial-gradient(circle, transparent 55%, black 56%)',
|
||||
'@keyframes spin': {
|
||||
'0%': { transform: 'rotate(0deg)' },
|
||||
'100%': { transform: 'rotate(360deg)' },
|
||||
},
|
||||
boxShadow: '0 0 2.5vw rgba(233,64,205,0.45)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{message && (
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
background:
|
||||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
textAlign: 'center',
|
||||
textShadow: '0 0 1.2vw rgba(0,0,0,0.45)',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Fade>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
// src/renderer/components/HeadAvatar.tsx
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
interface HeadAvatarProps {
|
||||
skinUrl?: string;
|
||||
size?: number;
|
||||
style?: React.CSSProperties;
|
||||
version?: number; // ✅ добавили
|
||||
}
|
||||
|
||||
const DEFAULT_SKIN =
|
||||
@ -14,12 +14,17 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
|
||||
skinUrl,
|
||||
size = 24,
|
||||
style,
|
||||
version = 0, // ✅ дефолт
|
||||
...canvasProps
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const finalSkinUrl = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
|
||||
const baseUrl = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
|
||||
|
||||
// ✅ cache-bust: чтобы браузер НЕ отдавал старую картинку
|
||||
const finalSkinUrl = `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}v=${version}`;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
@ -37,17 +42,14 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
|
||||
// База головы: (8, 8, 8, 8)
|
||||
ctx.drawImage(img, 8, 8, 8, 8, 0, 0, size, size);
|
||||
|
||||
// Слой шляпы: (40, 8, 8, 8)
|
||||
ctx.drawImage(img, 40, 8, 8, 8, 0, 0, size, size);
|
||||
};
|
||||
|
||||
img.onerror = (e) => {
|
||||
console.error('Не удалось загрузить скин для HeadAvatar:', e);
|
||||
};
|
||||
}, [skinUrl, size]);
|
||||
}, [skinUrl, size, version]); // ✅ version добавили
|
||||
|
||||
return (
|
||||
<canvas
|
||||
@ -58,7 +60,7 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
|
||||
height: size,
|
||||
borderRadius: 4,
|
||||
imageRendering: 'pixelated',
|
||||
...style, // 👈 даём переопределять снаружи
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
Select,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
fetchActiveServers,
|
||||
@ -20,6 +21,7 @@ import { FullScreenLoader } from './FullScreenLoader';
|
||||
import { HeadAvatar } from './HeadAvatar';
|
||||
import { translateServer } from '../utils/serverTranslator';
|
||||
import GradientTextField from './GradientTextField'; // <-- используем ваш градиентный инпут
|
||||
import { NONAME } from 'dns';
|
||||
|
||||
type OnlinePlayerFlat = {
|
||||
username: string;
|
||||
@ -142,6 +144,34 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
||||
|
||||
const totalOnline = onlinePlayers.length;
|
||||
|
||||
const controlSx = {
|
||||
minWidth: '16vw',
|
||||
'& .MuiInputLabel-root': {
|
||||
color: 'rgba(255,255,255,0.75)',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
},
|
||||
'& .MuiInputLabel-root.Mui-focused': {
|
||||
color: 'rgba(242,113,33,0.95)',
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: '3.2vw', // <-- ЕДИНАЯ высота
|
||||
borderRadius: '999px',
|
||||
backgroundColor: 'rgba(255,255,255,0.04)',
|
||||
color: 'white',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'rgba(255,255,255,0.14)',
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'rgba(242,113,33,0.55)',
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'rgba(233,64,205,0.65)',
|
||||
borderWidth: '2px',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
@ -203,16 +233,7 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
||||
{/* Select в “нашем” стиле */}
|
||||
<FormControl
|
||||
size="small"
|
||||
sx={{
|
||||
minWidth: '12vw',
|
||||
'& .MuiInputLabel-root': {
|
||||
color: 'rgba(255,255,255,0.75)',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
},
|
||||
'& .MuiInputLabel-root.Mui-focused': {
|
||||
color: 'rgba(242,113,33,0.95)',
|
||||
},
|
||||
}}
|
||||
sx={controlSx}
|
||||
>
|
||||
<InputLabel>Сервер</InputLabel>
|
||||
<Select
|
||||
@ -266,7 +287,7 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
||||
<MenuItem value="all">Все сервера</MenuItem>
|
||||
{servers.map((s) => (
|
||||
<MenuItem key={s.id} value={s.id}>
|
||||
{s.name}
|
||||
{translateServer(s.name)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
@ -274,25 +295,55 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
||||
|
||||
{/* Поиск через ваш GradientTextField */}
|
||||
<Box sx={{ minWidth: '16vw' }}>
|
||||
<GradientTextField
|
||||
size="small"
|
||||
<TextField
|
||||
size="small"
|
||||
label="Поиск по нику"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
sx={{
|
||||
...controlSx,
|
||||
'& .MuiOutlinedInput-input': {
|
||||
height: '100%',
|
||||
padding: '0 1.2vw', // <-- ТОЧНО ТАКОЙ ЖЕ padding
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: '0.9vw',
|
||||
color: 'rgba(255,255,255,0.92)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <GradientTextField
|
||||
label="Поиск по нику"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
sx={{
|
||||
mt: 0,
|
||||
mb: 0,
|
||||
'& .MuiOutlinedInput-root': { borderRadius: '999px' },
|
||||
'& .MuiOutlinedInput-root::before': { borderRadius: '999px' },
|
||||
'& .MuiInputBase-input': {
|
||||
padding: '0.85vw 1.2vw',
|
||||
fontSize: '0.9vw',
|
||||
padding: 'none',
|
||||
fontFamily: 'none',
|
||||
},
|
||||
'& .MuiInputLabel-root': {
|
||||
// background: 'rgba(10,10,20,0.92)',
|
||||
'& .css-16wblaj-MuiInputBase-input-MuiOutlinedInput-input': {
|
||||
padding: '4px 0 5px',
|
||||
},
|
||||
'& .css-19qnlrw-MuiFormLabel-root-MuiInputLabel-root': {
|
||||
top: '-15px',
|
||||
},
|
||||
|
||||
'& .MuiOutlinedInput-root::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
padding: '0.2vw', // толщина рамки
|
||||
borderRadius: '3.5vw',
|
||||
background: GRADIENT,
|
||||
WebkitMask:
|
||||
'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
|
||||
WebkitMaskComposite: 'xor',
|
||||
maskComposite: 'exclude',
|
||||
zIndex: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
/> */}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
@ -378,7 +429,7 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '0.6vw', flexShrink: 0 }}>
|
||||
<Chip
|
||||
label={translateServer({ name: p.serverName })}
|
||||
label={translateServer(p.serverName)}
|
||||
size="small"
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
|
||||
@ -38,6 +38,13 @@ export default function PageHeader() {
|
||||
return { title: '', subtitle: '', hidden: true };
|
||||
}
|
||||
|
||||
if (path === '/settings') {
|
||||
return {
|
||||
title: 'Настройки',
|
||||
subtitle: 'Персонализация интерфейса и поведения лаунчера',
|
||||
}
|
||||
}
|
||||
|
||||
if (path === '/news') {
|
||||
return {
|
||||
title: 'Новости',
|
||||
|
||||
@ -9,6 +9,9 @@ interface SkinViewerProps {
|
||||
autoRotate?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SKIN =
|
||||
'https://static.planetminecraft.com/files/resource_media/skin/original-steve-15053860.png';
|
||||
|
||||
export default function SkinViewer({
|
||||
width = 300,
|
||||
height = 400,
|
||||
@ -19,57 +22,111 @@ export default function SkinViewer({
|
||||
}: SkinViewerProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const viewerRef = useRef<any>(null);
|
||||
const animRef = useRef<any>(null);
|
||||
|
||||
// 1) Инициализируем viewer ОДИН РАЗ
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return;
|
||||
let disposed = false;
|
||||
|
||||
const init = async () => {
|
||||
if (!canvasRef.current || viewerRef.current) return;
|
||||
|
||||
// Используем динамический импорт для обхода проблемы ESM/CommonJS
|
||||
const initSkinViewer = async () => {
|
||||
try {
|
||||
const skinview3d = await import('skinview3d');
|
||||
if (disposed) return;
|
||||
|
||||
// Создаем просмотрщик скина по документации
|
||||
const viewer = new skinview3d.SkinViewer({
|
||||
canvas: canvasRef.current,
|
||||
width,
|
||||
height,
|
||||
skin:
|
||||
skinUrl ||
|
||||
'https://static.planetminecraft.com/files/resource_media/skin/original-steve-15053860.png',
|
||||
model: 'auto-detect',
|
||||
cape: capeUrl || undefined,
|
||||
});
|
||||
|
||||
// Настраиваем вращение
|
||||
// базовая настройка
|
||||
viewer.autoRotate = autoRotate;
|
||||
|
||||
// Настраиваем анимацию ходьбы
|
||||
viewer.animation = new skinview3d.WalkingAnimation();
|
||||
viewer.animation.speed = walkingSpeed;
|
||||
// анимация ходьбы
|
||||
const walking = new skinview3d.WalkingAnimation();
|
||||
walking.speed = walkingSpeed;
|
||||
viewer.animation = walking;
|
||||
|
||||
// Сохраняем экземпляр для очистки
|
||||
viewerRef.current = viewer;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при инициализации skinview3d:', error);
|
||||
animRef.current = walking;
|
||||
|
||||
// выставляем ресурсы сразу
|
||||
const finalSkin = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
|
||||
await viewer.loadSkin(finalSkin);
|
||||
|
||||
if (capeUrl?.trim()) {
|
||||
await viewer.loadCape(capeUrl);
|
||||
} else {
|
||||
viewer.cape = null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Ошибка при инициализации skinview3d:', e);
|
||||
}
|
||||
};
|
||||
|
||||
initSkinViewer();
|
||||
init();
|
||||
|
||||
// Очистка при размонтировании
|
||||
return () => {
|
||||
disposed = true;
|
||||
if (viewerRef.current) {
|
||||
viewerRef.current.dispose();
|
||||
viewerRef.current = null;
|
||||
animRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [width, height, skinUrl, capeUrl, walkingSpeed, autoRotate]);
|
||||
// ⚠️ пустой deps — создаём один раз
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
);
|
||||
// 2) Обновляем размеры (не пересоздаём viewer)
|
||||
useEffect(() => {
|
||||
const viewer = viewerRef.current;
|
||||
if (!viewer) return;
|
||||
viewer.width = width;
|
||||
viewer.height = height;
|
||||
}, [width, height]);
|
||||
|
||||
// 3) Обновляем автоповорот
|
||||
useEffect(() => {
|
||||
const viewer = viewerRef.current;
|
||||
if (!viewer) return;
|
||||
viewer.autoRotate = autoRotate;
|
||||
}, [autoRotate]);
|
||||
|
||||
// 4) Обновляем скорость анимации
|
||||
useEffect(() => {
|
||||
const walking = animRef.current;
|
||||
if (!walking) return;
|
||||
walking.speed = walkingSpeed;
|
||||
}, [walkingSpeed]);
|
||||
|
||||
// 5) Обновляем скин
|
||||
useEffect(() => {
|
||||
const viewer = viewerRef.current;
|
||||
if (!viewer) return;
|
||||
|
||||
const finalSkin = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
|
||||
|
||||
// защита от кеша: добавим “bust” только если URL уже имеет query — не обязательно, но помогает
|
||||
const url = finalSkin.includes('?') ? `${finalSkin}&t=${Date.now()}` : `${finalSkin}?t=${Date.now()}`;
|
||||
|
||||
viewer.loadSkin(url).catch((e: any) => console.error('loadSkin error:', e));
|
||||
}, [skinUrl]);
|
||||
|
||||
// 6) Обновляем плащ
|
||||
useEffect(() => {
|
||||
const viewer = viewerRef.current;
|
||||
if (!viewer) return;
|
||||
|
||||
if (capeUrl?.trim()) {
|
||||
const url = capeUrl.includes('?') ? `${capeUrl}&t=${Date.now()}` : `${capeUrl}?t=${Date.now()}`;
|
||||
viewer.loadCape(url).catch((e: any) => console.error('loadCape error:', e));
|
||||
} else {
|
||||
viewer.cape = null;
|
||||
}
|
||||
}, [capeUrl]);
|
||||
|
||||
return <canvas ref={canvasRef} width={width} height={height} style={{ display: 'block' }} />;
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import { fetchPlayer } from './../api';
|
||||
import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
|
||||
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
|
||||
import PersonIcon from '@mui/icons-material/Person';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: {
|
||||
@ -64,10 +65,14 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
||||
}, []);
|
||||
|
||||
const [skinUrl, setSkinUrl] = useState<string>('');
|
||||
const [skinVersion, setSkinVersion] = useState(0);
|
||||
const [avatarAnchorEl, setAvatarAnchorEl] = useState<null | HTMLElement>(
|
||||
null,
|
||||
);
|
||||
|
||||
const path = location.pathname || '';
|
||||
const isAuthPage = path.startsWith('/login') || path.startsWith('/registration');
|
||||
|
||||
const TAB_ROUTES: Array<{
|
||||
value: number;
|
||||
match: (p: string) => boolean;
|
||||
@ -219,18 +224,47 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const savedConfig = localStorage.getItem('launcher_config');
|
||||
const loadSkin = useCallback(async () => {
|
||||
if (!isAuthed) {
|
||||
setSkinUrl('');
|
||||
return;
|
||||
}
|
||||
|
||||
const savedConfig = localStorage.getItem('launcher_config');
|
||||
if (!savedConfig) return;
|
||||
|
||||
const config = JSON.parse(savedConfig);
|
||||
const uuid = config.uuid;
|
||||
let cfg: any = null;
|
||||
try {
|
||||
cfg = JSON.parse(savedConfig);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const uuid = cfg?.uuid;
|
||||
if (!uuid) return;
|
||||
|
||||
fetchPlayer(uuid)
|
||||
.then((player) => setSkinUrl(player.skin_url))
|
||||
.catch((e) => console.error('Не удалось получить скин:', e));
|
||||
}, []);
|
||||
try {
|
||||
const player = await fetchPlayer(uuid);
|
||||
setSkinUrl(player.skin_url || '');
|
||||
} catch (e) {
|
||||
console.error('Не удалось получить скин:', e);
|
||||
setSkinUrl('');
|
||||
}
|
||||
}, [isAuthed]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSkin();
|
||||
}, [loadSkin, location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
setSkinVersion((v) => v + 1);
|
||||
loadSkin();
|
||||
};
|
||||
|
||||
window.addEventListener('skin-updated', handler as EventListener);
|
||||
return () => window.removeEventListener('skin-updated', handler as EventListener);
|
||||
}, [loadSkin]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -251,10 +285,10 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
||||
// убрать если не оч
|
||||
|
||||
// стиль как в Registration
|
||||
background:
|
||||
'linear-gradient(71deg, rgba(242,113,33,0.18) 0%, rgba(233,64,205,0.14) 70%, rgba(138,35,135,0.16) 100%)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
boxShadow: '0 8px 30px rgba(0,0,0,0.35)',
|
||||
background: isAuthPage ?
|
||||
'none' : 'linear-gradient(71deg, rgba(242,113,33,0.18) 0%, rgba(233,64,205,0.14) 70%, rgba(138,35,135,0.16) 100%)',
|
||||
backdropFilter: isAuthPage ? 'none' : 'blur(10px)',
|
||||
boxShadow: isAuthPage ? 'none' : '0 8px 30px rgba(0,0,0,0.35)',
|
||||
}}
|
||||
>
|
||||
{/* Левая часть */}
|
||||
@ -433,6 +467,7 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
||||
<HeadAvatar
|
||||
skinUrl={skinUrl}
|
||||
size={44}
|
||||
version={skinVersion}
|
||||
style={{
|
||||
borderRadius: '3vw',
|
||||
cursor: 'pointer',
|
||||
@ -530,6 +565,7 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
||||
<HeadAvatar
|
||||
skinUrl={skinUrl}
|
||||
size={40}
|
||||
version={skinVersion}
|
||||
style={{ borderRadius: '3vw' }}
|
||||
/>
|
||||
|
||||
@ -547,7 +583,7 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
||||
username={username}
|
||||
size="medium"
|
||||
autoUpdate={true}
|
||||
showTooltip={true}
|
||||
showTooltip={false}
|
||||
sx={{
|
||||
border: 'none',
|
||||
padding: '0vw',
|
||||
@ -615,12 +651,31 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
||||
<EmojiEventsIcon sx={{ fontSize: '2vw' }} /> Ежедневная награда
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleAvatarMenuClose();
|
||||
navigate('/settings');
|
||||
}}
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '1.5vw',
|
||||
gap: '0.5vw',
|
||||
py: '0.7vw',
|
||||
'&:hover': {
|
||||
bgcolor: 'rgba(255,77,77,0.15)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SettingsIcon sx={{ fontSize: '2vw' }} /> Настройки
|
||||
</MenuItem>
|
||||
|
||||
<Divider sx={{ my: '0.4vw', borderColor: 'rgba(255,255,255,0.08)' }} />
|
||||
{!isLoginPage && !isRegistrationPage && username && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
handleAvatarMenuClose();
|
||||
logout();
|
||||
}}
|
||||
sx={{
|
||||
|
||||
Reference in New Issue
Block a user