add gradient to topbar, alert quests, restyle onlineplayerspanel
This commit is contained in:
@ -5,7 +5,6 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Paper,
|
Paper,
|
||||||
Chip,
|
Chip,
|
||||||
TextField,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Select,
|
Select,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -20,6 +19,7 @@ import {
|
|||||||
import { FullScreenLoader } from './FullScreenLoader';
|
import { FullScreenLoader } from './FullScreenLoader';
|
||||||
import { HeadAvatar } from './HeadAvatar';
|
import { HeadAvatar } from './HeadAvatar';
|
||||||
import { translateServer } from '../utils/serverTranslator';
|
import { translateServer } from '../utils/serverTranslator';
|
||||||
|
import GradientTextField from './GradientTextField'; // <-- используем ваш градиентный инпут
|
||||||
|
|
||||||
type OnlinePlayerFlat = {
|
type OnlinePlayerFlat = {
|
||||||
username: string;
|
username: string;
|
||||||
@ -33,6 +33,9 @@ interface OnlinePlayersPanelProps {
|
|||||||
currentUsername: string;
|
currentUsername: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GRADIENT =
|
||||||
|
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)';
|
||||||
|
|
||||||
export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
||||||
currentUsername,
|
currentUsername,
|
||||||
}) => {
|
}) => {
|
||||||
@ -42,7 +45,6 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [serverFilter, setServerFilter] = useState<string>('all');
|
const [serverFilter, setServerFilter] = useState<string>('all');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
const [skinMap, setSkinMap] = useState<Record<string, string>>({});
|
const [skinMap, setSkinMap] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -85,64 +87,46 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
|||||||
// Догружаем скины по uuid
|
// Догружаем скины по uuid
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSkins = async () => {
|
const loadSkins = async () => {
|
||||||
// Берём всех видимых игроков (чтобы не грузить для тысяч, если их много)
|
|
||||||
const uuids = Array.from(new Set(onlinePlayers.map((p) => p.uuid)));
|
const uuids = Array.from(new Set(onlinePlayers.map((p) => p.uuid)));
|
||||||
|
|
||||||
const toLoad = uuids.filter((uuid) => !skinMap[uuid]);
|
const toLoad = uuids.filter((uuid) => !skinMap[uuid]);
|
||||||
if (!toLoad.length) return;
|
if (!toLoad.length) return;
|
||||||
|
|
||||||
try {
|
for (const uuid of toLoad) {
|
||||||
// Просто по очереди, чтобы не DDOS'ить API
|
try {
|
||||||
for (const uuid of toLoad) {
|
const player = await fetchPlayer(uuid);
|
||||||
try {
|
if (player.skin_url) {
|
||||||
const player = await fetchPlayer(uuid);
|
setSkinMap((prev) => ({ ...prev, [uuid]: player.skin_url }));
|
||||||
if (player.skin_url) {
|
|
||||||
setSkinMap((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[uuid]: player.skin_url,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Не удалось получить скин для', uuid, e);
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Не удалось получить скин для', uuid, e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('Ошибка при загрузке скинов:', e);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadSkins();
|
loadSkins();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [onlinePlayers]);
|
}, [onlinePlayers]);
|
||||||
|
|
||||||
const filteredPlayers = useMemo(() => {
|
const filteredPlayers = useMemo(() => {
|
||||||
return (
|
return onlinePlayers
|
||||||
onlinePlayers
|
.filter((p) => (serverFilter === 'all' ? true : p.serverId === serverFilter))
|
||||||
.filter((p) =>
|
.filter((p) =>
|
||||||
serverFilter === 'all' ? true : p.serverId === serverFilter,
|
search.trim()
|
||||||
)
|
? p.username.toLowerCase().includes(search.toLowerCase())
|
||||||
.filter((p) =>
|
: true,
|
||||||
search.trim()
|
)
|
||||||
? p.username.toLowerCase().includes(search.toLowerCase())
|
.sort((a, b) => {
|
||||||
: true,
|
if (a.username === currentUsername && b.username !== currentUsername) return -1;
|
||||||
)
|
if (b.username === currentUsername && a.username !== currentUsername) return 1;
|
||||||
// свой ник наверх
|
return a.username.localeCompare(b.username);
|
||||||
.sort((a, b) => {
|
});
|
||||||
if (a.username === currentUsername && b.username !== currentUsername)
|
|
||||||
return -1;
|
|
||||||
if (b.username === currentUsername && a.username !== currentUsername)
|
|
||||||
return 1;
|
|
||||||
return a.username.localeCompare(b.username);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [onlinePlayers, serverFilter, search, currentUsername]);
|
}, [onlinePlayers, serverFilter, search, currentUsername]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) return <FullScreenLoader message="Загружаем игроков онлайн..." />;
|
||||||
return <FullScreenLoader message="Загружаем игроков онлайн..." />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<Typography color="error" sx={{ mt: 2 }}>
|
<Typography sx={{ mt: 2, color: '#ff8080', fontFamily: 'Benzin-Bold' }}>
|
||||||
{error}
|
{error}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
@ -150,7 +134,7 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
|||||||
|
|
||||||
if (!onlinePlayers.length) {
|
if (!onlinePlayers.length) {
|
||||||
return (
|
return (
|
||||||
<Typography sx={{ mt: 2, opacity: 0.8 }}>
|
<Typography sx={{ mt: 2, color: 'rgba(255,255,255,0.75)', fontWeight: 700 }}>
|
||||||
Сейчас на серверах никого нет.
|
Сейчас на серверах никого нет.
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
@ -160,121 +144,258 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
mt: 3,
|
mt: 3,
|
||||||
p: 2,
|
borderRadius: '1.2vw',
|
||||||
borderRadius: '1vw',
|
overflow: 'hidden',
|
||||||
background: 'rgba(255,255,255,0.03)',
|
background:
|
||||||
border: '1px solid rgba(255,255,255,0.06)',
|
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.16), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.12), transparent 55%), rgba(10,10,20,0.92)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
boxShadow: '0 1.2vw 3.2vw rgba(0,0,0,0.55)',
|
||||||
|
backdropFilter: 'blur(14px)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
}}
|
}}
|
||||||
elevation={0}
|
|
||||||
>
|
>
|
||||||
|
{/* header */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
px: '1.8vw',
|
||||||
justifyContent: 'space-between',
|
pt: '1.2vw',
|
||||||
alignItems: 'center',
|
pb: '1.1vw',
|
||||||
mb: 2,
|
position: 'sticky',
|
||||||
gap: 2,
|
top: 0,
|
||||||
|
zIndex: 2,
|
||||||
|
background:
|
||||||
|
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.16), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.12), transparent 55%), rgba(10,10,20,0.92)',
|
||||||
|
backdropFilter: 'blur(14px)',
|
||||||
|
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box
|
||||||
<Typography
|
sx={{
|
||||||
sx={{
|
display: 'flex',
|
||||||
fontFamily: 'Benzin-Bold',
|
justifyContent: 'space-between',
|
||||||
fontSize: '1.2vw',
|
alignItems: 'flex-end',
|
||||||
mb: 0.5,
|
gap: '1.6vw',
|
||||||
}}
|
flexWrap: 'wrap',
|
||||||
>
|
}}
|
||||||
Игроки онлайн
|
>
|
||||||
</Typography>
|
<Box sx={{ minWidth: 240 }}>
|
||||||
<Typography sx={{ fontSize: '0.9vw', opacity: 0.7 }}>
|
<Typography
|
||||||
Сейчас на серверах: {totalOnline}
|
sx={{
|
||||||
</Typography>
|
fontFamily: 'Benzin-Bold',
|
||||||
</Box>
|
fontSize: '1.35vw',
|
||||||
|
lineHeight: 1.1,
|
||||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
backgroundImage: GRADIENT,
|
||||||
<FormControl size="small" sx={{ minWidth: 160 }}>
|
WebkitBackgroundClip: 'text',
|
||||||
<InputLabel sx={{ color: 'white' }}>Сервер</InputLabel>
|
WebkitTextFillColor: 'transparent',
|
||||||
<Select
|
}}
|
||||||
label="Сервер"
|
|
||||||
value={serverFilter}
|
|
||||||
onChange={(e) => setServerFilter(e.target.value)}
|
|
||||||
sx={{ color: 'white' }}
|
|
||||||
>
|
>
|
||||||
<MenuItem value="all" sx={{ color: 'black' }}>
|
Игроки онлайн
|
||||||
Все сервера
|
</Typography>
|
||||||
</MenuItem>
|
<Typography sx={{ fontSize: '0.9vw', color: 'rgba(255,255,255,0.70)', fontWeight: 700 }}>
|
||||||
{servers.map((s) => (
|
Сейчас на серверах: {totalOnline}
|
||||||
<MenuItem key={s.id} value={s.id} sx={{ color: 'black' }}>
|
</Typography>
|
||||||
{s.name}
|
</Box>
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<TextField
|
<Box sx={{ display: 'flex', gap: '1vw', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
size="small"
|
{/* Select в “нашем” стиле */}
|
||||||
label="Поиск по нику"
|
<FormControl
|
||||||
value={search}
|
size="small"
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
sx={{
|
||||||
sx={{ color: 'white' }}
|
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)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InputLabel>Сервер</InputLabel>
|
||||||
|
<Select
|
||||||
|
label="Сервер"
|
||||||
|
value={serverFilter}
|
||||||
|
onChange={(e) => setServerFilter(e.target.value)}
|
||||||
|
MenuProps={{
|
||||||
|
PaperProps: {
|
||||||
|
sx: {
|
||||||
|
bgcolor: 'rgba(10,10,20,0.96)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.10)',
|
||||||
|
borderRadius: '1vw',
|
||||||
|
backdropFilter: 'blur(14px)',
|
||||||
|
'& .MuiMenuItem-root': {
|
||||||
|
color: 'rgba(255,255,255,0.9)',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
},
|
||||||
|
'& .MuiMenuItem-root.Mui-selected': {
|
||||||
|
backgroundColor: 'rgba(242,113,33,0.16)',
|
||||||
|
},
|
||||||
|
'& .MuiMenuItem-root:hover': {
|
||||||
|
backgroundColor: 'rgba(233,64,205,0.14)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
borderRadius: '999px',
|
||||||
|
bgcolor: 'rgba(255,255,255,0.04)',
|
||||||
|
'& .MuiSelect-select': {
|
||||||
|
py: '0.7vw',
|
||||||
|
px: '1.2vw',
|
||||||
|
},
|
||||||
|
'& .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',
|
||||||
|
},
|
||||||
|
'& .MuiSelect-icon': {
|
||||||
|
color: 'rgba(255,255,255,0.75)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="all">Все сервера</MenuItem>
|
||||||
|
{servers.map((s) => (
|
||||||
|
<MenuItem key={s.id} value={s.id}>
|
||||||
|
{s.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{/* Поиск через ваш GradientTextField */}
|
||||||
|
<Box sx={{ minWidth: '16vw' }}>
|
||||||
|
<GradientTextField
|
||||||
|
size="small"
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
'& .MuiInputLabel-root': {
|
||||||
|
// background: 'rgba(10,10,20,0.92)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* list */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
px: '1.8vw',
|
||||||
|
py: '1.3vw',
|
||||||
maxHeight: '35vh',
|
maxHeight: '35vh',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 1,
|
gap: '0.65vw',
|
||||||
|
|
||||||
|
// аккуратный скроллбар (webkit)
|
||||||
|
'&::-webkit-scrollbar': { width: '0.55vw' },
|
||||||
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
borderRadius: '999px',
|
||||||
|
background: 'rgba(255,255,255,0.12)',
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-thumb:hover': {
|
||||||
|
background: 'rgba(242,113,33,0.25)',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{filteredPlayers.map((p) => (
|
{filteredPlayers.map((p) => {
|
||||||
<Box
|
const isMe = p.username === currentUsername;
|
||||||
key={p.uuid}
|
|
||||||
sx={{
|
return (
|
||||||
display: 'flex',
|
<Paper
|
||||||
justifyContent: 'space-between',
|
key={p.uuid}
|
||||||
alignItems: 'center',
|
elevation={0}
|
||||||
py: 0.6,
|
sx={{
|
||||||
px: 1.5,
|
px: '1.1vw',
|
||||||
borderRadius: '999px',
|
py: '0.75vw',
|
||||||
background: 'rgba(0,0,0,0.35)',
|
borderRadius: '1.1vw',
|
||||||
}}
|
background: 'rgba(255,255,255,0.04)',
|
||||||
>
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
display: 'flex',
|
||||||
<HeadAvatar skinUrl={skinMap[p.uuid]} size={24} />
|
justifyContent: 'space-between',
|
||||||
<Typography sx={{ fontFamily: 'Benzin-Bold' }}>
|
alignItems: 'center',
|
||||||
{p.username}
|
gap: '1vw',
|
||||||
</Typography>
|
transition: 'transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease',
|
||||||
{p.username === currentUsername && (
|
'&:hover': {
|
||||||
|
transform: 'scale(1.01)',
|
||||||
|
borderColor: 'rgba(242,113,33,0.35)',
|
||||||
|
boxShadow: '0 0.8vw 2.4vw rgba(0,0,0,0.45)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: '0.8vw', minWidth: 0 }}>
|
||||||
|
<HeadAvatar skinUrl={skinMap[p.uuid]} size={26} />
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
color: 'rgba(255,255,255,0.92)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.username}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{isMe && (
|
||||||
|
<Chip
|
||||||
|
label="Вы"
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
height: '1.55rem',
|
||||||
|
fontSize: '0.72rem',
|
||||||
|
fontWeight: 900,
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: '999px',
|
||||||
|
backgroundImage: GRADIENT,
|
||||||
|
boxShadow: '0 10px 22px rgba(0,0,0,0.45)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: '0.6vw', flexShrink: 0 }}>
|
||||||
<Chip
|
<Chip
|
||||||
label="Вы"
|
label={translateServer({ name: p.serverName })}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
height: '1.4rem',
|
fontFamily: 'Benzin-Bold',
|
||||||
fontSize: '0.7rem',
|
fontSize: '0.72rem',
|
||||||
bgcolor: 'rgb(255,77,77)',
|
borderRadius: '999px',
|
||||||
color: 'white',
|
color: 'rgba(255,255,255,0.88)',
|
||||||
|
background:
|
||||||
|
'linear-gradient(120deg, rgba(242,113,33,0.18), rgba(233,64,205,0.12), rgba(138,35,135,0.16))',
|
||||||
|
border: '1px solid rgba(255,255,255,0.10)',
|
||||||
|
backdropFilter: 'blur(12px)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
{/* onlineSince можно потом красиво форматировать */}
|
||||||
</Box>
|
</Box>
|
||||||
|
</Paper>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
);
|
||||||
<Chip
|
})}
|
||||||
label={translateServer({ name: p.serverName })}
|
|
||||||
size="small"
|
|
||||||
sx={{ bgcolor: 'rgba(255,255,255,0.08)', color: 'white' }}
|
|
||||||
/>
|
|
||||||
{/* Можно позже красиво форматировать onlineSince */}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import CustomTooltip from './Notifications/CustomTooltip';
|
import CustomTooltip from './Notifications/CustomTooltip';
|
||||||
import CoinsDisplay from './CoinsDisplay';
|
import CoinsDisplay from './CoinsDisplay';
|
||||||
import { HeadAvatar } from './HeadAvatar';
|
import { HeadAvatar } from './HeadAvatar';
|
||||||
@ -46,6 +46,23 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
|||||||
const isRegistrationPage = location.pathname === '/registration';
|
const isRegistrationPage = location.pathname === '/registration';
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const tabsWrapperRef = useRef<HTMLDivElement | null>(null);
|
const tabsWrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const tabsRootRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const updateGradientVars = useCallback(() => {
|
||||||
|
const root = tabsRootRef.current;
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
const tabsRect = root.getBoundingClientRect();
|
||||||
|
const active = root.querySelector<HTMLElement>('.MuiTab-root.Mui-selected');
|
||||||
|
if (!active) return;
|
||||||
|
|
||||||
|
const activeRect = active.getBoundingClientRect();
|
||||||
|
const x = activeRect.left - tabsRect.left;
|
||||||
|
|
||||||
|
root.style.setProperty('--tabs-w', `${tabsRect.width}px`);
|
||||||
|
root.style.setProperty('--active-x', `${x}px`);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [skinUrl, setSkinUrl] = useState<string>('');
|
const [skinUrl, setSkinUrl] = useState<string>('');
|
||||||
const [avatarAnchorEl, setAvatarAnchorEl] = useState<null | HTMLElement>(
|
const [avatarAnchorEl, setAvatarAnchorEl] = useState<null | HTMLElement>(
|
||||||
null,
|
null,
|
||||||
@ -81,6 +98,12 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
|||||||
const selectedTab =
|
const selectedTab =
|
||||||
TAB_ROUTES.find((r) => r.match(location.pathname))?.value ?? false;
|
TAB_ROUTES.find((r) => r.match(location.pathname))?.value ?? false;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateGradientVars();
|
||||||
|
window.addEventListener('resize', updateGradientVars);
|
||||||
|
return () => window.removeEventListener('resize', updateGradientVars);
|
||||||
|
}, [updateGradientVars, selectedTab, location.pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saved = localStorage.getItem('launcher_config');
|
const saved = localStorage.getItem('launcher_config');
|
||||||
try {
|
try {
|
||||||
@ -140,6 +163,8 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
|||||||
|
|
||||||
// Прокручиваем горизонтально, используя вертикальный скролл мыши
|
// Прокручиваем горизонтально, используя вертикальный скролл мыши
|
||||||
scroller.scrollLeft += event.deltaY * 0.3;
|
scroller.scrollLeft += event.deltaY * 0.3;
|
||||||
|
|
||||||
|
requestAnimationFrame(updateGradientVars);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const getPageTitle = () => {
|
// const getPageTitle = () => {
|
||||||
@ -168,6 +193,27 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
|||||||
|
|
||||||
// Функция для получения количества монет
|
// Функция для получения количества монет
|
||||||
|
|
||||||
|
const tabBaseSx = {
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
fontSize: '0.7em',
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
'&:hover': {
|
||||||
|
color: 'rgb(170, 170, 170)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeTabSx = {
|
||||||
|
color: 'transparent',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
backgroundImage: 'var(--tabs-grad)',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundSize: 'var(--tabs-w) 100%',
|
||||||
|
backgroundPosition: 'calc(-1 * var(--active-x)) 0',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
backgroundClip: 'text',
|
||||||
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
localStorage.removeItem('launcher_config');
|
localStorage.removeItem('launcher_config');
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
@ -208,7 +254,6 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
|||||||
background:
|
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%)',
|
'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)',
|
backdropFilter: 'blur(10px)',
|
||||||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
|
||||||
boxShadow: '0 8px 30px rgba(0,0,0,0.35)',
|
boxShadow: '0 8px 30px rgba(0,0,0,0.35)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -248,29 +293,29 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
|||||||
ref={tabsWrapperRef}
|
ref={tabsWrapperRef}
|
||||||
onWheel={handleTabsWheel}
|
onWheel={handleTabsWheel}
|
||||||
// старый вариант
|
// старый вариант
|
||||||
|
sx={{
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
// '& .MuiTabs-indicator': {
|
||||||
|
// backgroundColor: 'rgba(255, 77, 77, 1)',
|
||||||
|
// },
|
||||||
|
}}
|
||||||
// sx={{
|
// sx={{
|
||||||
// borderBottom: 1,
|
// borderBottom: 'none',
|
||||||
// borderColor: 'transparent',
|
// borderRadius: '2vw',
|
||||||
|
// px: '0.6vw',
|
||||||
|
// py: '0.4vw',
|
||||||
|
// background: 'rgba(0,0,0,0.35)',
|
||||||
|
// border: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
// boxShadow: '0 8px 20px rgba(0,0,0,0.25)',
|
||||||
// '& .MuiTabs-indicator': {
|
// '& .MuiTabs-indicator': {
|
||||||
// backgroundColor: 'rgba(255, 77, 77, 1)',
|
// height: '100%',
|
||||||
|
// borderRadius: '1.6vw',
|
||||||
|
// background:
|
||||||
|
// 'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
|
||||||
|
// opacity: 0.18,
|
||||||
// },
|
// },
|
||||||
// }}
|
// }}
|
||||||
sx={{
|
|
||||||
borderBottom: 'none',
|
|
||||||
borderRadius: '2vw',
|
|
||||||
px: '0.6vw',
|
|
||||||
py: '0.4vw',
|
|
||||||
background: 'rgba(0,0,0,0.35)',
|
|
||||||
border: '1px solid rgba(255,255,255,0.08)',
|
|
||||||
boxShadow: '0 8px 20px rgba(0,0,0,0.25)',
|
|
||||||
'& .MuiTabs-indicator': {
|
|
||||||
height: '100%',
|
|
||||||
borderRadius: '1.6vw',
|
|
||||||
background:
|
|
||||||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
|
|
||||||
opacity: 0.18,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<CustomTooltip
|
<CustomTooltip
|
||||||
title={
|
title={
|
||||||
@ -281,81 +326,71 @@ export default function TopBar({ onRegister, username }: TopBarProps) {
|
|||||||
TransitionProps={{ timeout: 100 }}
|
TransitionProps={{ timeout: 100 }}
|
||||||
>
|
>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
ref={tabsRootRef}
|
||||||
value={selectedTab}
|
value={selectedTab}
|
||||||
onChange={(_, newValue) => {
|
onChange={(_, newValue) => {
|
||||||
const route = TAB_ROUTES.find((r) => r.value === newValue);
|
const route = TAB_ROUTES.find((r) => r.value === newValue);
|
||||||
if (route) {
|
if (route) navigate(route.to);
|
||||||
navigate(route.to);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
aria-label="basic tabs example"
|
aria-label="basic tabs example"
|
||||||
variant="scrollable"
|
variant="scrollable"
|
||||||
scrollButtons={false}
|
scrollButtons={false}
|
||||||
disableRipple={true}
|
disableRipple={true}
|
||||||
sx={{ maxWidth: '42vw' }}
|
sx={{
|
||||||
|
// один градиент на весь Tabs
|
||||||
|
'--tabs-grad': 'linear-gradient(90deg, #F27121 0%, #E940CD 50%, #8A2387 100%)',
|
||||||
|
|
||||||
|
// активный текст показывает “срез” общего градиента
|
||||||
|
'& .MuiTab-root.Mui-selected': {
|
||||||
|
color: 'transparent',
|
||||||
|
backgroundImage: 'var(--tabs-grad)',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundSize: 'var(--tabs-w) 100%',
|
||||||
|
backgroundPosition: 'calc(-1 * var(--active-x)) 0',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
backgroundClip: 'text',
|
||||||
|
},
|
||||||
|
|
||||||
|
// подчёркивание тоже из того же “единого” градиента
|
||||||
|
'& .MuiTabs-indicator': {
|
||||||
|
height: '2px',
|
||||||
|
backgroundImage: 'var(--tabs-grad)',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundSize: 'var(--tabs-w) 100%',
|
||||||
|
backgroundPosition: 'calc(-1 * var(--active-x)) 0',
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tab
|
<Tab
|
||||||
label="Новости"
|
label="Новости"
|
||||||
disableRipple={true}
|
disableRipple={true}
|
||||||
sx={{
|
sx={{
|
||||||
color: 'white',
|
...tabBaseSx,
|
||||||
fontFamily: 'Benzin-Bold',
|
...(selectedTab === 0 ? activeTabSx : null),
|
||||||
fontSize: '0.7em',
|
|
||||||
'&.Mui-selected': {
|
|
||||||
color: 'rgba(255, 77, 77, 1)',
|
|
||||||
},
|
|
||||||
'&:hover': {
|
|
||||||
color: 'rgb(177, 52, 52)',
|
|
||||||
},
|
|
||||||
transition: 'all 0.3s ease',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
label="Версии"
|
label="Версии"
|
||||||
disableRipple={true}
|
disableRipple={true}
|
||||||
sx={{
|
sx={{
|
||||||
color: 'white',
|
...tabBaseSx,
|
||||||
fontFamily: 'Benzin-Bold',
|
...(selectedTab === 1 ? activeTabSx : null),
|
||||||
fontSize: '0.7em',
|
|
||||||
'&.Mui-selected': {
|
|
||||||
color: 'rgba(255, 77, 77, 1)',
|
|
||||||
},
|
|
||||||
'&:hover': {
|
|
||||||
color: 'rgb(177, 52, 52)',
|
|
||||||
},
|
|
||||||
transition: 'all 0.3s ease',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
label="Магазин"
|
label="Магазин"
|
||||||
disableRipple={true}
|
disableRipple={true}
|
||||||
sx={{
|
sx={{
|
||||||
color: 'white',
|
...tabBaseSx,
|
||||||
fontFamily: 'Benzin-Bold',
|
...(selectedTab === 2 ? activeTabSx : null),
|
||||||
fontSize: '0.7em',
|
|
||||||
'&.Mui-selected': {
|
|
||||||
color: 'rgba(255, 77, 77, 1)',
|
|
||||||
},
|
|
||||||
'&:hover': {
|
|
||||||
color: 'rgb(177, 52, 52)',
|
|
||||||
},
|
|
||||||
transition: 'all 0.3s ease',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
label="Рынок"
|
label="Рынок"
|
||||||
disableRipple={true}
|
disableRipple={true}
|
||||||
sx={{
|
sx={{
|
||||||
color: 'white',
|
...tabBaseSx,
|
||||||
fontFamily: 'Benzin-Bold',
|
...(selectedTab === 3 ? activeTabSx : null),
|
||||||
fontSize: '0.7em',
|
|
||||||
'&.Mui-selected': {
|
|
||||||
color: 'rgba(255, 77, 77, 1)',
|
|
||||||
},
|
|
||||||
'&:hover': {
|
|
||||||
color: 'rgb(177, 52, 52)',
|
|
||||||
},
|
|
||||||
transition: 'all 0.3s ease',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@ -210,6 +210,8 @@ export default function DailyQuests() {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: '2.5vw',
|
borderRadius: '2.5vw',
|
||||||
|
fontSize: '1vw',
|
||||||
|
px: '3vw',
|
||||||
fontFamily: 'Benzin-Bold',
|
fontFamily: 'Benzin-Bold',
|
||||||
borderColor: 'rgba(255,255,255,0.25)',
|
borderColor: 'rgba(255,255,255,0.25)',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
@ -231,8 +233,50 @@ export default function DailyQuests() {
|
|||||||
{/* content */}
|
{/* content */}
|
||||||
<Box sx={{ px: '2vw', py: '2vh', overflowY: 'auto', flex: 1 }}>
|
<Box sx={{ px: '2vw', py: '2vh', overflowY: 'auto', flex: 1 }}>
|
||||||
{!wasOnlineToday && (
|
{!wasOnlineToday && (
|
||||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
<Alert
|
||||||
Зайдите на сервер сегодня, чтобы открыть получение наград за квесты.
|
severity="warning"
|
||||||
|
icon={false}
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
borderRadius: '1.1vw',
|
||||||
|
px: '1.4vw',
|
||||||
|
py: '1.1vw',
|
||||||
|
color: 'rgba(255,255,255,0.90)',
|
||||||
|
fontWeight: 800,
|
||||||
|
bgcolor: 'rgba(255,255,255,0.04)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.10)',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
boxShadow: '0 1.0vw 2.8vw rgba(0,0,0,0.45)',
|
||||||
|
'& .MuiAlert-message': {
|
||||||
|
padding: 0,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
'&:before': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
background:
|
||||||
|
'radial-gradient(circle at 12% 30%, rgba(242,113,33,0.22), transparent 60%), radial-gradient(circle at 85% 0%, rgba(233,64,205,0.14), transparent 55%)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
'&:after': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: '0.35vw',
|
||||||
|
background: 'linear-gradient(180deg, #F27121 0%, #E940CD 65%, #8A2387 100%)',
|
||||||
|
opacity: 0.95,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ position: 'relative', zIndex: 1, color: 'rgba(255,255,255,0.90)', fontWeight: 800 }}>
|
||||||
|
Зайдите на сервер сегодня, чтобы открыть получение наград за квесты.
|
||||||
|
</Typography>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user