add commands tab in shop
This commit is contained in:
59
src/renderer/api/commands.ts
Normal file
59
src/renderer/api/commands.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { API_BASE_URL } from '../api';
|
||||||
|
|
||||||
|
export interface PrankCommand {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
price: number;
|
||||||
|
command_template: string;
|
||||||
|
server_ids: string[]; // ["*"] или конкретные id
|
||||||
|
targetDescription: string;
|
||||||
|
globalDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrankServer {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
ip: string;
|
||||||
|
online_players: number;
|
||||||
|
max_players: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchPrankCommands = async (): Promise<PrankCommand[]> => {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/pranks/commands`);
|
||||||
|
if (!res.ok) throw new Error('Failed to load prank commands');
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchPrankServers = async (): Promise<PrankServer[]> => {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/pranks/servers`);
|
||||||
|
if (!res.ok) throw new Error('Failed to load prank servers');
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const executePrank = async (
|
||||||
|
username: string,
|
||||||
|
commandId: string,
|
||||||
|
targetPlayer: string,
|
||||||
|
serverId: string,
|
||||||
|
) => {
|
||||||
|
const res = await fetch(
|
||||||
|
`${API_BASE_URL}/api/pranks/execute?username=${username}`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
command_id: commandId,
|
||||||
|
target_player: targetPlayer,
|
||||||
|
server_id: serverId,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.text();
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
@ -1,6 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
Box, Typography, Button, Grid,
|
Box,
|
||||||
FormControl, Select, MenuItem, InputLabel
|
Typography,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
InputLabel,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
TextField,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Cape,
|
Cape,
|
||||||
@ -32,9 +45,36 @@ import ShopItem from '../components/ShopItem';
|
|||||||
import { playBuySound, primeSounds } from '../utils/sounds';
|
import { playBuySound, primeSounds } from '../utils/sounds';
|
||||||
import CustomNotification from '../components/Notifications/CustomNotification';
|
import CustomNotification from '../components/Notifications/CustomNotification';
|
||||||
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
|
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
|
||||||
import { isNotificationsEnabled, getNotifPositionFromSettings } from '../utils/notifications';
|
import {
|
||||||
|
executePrank,
|
||||||
|
fetchPrankCommands,
|
||||||
|
fetchPrankServers,
|
||||||
|
PrankCommand,
|
||||||
|
PrankServer,
|
||||||
|
} from '../api/commands';
|
||||||
|
|
||||||
|
import {
|
||||||
|
isNotificationsEnabled,
|
||||||
|
getNotifPositionFromSettings,
|
||||||
|
} from '../utils/notifications';
|
||||||
import { translateServer } from '../utils/serverTranslator';
|
import { translateServer } from '../utils/serverTranslator';
|
||||||
|
|
||||||
|
interface TabPanelProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
index: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabPanel(props: TabPanelProps) {
|
||||||
|
const { children, value, index, ...other } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div role="tabpanel" hidden={value !== index} {...other}>
|
||||||
|
{value === index && <Box sx={{ pt: '1.4vw' }}>{children}</Box>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getRarityByWeight(
|
function getRarityByWeight(
|
||||||
weight?: number,
|
weight?: number,
|
||||||
): 'common' | 'rare' | 'epic' | 'legendary' {
|
): 'common' | 'rare' | 'epic' | 'legendary' {
|
||||||
@ -64,6 +104,15 @@ function getRarityColor(weight?: number): string {
|
|||||||
const GRADIENT =
|
const GRADIENT =
|
||||||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)';
|
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)';
|
||||||
|
|
||||||
|
const GLASS_DIALOG_SX = {
|
||||||
|
borderRadius: '1.4vw',
|
||||||
|
background:
|
||||||
|
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.16), transparent 55%), rgba(10,10,20,0.92)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.10)',
|
||||||
|
boxShadow: '0 1.6vw 4vw rgba(0,0,0,0.6)',
|
||||||
|
backdropFilter: 'blur(16px)',
|
||||||
|
};
|
||||||
|
|
||||||
export default function Shop() {
|
export default function Shop() {
|
||||||
const [storeCapes, setStoreCapes] = useState<StoreCape[]>([]);
|
const [storeCapes, setStoreCapes] = useState<StoreCape[]>([]);
|
||||||
const [userCapes, setUserCapes] = useState<Cape[]>([]);
|
const [userCapes, setUserCapes] = useState<Cape[]>([]);
|
||||||
@ -112,6 +161,33 @@ export default function Shop() {
|
|||||||
const [rouletteCaseItems, setRouletteCaseItems] = useState<CaseItem[]>([]);
|
const [rouletteCaseItems, setRouletteCaseItems] = useState<CaseItem[]>([]);
|
||||||
const [rouletteReward, setRouletteReward] = useState<CaseItem | null>(null);
|
const [rouletteReward, setRouletteReward] = useState<CaseItem | null>(null);
|
||||||
|
|
||||||
|
// TABS
|
||||||
|
const [tabValue, setTabValue] = useState<number>(0);
|
||||||
|
|
||||||
|
const handleTabChange = (_: React.SyntheticEvent, newValue: number) => {
|
||||||
|
setTabValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
//команды
|
||||||
|
|
||||||
|
const [prankCommands, setPrankCommands] = useState<PrankCommand[]>([]);
|
||||||
|
const [prankServers, setPrankServers] = useState<PrankServer[]>([]);
|
||||||
|
|
||||||
|
const [pranksLoading, setPranksLoading] = useState(false);
|
||||||
|
|
||||||
|
const [selectedPrankServer, setSelectedPrankServer] = useState<string>('');
|
||||||
|
const [targetPlayer, setTargetPlayer] = useState<string>('');
|
||||||
|
const [processingCommandId, setProcessingCommandId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [prankDialogOpen, setPrankDialogOpen] = useState(false);
|
||||||
|
const [selectedPrank, setSelectedPrank] = useState<PrankCommand | null>(null);
|
||||||
|
|
||||||
|
const [prankTarget, setPrankTarget] = useState('');
|
||||||
|
const [prankServerId, setPrankServerId] = useState<string>('');
|
||||||
|
const [prankProcessing, setPrankProcessing] = useState(false);
|
||||||
|
|
||||||
// Уведомления
|
// Уведомления
|
||||||
const [notification, setNotification] = useState<{
|
const [notification, setNotification] = useState<{
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -148,7 +224,7 @@ export default function Shop() {
|
|||||||
console.error('Ошибка при получении прокачек:', error);
|
console.error('Ошибка при получении прокачек:', error);
|
||||||
|
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification('Ошибка при загрузке прокачки!', 'error')
|
showNotification('Ошибка при загрузке прокачки!', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setBonusesLoading(false);
|
setBonusesLoading(false);
|
||||||
}
|
}
|
||||||
@ -175,6 +251,31 @@ export default function Shop() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Загрузка команд
|
||||||
|
useEffect(() => {
|
||||||
|
if (tabValue !== 3) return;
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
setPranksLoading(true);
|
||||||
|
const [commands, servers] = await Promise.all([
|
||||||
|
fetchPrankCommands(),
|
||||||
|
fetchPrankServers(),
|
||||||
|
]);
|
||||||
|
setPrankCommands(commands);
|
||||||
|
setPrankServers(servers);
|
||||||
|
if (servers.length) setSelectedPrankServer(servers[0].id);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showNotification('Ошибка загрузки пакостей', 'error');
|
||||||
|
} finally {
|
||||||
|
setPranksLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
|
}, [tabValue]);
|
||||||
|
|
||||||
// Функция для загрузки плащей пользователя
|
// Функция для загрузки плащей пользователя
|
||||||
const loadUserCapes = async (username: string) => {
|
const loadUserCapes = async (username: string) => {
|
||||||
try {
|
try {
|
||||||
@ -194,12 +295,12 @@ export default function Shop() {
|
|||||||
playBuySound();
|
playBuySound();
|
||||||
|
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification('Плащ успешно куплен!', 'success')
|
showNotification('Плащ успешно куплен!', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при покупке плаща:', error);
|
console.error('Ошибка при покупке плаща:', error);
|
||||||
|
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification('Ошибка при покупке плаща!', 'error')
|
showNotification('Ошибка при покупке плаща!', 'error');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -279,9 +380,11 @@ export default function Shop() {
|
|||||||
|
|
||||||
const handlePurchaseBonus = async (bonusTypeId: string) => {
|
const handlePurchaseBonus = async (bonusTypeId: string) => {
|
||||||
if (!username) {
|
if (!username) {
|
||||||
|
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification('Не найдено имя игрока. Авторизируйтесь в лаунчере!', 'error')
|
showNotification(
|
||||||
|
'Не найдено имя игрока. Авторизируйтесь в лаунчере!',
|
||||||
|
'error',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,12 +397,12 @@ export default function Shop() {
|
|||||||
await loadBonuses(username);
|
await loadBonuses(username);
|
||||||
|
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification('Прокачка успешно куплена!', 'success')
|
showNotification('Прокачка успешно куплена!', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при покупке прокачки:', error);
|
console.error('Ошибка при покупке прокачки:', error);
|
||||||
|
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification('Ошибка при прокачке!', 'error')
|
showNotification('Ошибка при прокачке!', 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -314,12 +417,12 @@ export default function Shop() {
|
|||||||
await loadBonuses(username);
|
await loadBonuses(username);
|
||||||
|
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification('Бонус улучшен!', 'success')
|
showNotification('Бонус улучшен!', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при улучшении бонуса:', error);
|
console.error('Ошибка при улучшении бонуса:', error);
|
||||||
|
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification('Ошибка при улучшении бонуса!', 'error')
|
showNotification('Ошибка при улучшении бонуса!', 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -334,17 +437,13 @@ export default function Shop() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при переключении бонуса:', error);
|
console.error('Ошибка при переключении бонуса:', error);
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification('Ошибка при переключении бонуса!', 'error')
|
showNotification('Ошибка при переключении бонуса!', 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const caseServers = Array.from(
|
const caseServers = Array.from(
|
||||||
new Set(
|
new Set((cases || []).flatMap((c) => c.server_ips || []).filter(Boolean)),
|
||||||
(cases || [])
|
|
||||||
.flatMap((c) => c.server_ips || [])
|
|
||||||
.filter(Boolean),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -377,22 +476,28 @@ const filteredCases = (cases || []).filter((c) => {
|
|||||||
const handleOpenCase = async (caseData: Case) => {
|
const handleOpenCase = async (caseData: Case) => {
|
||||||
if (!username) {
|
if (!username) {
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification('Не найдено имя игрока. Авторизуйтесь в лаунчере!', 'error')
|
showNotification(
|
||||||
|
'Не найдено имя игрока. Авторизуйтесь в лаунчере!',
|
||||||
|
'error',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedCaseServerIp) {
|
if (!selectedCaseServerIp) {
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification('Выберите сервер для открытия кейса!', 'warning')
|
showNotification('Выберите сервер для открытия кейса!', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedIps = caseData.server_ips || [];
|
const allowedIps = caseData.server_ips || [];
|
||||||
if (allowedIps.length > 0 && !allowedIps.includes(selectedCaseServerIp)) {
|
if (allowedIps.length > 0 && !allowedIps.includes(selectedCaseServerIp)) {
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification(`Этот кейс доступен на: ${allowedIps
|
showNotification(
|
||||||
|
`Этот кейс доступен на: ${allowedIps
|
||||||
.map((ip) => translateServer(`Server ${ip}`))
|
.map((ip) => translateServer(`Server ${ip}`))
|
||||||
.join(', ')}`, 'warning')
|
.join(', ')}`,
|
||||||
|
'warning',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,7 +511,11 @@ const filteredCases = (cases || []).filter((c) => {
|
|||||||
setSelectedCase(fullCase);
|
setSelectedCase(fullCase);
|
||||||
|
|
||||||
// ✅ открываем на выбранном сервере (даже если игрок не на сервере)
|
// ✅ открываем на выбранном сервере (даже если игрок не на сервере)
|
||||||
const result = await openCase(fullCase.id, username, selectedCaseServerIp);
|
const result = await openCase(
|
||||||
|
fullCase.id,
|
||||||
|
username,
|
||||||
|
selectedCaseServerIp,
|
||||||
|
);
|
||||||
|
|
||||||
setRouletteCaseItems(caseItems);
|
setRouletteCaseItems(caseItems);
|
||||||
setRouletteReward(result.reward);
|
setRouletteReward(result.reward);
|
||||||
@ -414,12 +523,17 @@ const filteredCases = (cases || []).filter((c) => {
|
|||||||
playBuySound();
|
playBuySound();
|
||||||
|
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification('Кейс открыт!', 'success')
|
showNotification('Кейс открыт!', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при открытии кейса:', error);
|
console.error('Ошибка при открытии кейса:', error);
|
||||||
|
|
||||||
if (!isNotificationsEnabled()) return;
|
if (!isNotificationsEnabled()) return;
|
||||||
showNotification((String(error instanceof Error ? error.message : 'Ошибка при открытии кейса!')), 'error')
|
showNotification(
|
||||||
|
String(
|
||||||
|
error instanceof Error ? error.message : 'Ошибка при открытии кейса!',
|
||||||
|
),
|
||||||
|
'error',
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsOpening(false);
|
setIsOpening(false);
|
||||||
}
|
}
|
||||||
@ -470,13 +584,53 @@ const filteredCases = (cases || []).filter((c) => {
|
|||||||
paddingRight: '1.5vw',
|
paddingRight: '1.5vw',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Tabs
|
||||||
|
value={tabValue}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
disableRipple
|
||||||
|
sx={{
|
||||||
|
minHeight: '3vw',
|
||||||
|
mb: '1.2vw',
|
||||||
|
'& .MuiTabs-indicator': {
|
||||||
|
height: '0.35vw',
|
||||||
|
borderRadius: '999px',
|
||||||
|
background: GRADIENT,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{['Прокачка', 'Кейсы', 'Плащи', 'Предметы/пакости'].map((label) => (
|
||||||
|
<Tab
|
||||||
|
key={label}
|
||||||
|
label={label}
|
||||||
|
disableRipple
|
||||||
|
sx={{
|
||||||
|
minHeight: '3vw',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
color: 'rgba(255,255,255,0.85)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 0.4,
|
||||||
|
borderRadius: '999px',
|
||||||
|
mx: '0.2vw',
|
||||||
|
'&.Mui-selected': {
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
color: '#fff',
|
||||||
|
background: 'rgba(255,255,255,0.06)',
|
||||||
|
},
|
||||||
|
transition: 'all 0.18s ease',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
<TabPanel value={tabValue} index={0}>
|
||||||
{/* Блок прокачки */}
|
{/* Блок прокачки */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
mt: '2vh'
|
mt: '2vh',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
@ -541,7 +695,9 @@ const filteredCases = (cases || []).filter((c) => {
|
|||||||
isPermanent={isPermanent}
|
isPermanent={isPermanent}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
onBuy={
|
onBuy={
|
||||||
!owned ? () => handlePurchaseBonus(bt.id) : undefined
|
!owned
|
||||||
|
? () => handlePurchaseBonus(bt.id)
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
onUpgrade={
|
onUpgrade={
|
||||||
owned
|
owned
|
||||||
@ -562,7 +718,9 @@ const filteredCases = (cases || []).filter((c) => {
|
|||||||
<Typography>Прокачка временно недоступна.</Typography>
|
<Typography>Прокачка временно недоступна.</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={tabValue} index={1}>
|
||||||
{/* Блок кейсов */}
|
{/* Блок кейсов */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -664,7 +822,9 @@ const filteredCases = (cases || []).filter((c) => {
|
|||||||
labelId="cases-server-label"
|
labelId="cases-server-label"
|
||||||
label="Сервер"
|
label="Сервер"
|
||||||
value={selectedCaseServerIp}
|
value={selectedCaseServerIp}
|
||||||
onChange={(e) => setSelectedCaseServerIp(String(e.target.value))}
|
onChange={(e) =>
|
||||||
|
setSelectedCaseServerIp(String(e.target.value))
|
||||||
|
}
|
||||||
MenuProps={{
|
MenuProps={{
|
||||||
PaperProps: {
|
PaperProps: {
|
||||||
sx: {
|
sx: {
|
||||||
@ -761,7 +921,10 @@ const filteredCases = (cases || []).filter((c) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{casesLoading ? (
|
{casesLoading ? (
|
||||||
<FullScreenLoader fullScreen={false} message="Загрузка кейсов..." />
|
<FullScreenLoader
|
||||||
|
fullScreen={false}
|
||||||
|
message="Загрузка кейсов..."
|
||||||
|
/>
|
||||||
) : cases.length > 0 ? (
|
) : cases.length > 0 ? (
|
||||||
<Grid container spacing={2} sx={{ mb: 4 }}>
|
<Grid container spacing={2} sx={{ mb: 4 }}>
|
||||||
{filteredCases.map((c) => (
|
{filteredCases.map((c) => (
|
||||||
@ -785,9 +948,9 @@ const filteredCases = (cases || []).filter((c) => {
|
|||||||
<Typography>Кейсы временно недоступны.</Typography>
|
<Typography>Кейсы временно недоступны.</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
{/* Блок плащей (как был) */}
|
<TabPanel value={tabValue} index={2}>
|
||||||
|
|
||||||
{/* Блок плащей */}
|
{/* Блок плащей */}
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
<Typography
|
<Typography
|
||||||
@ -825,9 +988,171 @@ const filteredCases = (cases || []).filter((c) => {
|
|||||||
<Typography>У вас уже есть все доступные плащи!</Typography>
|
<Typography>У вас уже есть все доступные плащи!</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={tabValue} index={3}>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{prankCommands.map((cmd) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} lg={3} key={cmd.id}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: '1.4vw',
|
||||||
|
borderRadius: '1.4vw',
|
||||||
|
background:
|
||||||
|
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.14), transparent 55%), rgba(10,10,20,0.88)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.10)',
|
||||||
|
boxShadow: '0 1.2vw 3.2vw rgba(0,0,0,0.55)',
|
||||||
|
backdropFilter: 'blur(14px)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.8vw',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
fontSize: '1.1vw',
|
||||||
|
background: GRADIENT,
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cmd.name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
sx={{ color: 'rgba(255,255,255,0.75)', fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
{cmd.description}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography sx={{ fontWeight: 900 }}>
|
||||||
|
Цена: {cmd.price} монет
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disableRipple
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedPrank(cmd);
|
||||||
|
setPrankTarget('');
|
||||||
|
setPrankDialogOpen(true);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
mt: '0.6vw',
|
||||||
|
borderRadius: '999px',
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
color: '#fff',
|
||||||
|
background: GRADIENT,
|
||||||
|
py: '0.6vw',
|
||||||
|
'&:hover': { filter: 'brightness(1.05)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Выполнить
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</TabPanel>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={prankDialogOpen}
|
||||||
|
onClose={() => setPrankDialogOpen(false)}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="xs"
|
||||||
|
PaperProps={{ sx: GLASS_DIALOG_SX }}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ fontFamily: 'Benzin-Bold' }}>
|
||||||
|
{selectedPrank?.name}
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent dividers sx={{ borderColor: 'rgba(255,255,255,0.10)' }}>
|
||||||
|
<Typography sx={{ opacity: 0.75, mb: 2 }}>
|
||||||
|
{selectedPrank?.description}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* сервер */}
|
||||||
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
|
<Select
|
||||||
|
value={prankServerId}
|
||||||
|
onChange={(e) => setPrankServerId(String(e.target.value))}
|
||||||
|
displayEmpty
|
||||||
|
>
|
||||||
|
<MenuItem disabled value="">
|
||||||
|
Выберите сервер
|
||||||
|
</MenuItem>
|
||||||
|
{prankServers.map((s) => (
|
||||||
|
<MenuItem key={s.id} value={s.id}>
|
||||||
|
{translateServer(s.name)} ({s.online_players}/{s.max_players})
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{/* цель */}
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Ник игрока"
|
||||||
|
value={prankTarget}
|
||||||
|
onChange={(e) => setPrankTarget(e.target.value)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions sx={{ p: '1.2vw' }}>
|
||||||
|
<Button
|
||||||
|
onClick={() => setPrankDialogOpen(false)}
|
||||||
|
sx={{ color: 'rgba(255,255,255,0.75)', fontFamily: 'Benzin-Bold' }}
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={!prankTarget || !prankServerId || prankProcessing}
|
||||||
|
onClick={async () => {
|
||||||
|
if (!selectedPrank) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setPrankProcessing(true);
|
||||||
|
await executePrank(
|
||||||
|
username,
|
||||||
|
selectedPrank.id,
|
||||||
|
prankTarget,
|
||||||
|
prankServerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
playBuySound();
|
||||||
|
showNotification(
|
||||||
|
selectedPrank.globalDescription
|
||||||
|
.replace('{username}', username)
|
||||||
|
.replace('{targetPlayer}', prankTarget),
|
||||||
|
'success',
|
||||||
|
);
|
||||||
|
|
||||||
|
setPrankDialogOpen(false);
|
||||||
|
} catch (e) {
|
||||||
|
showNotification(
|
||||||
|
e instanceof Error ? e.message : 'Ошибка выполнения',
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setPrankProcessing(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
color: '#fff',
|
||||||
|
background: GRADIENT,
|
||||||
|
borderRadius: '999px',
|
||||||
|
px: '1.6vw',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Подтвердить
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Компонент с анимацией рулетки */}
|
{/* Компонент с анимацией рулетки */}
|
||||||
<CaseRoulette
|
<CaseRoulette
|
||||||
open={rouletteOpen}
|
open={rouletteOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user