772 lines
26 KiB
TypeScript
772 lines
26 KiB
TypeScript
import {
|
||
Box, Typography, Button, Grid,
|
||
FormControl, Select, MenuItem, InputLabel
|
||
} from '@mui/material';
|
||
import {
|
||
Cape,
|
||
fetchCapes,
|
||
fetchCapesStore,
|
||
purchaseCape,
|
||
StoreCape,
|
||
Case,
|
||
CaseItem,
|
||
fetchCases,
|
||
fetchCase,
|
||
openCase,
|
||
Server,
|
||
fetchPlayer,
|
||
BonusType,
|
||
UserBonus,
|
||
fetchBonusTypes,
|
||
fetchUserBonuses,
|
||
purchaseBonus,
|
||
upgradeBonus,
|
||
toggleBonusActivation,
|
||
} from '../api';
|
||
import { useEffect, useState } from 'react';
|
||
import { FullScreenLoader } from '../components/FullScreenLoader';
|
||
import { getPlayerServer } from '../utils/playerOnlineCheck';
|
||
import CaseRoulette from '../components/CaseRoulette';
|
||
import BonusShopItem from '../components/BonusShopItem';
|
||
import ShopItem from '../components/ShopItem';
|
||
import { playBuySound, primeSounds } from '../utils/sounds';
|
||
import CustomNotification from '../components/Notifications/CustomNotification';
|
||
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
|
||
import { isNotificationsEnabled, getNotifPositionFromSettings } from '../utils/notifications';
|
||
import { translateServer } from '../utils/serverTranslator';
|
||
|
||
function getRarityByWeight(
|
||
weight?: number,
|
||
): 'common' | 'rare' | 'epic' | 'legendary' {
|
||
if (weight === undefined || weight === null) return 'common';
|
||
|
||
if (weight <= 5) return 'legendary';
|
||
if (weight <= 20) return 'epic';
|
||
if (weight <= 50) return 'rare';
|
||
return 'common';
|
||
}
|
||
|
||
function getRarityColor(weight?: number): string {
|
||
const rarity = getRarityByWeight(weight);
|
||
switch (rarity) {
|
||
case 'legendary':
|
||
return 'rgba(255, 215, 0, 1)'; // золотой
|
||
case 'epic':
|
||
return 'rgba(186, 85, 211, 1)'; // фиолетовый
|
||
case 'rare':
|
||
return 'rgba(65, 105, 225, 1)'; // синий
|
||
case 'common':
|
||
default:
|
||
return 'rgba(255, 255, 255, 0.25)'; // сероватый
|
||
}
|
||
}
|
||
|
||
export default function Shop() {
|
||
const [storeCapes, setStoreCapes] = useState<StoreCape[]>([]);
|
||
const [userCapes, setUserCapes] = useState<Cape[]>([]);
|
||
const [username, setUsername] = useState<string>('');
|
||
const [uuid, setUuid] = useState<string>('');
|
||
const [loading, setLoading] = useState<boolean>(false);
|
||
|
||
const [playerSkinUrl, setPlayerSkinUrl] = useState<string>('');
|
||
|
||
const [selectedCaseServerIp, setSelectedCaseServerIp] = useState<string>('');
|
||
|
||
// Уведомления
|
||
|
||
const [notifOpen, setNotifOpen] = useState(false);
|
||
const [notifMsg, setNotifMsg] = useState<React.ReactNode>('');
|
||
const [notifSeverity, setNotifSeverity] = useState<
|
||
'success' | 'info' | 'warning' | 'error'
|
||
>('info');
|
||
|
||
const [notifPos, setNotifPos] = useState<NotificationPosition>({
|
||
vertical: 'bottom',
|
||
horizontal: 'center',
|
||
});
|
||
|
||
// Прокачка
|
||
|
||
const [bonusTypes, setBonusTypes] = useState<BonusType[]>([]);
|
||
const [userBonuses, setUserBonuses] = useState<UserBonus[]>([]);
|
||
const [bonusesLoading, setBonusesLoading] = useState<boolean>(false);
|
||
const [processingBonusIds, setProcessingBonusIds] = useState<string[]>([]);
|
||
|
||
// Кейсы
|
||
const [cases, setCases] = useState<Case[]>([]);
|
||
const [casesLoading, setCasesLoading] = useState<boolean>(false);
|
||
|
||
// Онлайн/сервер (по аналогии с Marketplace)
|
||
const [isOnline, setIsOnline] = useState<boolean>(false);
|
||
const [playerServer, setPlayerServer] = useState<Server | null>(null);
|
||
const [onlineCheckLoading, setOnlineCheckLoading] = useState<boolean>(true);
|
||
|
||
// Рулетка
|
||
const [isOpening, setIsOpening] = useState<boolean>(false);
|
||
const [selectedCase, setSelectedCase] = useState<Case | null>(null);
|
||
|
||
const [rouletteOpen, setRouletteOpen] = useState(false);
|
||
const [rouletteCaseItems, setRouletteCaseItems] = useState<CaseItem[]>([]);
|
||
const [rouletteReward, setRouletteReward] = useState<CaseItem | null>(null);
|
||
|
||
// Уведомления
|
||
const [notification, setNotification] = useState<{
|
||
open: boolean;
|
||
message: string;
|
||
type: 'success' | 'error';
|
||
}>({
|
||
open: false,
|
||
message: '',
|
||
type: 'success',
|
||
});
|
||
|
||
const loadBonuses = async (username: string) => {
|
||
try {
|
||
setBonusesLoading(true);
|
||
const [types, user] = await Promise.all([
|
||
fetchBonusTypes(),
|
||
fetchUserBonuses(username),
|
||
]);
|
||
setBonusTypes(types);
|
||
setUserBonuses(user);
|
||
} catch (error) {
|
||
console.error('Ошибка при получении прокачек:', error);
|
||
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg('Ошибка при загрузке прокачки!');
|
||
setNotifSeverity('error');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
} finally {
|
||
setBonusesLoading(false);
|
||
}
|
||
};
|
||
|
||
const loadPlayerSkin = async (uuid: string) => {
|
||
try {
|
||
const player = await fetchPlayer(uuid);
|
||
setPlayerSkinUrl(player.skin_url);
|
||
} catch (error) {
|
||
console.error('Ошибка при получении скина игрока:', error);
|
||
setPlayerSkinUrl('');
|
||
}
|
||
};
|
||
|
||
// Функция для загрузки плащей из магазина
|
||
const loadStoreCapes = async () => {
|
||
try {
|
||
const capes = await fetchCapesStore();
|
||
setStoreCapes(capes);
|
||
} catch (error) {
|
||
console.error('Ошибка при получении плащей магазина:', error);
|
||
setStoreCapes([]);
|
||
}
|
||
};
|
||
|
||
// Функция для загрузки плащей пользователя
|
||
const loadUserCapes = async (username: string) => {
|
||
try {
|
||
const userCapes = await fetchCapes(username);
|
||
setUserCapes(userCapes);
|
||
} catch (error) {
|
||
console.error('Ошибка при получении плащей пользователя:', error);
|
||
setUserCapes([]);
|
||
}
|
||
};
|
||
|
||
const handlePurchaseCape = async (cape_id: string) => {
|
||
try {
|
||
await purchaseCape(username, cape_id);
|
||
await loadUserCapes(username);
|
||
|
||
playBuySound();
|
||
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg('Плащ успешно куплен!');
|
||
setNotifSeverity('success');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
} catch (error) {
|
||
console.error('Ошибка при покупке плаща:', error);
|
||
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg('Ошибка при покупке плаща!');
|
||
setNotifSeverity('error');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
}
|
||
};
|
||
|
||
// Загрузка кейсов
|
||
const loadCases = async () => {
|
||
try {
|
||
setCasesLoading(true);
|
||
const casesData = await fetchCases();
|
||
setCases(casesData);
|
||
} catch (error) {
|
||
console.error('Ошибка при получении кейсов:', error);
|
||
setCases([]);
|
||
} finally {
|
||
setCasesLoading(false);
|
||
}
|
||
};
|
||
|
||
// Проверка онлайна игрока (по аналогии с Marketplace.tsx)
|
||
const checkPlayerStatus = async () => {
|
||
if (!username) return;
|
||
|
||
try {
|
||
setOnlineCheckLoading(true);
|
||
const { online, server } = await getPlayerServer(username);
|
||
setIsOnline(online);
|
||
setPlayerServer(server || null);
|
||
} catch (error) {
|
||
console.error('Ошибка при проверке онлайн-статуса:', error);
|
||
setIsOnline(false);
|
||
setPlayerServer(null);
|
||
} finally {
|
||
setOnlineCheckLoading(false);
|
||
}
|
||
};
|
||
|
||
// Загружаем базовые данные при монтировании
|
||
useEffect(() => {
|
||
const savedConfig = localStorage.getItem('launcher_config');
|
||
if (savedConfig) {
|
||
const config = JSON.parse(savedConfig);
|
||
if (config.uuid && config.username) {
|
||
setUsername(config.username);
|
||
setUuid(config.uuid);
|
||
|
||
setLoading(true);
|
||
|
||
Promise.all([
|
||
loadStoreCapes(),
|
||
loadUserCapes(config.username),
|
||
loadCases(),
|
||
loadPlayerSkin(config.uuid),
|
||
loadBonuses(config.username),
|
||
])
|
||
.catch((err) => console.error(err))
|
||
.finally(() => {
|
||
setLoading(false);
|
||
});
|
||
}
|
||
}
|
||
}, []);
|
||
|
||
// Проверяем онлайн после того, как знаем username
|
||
useEffect(() => {
|
||
if (username) {
|
||
checkPlayerStatus();
|
||
}
|
||
}, [username]);
|
||
|
||
const withProcessing = async (id: string, fn: () => Promise<void>) => {
|
||
setProcessingBonusIds((prev) => [...prev, id]);
|
||
try {
|
||
await fn();
|
||
} finally {
|
||
setProcessingBonusIds((prev) => prev.filter((x) => x !== id));
|
||
}
|
||
};
|
||
|
||
const handlePurchaseBonus = async (bonusTypeId: string) => {
|
||
if (!username) {
|
||
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg('Не найдено имя игрока. Авторизируйтесь в лаунчере!');
|
||
setNotifSeverity('error');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
return;
|
||
}
|
||
|
||
await withProcessing(bonusTypeId, async () => {
|
||
try {
|
||
const res = await purchaseBonus(username, bonusTypeId);
|
||
|
||
playBuySound();
|
||
|
||
await loadBonuses(username);
|
||
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg('Прокачка успешно куплена!');
|
||
setNotifSeverity('success');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
} catch (error) {
|
||
console.error('Ошибка при покупке прокачки:', error);
|
||
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg('Ошибка при прокачке!');
|
||
setNotifSeverity('error');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
}
|
||
});
|
||
};
|
||
|
||
const handleUpgradeBonus = async (bonusId: string) => {
|
||
if (!username) return;
|
||
|
||
await withProcessing(bonusId, async () => {
|
||
try {
|
||
await upgradeBonus(username, bonusId);
|
||
|
||
await loadBonuses(username);
|
||
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg('Бонус улучшен!');
|
||
setNotifSeverity('success');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
} catch (error) {
|
||
console.error('Ошибка при улучшении бонуса:', error);
|
||
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg('Ошибка при улучшении бонуса!');
|
||
setNotifSeverity('error');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
}
|
||
});
|
||
};
|
||
|
||
const handleToggleBonusActivation = async (bonusId: string) => {
|
||
if (!username) return;
|
||
|
||
await withProcessing(bonusId, async () => {
|
||
try {
|
||
await toggleBonusActivation(username, bonusId);
|
||
await loadBonuses(username);
|
||
} catch (error) {
|
||
console.error('Ошибка при переключении бонуса:', error);
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg('Ошибка при переключении бонуса!');
|
||
setNotifSeverity('error');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
}
|
||
});
|
||
};
|
||
|
||
const caseServers = Array.from(
|
||
new Set(
|
||
(cases || [])
|
||
.flatMap((c) => c.server_ips || [])
|
||
.filter(Boolean),
|
||
),
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (caseServers.length > 0) {
|
||
// если игрок онлайн — по умолчанию его сервер, если он есть в кейсах
|
||
const preferred =
|
||
playerServer?.ip && caseServers.includes(playerServer.ip)
|
||
? playerServer.ip
|
||
: caseServers[0];
|
||
|
||
setSelectedCaseServerIp(preferred);
|
||
}
|
||
}, [caseServers.length, playerServer?.ip]);
|
||
|
||
const filteredCases = (cases || []).filter((c) => {
|
||
const allowed = c.server_ips || [];
|
||
// если список пуст — значит кейс доступен везде
|
||
if (allowed.length === 0) return true;
|
||
|
||
// иначе — показываем только те, где выбранный сервер разрешён
|
||
return !!selectedCaseServerIp && allowed.includes(selectedCaseServerIp);
|
||
});
|
||
|
||
// Фильтруем плащи, которые уже куплены пользователем
|
||
const availableCapes = storeCapes.filter(
|
||
(storeCape) =>
|
||
!userCapes.some((userCape) => userCape.cape_id === storeCape.id),
|
||
);
|
||
|
||
const handleOpenCase = async (caseData: Case) => {
|
||
if (!username) {
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg('Не найдено имя игрока. Авторизуйтесь в лаунчере!');
|
||
setNotifSeverity('error');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
return;
|
||
}
|
||
|
||
if (!selectedCaseServerIp) {
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg('Выберите сервер для открытия кейса!');
|
||
setNotifSeverity('warning');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
return;
|
||
}
|
||
|
||
const allowedIps = caseData.server_ips || [];
|
||
if (allowedIps.length > 0 && !allowedIps.includes(selectedCaseServerIp)) {
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg(
|
||
`Этот кейс доступен на: ${allowedIps
|
||
.map((ip) => translateServer(`Server ${ip}`))
|
||
.join(', ')}`,
|
||
);
|
||
setNotifSeverity('warning');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
return;
|
||
}
|
||
|
||
if (isOpening) return;
|
||
|
||
try {
|
||
setIsOpening(true);
|
||
|
||
const fullCase = await fetchCase(caseData.id);
|
||
const caseItems: CaseItem[] = fullCase.items || [];
|
||
setSelectedCase(fullCase);
|
||
|
||
// ✅ открываем на выбранном сервере (даже если игрок не на сервере)
|
||
const result = await openCase(fullCase.id, username, selectedCaseServerIp);
|
||
|
||
setRouletteCaseItems(caseItems);
|
||
setRouletteReward(result.reward);
|
||
setRouletteOpen(true);
|
||
playBuySound();
|
||
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg('Кейс открыт!');
|
||
setNotifSeverity('success');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
} catch (error) {
|
||
console.error('Ошибка при открытии кейса:', error);
|
||
|
||
if (!isNotificationsEnabled()) return;
|
||
setNotifMsg(String(error instanceof Error ? error.message : 'Ошибка при открытии кейса!'));
|
||
setNotifSeverity('error');
|
||
setNotifPos({ vertical: 'top', horizontal: 'center' });
|
||
setNotifOpen(true);
|
||
} finally {
|
||
setIsOpening(false);
|
||
}
|
||
};
|
||
|
||
const handleCloseNotification = () => {
|
||
setNotification((prev) => ({ ...prev, open: false }));
|
||
};
|
||
|
||
const handleCloseRoulette = () => {
|
||
setRouletteOpen(false);
|
||
};
|
||
|
||
useEffect(() => {
|
||
const onFirstUserGesture = () => primeSounds();
|
||
|
||
window.addEventListener('pointerdown', onFirstUserGesture, { once: true });
|
||
return () => window.removeEventListener('pointerdown', onFirstUserGesture);
|
||
}, []);
|
||
|
||
return (
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
gap: '2vw',
|
||
width: '100%',
|
||
height: '100%',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
overflow: 'hidden',
|
||
}}
|
||
>
|
||
{(loading || onlineCheckLoading) && (
|
||
<FullScreenLoader message="Загрузка магазина..." />
|
||
)}
|
||
|
||
{!loading && !onlineCheckLoading && (
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
width: '100%',
|
||
height: '100%',
|
||
gap: '2vw',
|
||
overflow: 'auto',
|
||
paddingBottom: '5vw',
|
||
paddingLeft: '2.5vw',
|
||
paddingRight: '1.5vw',
|
||
}}
|
||
>
|
||
{/* Блок прокачки */}
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: 2,
|
||
mt: '2vh'
|
||
}}
|
||
>
|
||
<Typography
|
||
variant="h6"
|
||
sx={{
|
||
fontFamily: 'Benzin-Bold',
|
||
backgroundImage:
|
||
'linear-gradient(136deg, rgb(242,113,33), rgb(233,64,87), rgb(138,35,135))',
|
||
WebkitBackgroundClip: 'text',
|
||
WebkitTextFillColor: 'transparent',
|
||
}}
|
||
>
|
||
Прокачка
|
||
</Typography>
|
||
|
||
{bonusesLoading ? (
|
||
<FullScreenLoader
|
||
fullScreen={false}
|
||
message="Загрузка прокачки..."
|
||
/>
|
||
) : bonusTypes.length > 0 ? (
|
||
<Grid container spacing={2} sx={{ mb: 4 }}>
|
||
{bonusTypes.map((bt) => {
|
||
const userBonus = userBonuses.find(
|
||
(ub) => ub.bonus_type_id === bt.id,
|
||
);
|
||
const owned = !!userBonus;
|
||
|
||
const level = owned ? userBonus!.level : 0;
|
||
const effectValue = owned
|
||
? userBonus!.effect_value
|
||
: bt.base_effect_value;
|
||
const nextEffectValue =
|
||
owned && userBonus!.can_upgrade
|
||
? bt.base_effect_value +
|
||
userBonus!.level * bt.effect_increment
|
||
: undefined;
|
||
|
||
const isActive = owned ? userBonus!.is_active : false;
|
||
const isPermanent = owned
|
||
? userBonus!.is_permanent
|
||
: bt.duration === 0;
|
||
|
||
const cardId = owned ? userBonus!.id : bt.id;
|
||
const processing = processingBonusIds.includes(cardId);
|
||
|
||
return (
|
||
<Grid item xs={12} sm={6} md={4} lg={3} key={bt.id}>
|
||
<BonusShopItem
|
||
id={cardId}
|
||
name={bt.name}
|
||
description={bt.description}
|
||
imageUrl={bt.image_url}
|
||
level={level}
|
||
effectValue={effectValue}
|
||
nextEffectValue={nextEffectValue}
|
||
price={bt.price}
|
||
upgradePrice={bt.upgrade_price}
|
||
canUpgrade={userBonus?.can_upgrade ?? false}
|
||
mode={owned ? 'upgrade' : 'buy'}
|
||
isActive={isActive}
|
||
isPermanent={isPermanent}
|
||
disabled={processing}
|
||
onBuy={
|
||
!owned ? () => handlePurchaseBonus(bt.id) : undefined
|
||
}
|
||
onUpgrade={
|
||
owned
|
||
? () => handleUpgradeBonus(userBonus!.id)
|
||
: undefined
|
||
}
|
||
onToggleActive={
|
||
owned
|
||
? () => handleToggleBonusActivation(userBonus!.id)
|
||
: undefined
|
||
}
|
||
/>
|
||
</Grid>
|
||
);
|
||
})}
|
||
</Grid>
|
||
) : (
|
||
<Typography>Прокачка временно недоступна.</Typography>
|
||
)}
|
||
</Box>
|
||
|
||
{/* Блок кейсов */}
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
alignItems: 'flex-start',
|
||
gap: 2,
|
||
mb: 1,
|
||
flexDirection: 'column',
|
||
}}
|
||
>
|
||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||
<Typography
|
||
variant="h6"
|
||
sx={{
|
||
fontFamily: 'Benzin-Bold',
|
||
backgroundImage:
|
||
'linear-gradient(136deg, rgb(242,113,33), rgb(233,64,87), rgb(138,35,135))',
|
||
WebkitBackgroundClip: 'text',
|
||
WebkitTextFillColor: 'transparent',
|
||
}}
|
||
>
|
||
Кейсы
|
||
</Typography>
|
||
|
||
{caseServers.length > 0 && (
|
||
<FormControl size="small" sx={{ minWidth: 220 }}>
|
||
<InputLabel id="cases-server-label" sx={{ fontFamily: 'Benzin-Bold', color: 'rgba(255,255,255,0.75)' }}>
|
||
Сервер
|
||
</InputLabel>
|
||
<Select
|
||
labelId="cases-server-label"
|
||
label="Сервер"
|
||
value={selectedCaseServerIp}
|
||
onChange={(e) => setSelectedCaseServerIp(String(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={{
|
||
borderRadius: '999px',
|
||
bgcolor: 'rgba(255,255,255,0.04)',
|
||
color: 'rgba(255,255,255,0.92)',
|
||
fontFamily: 'Benzin-Bold',
|
||
'& .MuiSelect-select': {
|
||
py: '0.9vw',
|
||
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)',
|
||
},
|
||
}}
|
||
>
|
||
{caseServers.map((ip) => (
|
||
<MenuItem key={ip} value={ip}>
|
||
{translateServer(`Server ${ip}`)}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
</FormControl>
|
||
)}
|
||
</Box>
|
||
|
||
{casesLoading ? (
|
||
<FullScreenLoader fullScreen={false} message="Загрузка кейсов..." />
|
||
) : cases.length > 0 ? (
|
||
<Grid container spacing={2} sx={{ mb: 4 }}>
|
||
{filteredCases.map((c) => (
|
||
<Grid item xs={12} sm={6} md={4} lg={3} key={c.id}>
|
||
<ShopItem
|
||
type="case"
|
||
id={c.id}
|
||
name={c.name}
|
||
description={c.description}
|
||
imageUrl={c.image_url}
|
||
price={c.price}
|
||
itemsCount={c.items_count}
|
||
isOpening={isOpening && selectedCase?.id === c.id}
|
||
disabled={isOpening || !selectedCaseServerIp}
|
||
onClick={() => handleOpenCase(c)}
|
||
/>
|
||
</Grid>
|
||
))}
|
||
</Grid>
|
||
) : (
|
||
<Typography>Кейсы временно недоступны.</Typography>
|
||
)}
|
||
</Box>
|
||
|
||
{/* Блок плащей (как был) */}
|
||
|
||
{/* Блок плащей */}
|
||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||
<Typography
|
||
variant="h6"
|
||
sx={{
|
||
fontFamily: 'Benzin-Bold',
|
||
backgroundImage:
|
||
'linear-gradient(136deg, rgb(242,113,33), rgb(233,64,87), rgb(138,35,135))',
|
||
WebkitBackgroundClip: 'text',
|
||
WebkitTextFillColor: 'transparent',
|
||
}}
|
||
>
|
||
Плащи
|
||
</Typography>
|
||
|
||
{availableCapes.length > 0 ? (
|
||
<Grid container spacing={2}>
|
||
{availableCapes.map((cape) => (
|
||
<Grid item xs={12} sm={6} md={4} lg={3} key={cape.id}>
|
||
<ShopItem
|
||
type="cape"
|
||
id={cape.id}
|
||
name={cape.name}
|
||
description={cape.description}
|
||
imageUrl={cape.image_url}
|
||
price={cape.price}
|
||
disabled={false}
|
||
playerSkinUrl={playerSkinUrl}
|
||
onClick={() => handlePurchaseCape(cape.id)}
|
||
/>
|
||
</Grid>
|
||
))}
|
||
</Grid>
|
||
) : (
|
||
<Typography>У вас уже есть все доступные плащи!</Typography>
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
|
||
{/* Компонент с анимацией рулетки */}
|
||
<CaseRoulette
|
||
open={rouletteOpen}
|
||
onClose={handleCloseRoulette}
|
||
caseName={selectedCase?.name}
|
||
items={rouletteCaseItems}
|
||
reward={rouletteReward}
|
||
/>
|
||
|
||
{/* Уведомления */}
|
||
<CustomNotification
|
||
open={notifOpen}
|
||
message={notifMsg}
|
||
severity={notifSeverity}
|
||
position={notifPos}
|
||
onClose={() => setNotifOpen(false)}
|
||
autoHideDuration={2500}
|
||
/>
|
||
</Box>
|
||
);
|
||
}
|