diff --git a/src/renderer/components/OnlinePlayersPanel.tsx b/src/renderer/components/OnlinePlayersPanel.tsx index 2d485a8..59d6b35 100644 --- a/src/renderer/components/OnlinePlayersPanel.tsx +++ b/src/renderer/components/OnlinePlayersPanel.tsx @@ -5,7 +5,6 @@ import { Typography, Paper, Chip, - TextField, MenuItem, Select, FormControl, @@ -20,6 +19,7 @@ import { import { FullScreenLoader } from './FullScreenLoader'; import { HeadAvatar } from './HeadAvatar'; import { translateServer } from '../utils/serverTranslator'; +import GradientTextField from './GradientTextField'; // <-- используем ваш градиентный инпут type OnlinePlayerFlat = { username: string; @@ -33,6 +33,9 @@ interface OnlinePlayersPanelProps { currentUsername: string; } +const GRADIENT = + 'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)'; + export const OnlinePlayersPanel: React.FC = ({ currentUsername, }) => { @@ -42,7 +45,6 @@ export const OnlinePlayersPanel: React.FC = ({ const [error, setError] = useState(null); const [serverFilter, setServerFilter] = useState('all'); const [search, setSearch] = useState(''); - const [skinMap, setSkinMap] = useState>({}); useEffect(() => { @@ -85,64 +87,46 @@ export const OnlinePlayersPanel: React.FC = ({ // Догружаем скины по uuid useEffect(() => { const loadSkins = async () => { - // Берём всех видимых игроков (чтобы не грузить для тысяч, если их много) const uuids = Array.from(new Set(onlinePlayers.map((p) => p.uuid))); - const toLoad = uuids.filter((uuid) => !skinMap[uuid]); if (!toLoad.length) return; - try { - // Просто по очереди, чтобы не DDOS'ить API - for (const uuid of toLoad) { - try { - const player = await fetchPlayer(uuid); - if (player.skin_url) { - setSkinMap((prev) => ({ - ...prev, - [uuid]: player.skin_url, - })); - } - } catch (e) { - console.warn('Не удалось получить скин для', uuid, e); + for (const uuid of toLoad) { + try { + const player = await fetchPlayer(uuid); + if (player.skin_url) { + setSkinMap((prev) => ({ ...prev, [uuid]: player.skin_url })); } + } catch (e) { + console.warn('Не удалось получить скин для', uuid, e); } - } catch (e) { - console.error('Ошибка при загрузке скинов:', e); } }; loadSkins(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [onlinePlayers]); const filteredPlayers = useMemo(() => { - return ( - onlinePlayers - .filter((p) => - serverFilter === 'all' ? true : p.serverId === serverFilter, - ) - .filter((p) => - search.trim() - ? p.username.toLowerCase().includes(search.toLowerCase()) - : true, - ) - // свой ник наверх - .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); - }) - ); + return onlinePlayers + .filter((p) => (serverFilter === 'all' ? true : p.serverId === serverFilter)) + .filter((p) => + search.trim() + ? p.username.toLowerCase().includes(search.toLowerCase()) + : true, + ) + .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]); - if (loading) { - return ; - } + if (loading) return ; if (error) { return ( - + {error} ); @@ -150,7 +134,7 @@ export const OnlinePlayersPanel: React.FC = ({ if (!onlinePlayers.length) { return ( - + Сейчас на серверах никого нет. ); @@ -160,121 +144,258 @@ export const OnlinePlayersPanel: React.FC = ({ return ( + {/* header */} - - - Игроки онлайн - - - Сейчас на серверах: {totalOnline} - - - - - - Сервер - - + Игроки онлайн + + + Сейчас на серверах: {totalOnline} + + - setSearch(e.target.value)} - sx={{ color: 'white' }} - /> + + {/* Select в “нашем” стиле */} + + Сервер + + + + {/* Поиск через ваш GradientTextField */} + + 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)', + }, + }} + /> + + + {/* list */} - {filteredPlayers.map((p) => ( - - - - - {p.username} - - {p.username === currentUsername && ( + {filteredPlayers.map((p) => { + const isMe = p.username === currentUsername; + + return ( + + + + + {p.username} + + + {isMe && ( + + )} + + + - )} - - - - - {/* Можно позже красиво форматировать onlineSince */} - - - ))} + {/* onlineSince можно потом красиво форматировать */} + + + ); + })} ); diff --git a/src/renderer/components/TopBar.tsx b/src/renderer/components/TopBar.tsx index 7532073..726bf75 100644 --- a/src/renderer/components/TopBar.tsx +++ b/src/renderer/components/TopBar.tsx @@ -11,7 +11,7 @@ import { import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import { useLocation, useNavigate } from 'react-router-dom'; 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 CoinsDisplay from './CoinsDisplay'; import { HeadAvatar } from './HeadAvatar'; @@ -46,6 +46,23 @@ export default function TopBar({ onRegister, username }: TopBarProps) { const isRegistrationPage = location.pathname === '/registration'; const navigate = useNavigate(); const tabsWrapperRef = useRef(null); + const tabsRootRef = useRef(null); + + const updateGradientVars = useCallback(() => { + const root = tabsRootRef.current; + if (!root) return; + + const tabsRect = root.getBoundingClientRect(); + const active = root.querySelector('.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(''); const [avatarAnchorEl, setAvatarAnchorEl] = useState( null, @@ -81,6 +98,12 @@ export default function TopBar({ onRegister, username }: TopBarProps) { const selectedTab = 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(() => { const saved = localStorage.getItem('launcher_config'); try { @@ -140,6 +163,8 @@ export default function TopBar({ onRegister, username }: TopBarProps) { // Прокручиваем горизонтально, используя вертикальный скролл мыши scroller.scrollLeft += event.deltaY * 0.3; + + requestAnimationFrame(updateGradientVars); }; // 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 = () => { localStorage.removeItem('launcher_config'); navigate('/login'); @@ -208,7 +254,6 @@ export default function TopBar({ onRegister, username }: TopBarProps) { 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)', - borderBottom: '1px solid rgba(255,255,255,0.08)', boxShadow: '0 8px 30px rgba(0,0,0,0.35)', }} > @@ -248,29 +293,29 @@ export default function TopBar({ onRegister, username }: TopBarProps) { ref={tabsWrapperRef} onWheel={handleTabsWheel} // старый вариант + sx={{ + borderBottom: 1, + borderColor: 'transparent', + // '& .MuiTabs-indicator': { + // backgroundColor: 'rgba(255, 77, 77, 1)', + // }, + }} // sx={{ - // borderBottom: 1, - // borderColor: 'transparent', + // 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': { - // 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, - }, - }} > { const route = TAB_ROUTES.find((r) => r.value === newValue); - if (route) { - navigate(route.to); - } + if (route) navigate(route.to); }} aria-label="basic tabs example" variant="scrollable" scrollButtons={false} 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', + }, + }} > diff --git a/src/renderer/pages/DailyQuests.tsx b/src/renderer/pages/DailyQuests.tsx index 00e2c0a..881607e 100644 --- a/src/renderer/pages/DailyQuests.tsx +++ b/src/renderer/pages/DailyQuests.tsx @@ -210,6 +210,8 @@ export default function DailyQuests() { variant="outlined" sx={{ borderRadius: '2.5vw', + fontSize: '1vw', + px: '3vw', fontFamily: 'Benzin-Bold', borderColor: 'rgba(255,255,255,0.25)', color: '#fff', @@ -231,8 +233,50 @@ export default function DailyQuests() { {/* content */} {!wasOnlineToday && ( - - Зайдите на сервер сегодня, чтобы открыть получение наград за квесты. + + + Зайдите на сервер сегодня, чтобы открыть получение наград за квесты. + )}