add cases to Shop (roulette don't work :( )
This commit is contained in:
@ -207,6 +207,94 @@ export interface MeResponse {
|
||||
is_admin: boolean;
|
||||
}
|
||||
|
||||
export interface CaseItemMeta {
|
||||
display_name?: string | null;
|
||||
lore?: string[] | null;
|
||||
}
|
||||
|
||||
export interface CaseItem {
|
||||
id: string;
|
||||
name?: string;
|
||||
material: string;
|
||||
amount: number;
|
||||
weight?: number;
|
||||
meta?: CaseItemMeta;
|
||||
}
|
||||
|
||||
export interface Case {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
image_url?: string;
|
||||
server_ids?: string[];
|
||||
items_count?: number;
|
||||
items?: CaseItem[];
|
||||
}
|
||||
|
||||
export interface OpenCaseResponse {
|
||||
status: string;
|
||||
message: string;
|
||||
operation_id: string;
|
||||
balance: number;
|
||||
reward: CaseItem;
|
||||
}
|
||||
|
||||
// ===== КЕЙСЫ =====
|
||||
|
||||
export async function fetchCases(): Promise<Case[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/cases`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Не удалось получить список кейсов');
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Если у тебя есть отдельный эндпоинт деталей кейса, можно использовать это:
|
||||
export async function fetchCase(case_id: string): Promise<Case> {
|
||||
const response = await fetch(`${API_BASE_URL}/cases/${case_id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Не удалось получить информацию о кейсе');
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function openCase(
|
||||
case_id: string,
|
||||
username: string,
|
||||
server_id: string,
|
||||
): Promise<OpenCaseResponse> {
|
||||
// Формируем URL с query-параметрами, как любит текущий бэкенд
|
||||
const url = new URL(`${API_BASE_URL}/cases/${case_id}/open`);
|
||||
url.searchParams.append('username', username);
|
||||
url.searchParams.append('server_id', server_id);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let msg = 'Не удалось открыть кейс';
|
||||
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
if (errorData.message) {
|
||||
msg = errorData.message;
|
||||
} else if (Array.isArray(errorData.detail)) {
|
||||
msg = errorData.detail.map((d: any) => d.msg).join(', ');
|
||||
} else if (typeof errorData.detail === 'string') {
|
||||
msg = errorData.detail;
|
||||
}
|
||||
} catch {
|
||||
// если бэкенд вернул не-JSON, оставляем дефолтное сообщение
|
||||
}
|
||||
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function fetchMe(): Promise<MeResponse> {
|
||||
const { accessToken, clientToken } = getAuthTokens();
|
||||
|
||||
|
||||
254
src/renderer/components/CaseRoulette.tsx
Normal file
254
src/renderer/components/CaseRoulette.tsx
Normal file
@ -0,0 +1,254 @@
|
||||
import { Box, Typography, Button, Dialog, DialogContent } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CaseItem } from '../api';
|
||||
|
||||
type Rarity = 'common' | 'rare' | 'epic' | 'legendary';
|
||||
|
||||
interface CaseRouletteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
caseName?: string;
|
||||
items: CaseItem[];
|
||||
reward: CaseItem | null; // что реально выпало с бэка
|
||||
}
|
||||
|
||||
// --- настройки рулетки ---
|
||||
const ITEM_WIDTH = 110;
|
||||
const ITEM_GAP = 8;
|
||||
const VISIBLE_ITEMS = 21;
|
||||
const CENTER_INDEX = Math.floor(VISIBLE_ITEMS / 2);
|
||||
|
||||
// ширина видимой области и позиция линии
|
||||
const CONTAINER_WIDTH = 800;
|
||||
const LINE_X = CONTAINER_WIDTH / 2;
|
||||
|
||||
// редкость по weight (только фронт)
|
||||
function getRarityByWeight(weight?: number): Rarity {
|
||||
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 CaseRoulette({
|
||||
open,
|
||||
onClose,
|
||||
caseName,
|
||||
items,
|
||||
reward,
|
||||
}: CaseRouletteProps) {
|
||||
const [sequence, setSequence] = useState<CaseItem[]>([]);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [animating, setAnimating] = useState(false);
|
||||
|
||||
const winningName =
|
||||
reward?.meta?.display_name || reward?.name || reward?.material || '';
|
||||
|
||||
// генерируем полосу предметов и запускаем анимацию,
|
||||
// когда диалог открыт и есть reward
|
||||
useEffect(() => {
|
||||
if (!open || !reward || !items || items.length === 0) return;
|
||||
|
||||
// 1. генерим последовательность
|
||||
const seq: CaseItem[] = [];
|
||||
for (let i = 0; i < VISIBLE_ITEMS; i++) {
|
||||
const randomItem = items[Math.floor(Math.random() * items.length)];
|
||||
seq.push(randomItem);
|
||||
}
|
||||
|
||||
// 2. подменяем центр на тот предмет, который реально выпал
|
||||
const fromCase =
|
||||
items.find((i) => i.material === reward.material) || reward;
|
||||
seq[CENTER_INDEX] = fromCase;
|
||||
|
||||
setSequence(seq);
|
||||
|
||||
// 3. считаем финальный offset, при котором CENTER_INDEX оказывается под линией
|
||||
const centerItemCenter =
|
||||
CENTER_INDEX * (ITEM_WIDTH + ITEM_GAP) + ITEM_WIDTH / 2;
|
||||
const finalOffset = Math.max(0, centerItemCenter - LINE_X);
|
||||
|
||||
// стартуем анимацию
|
||||
setAnimating(false);
|
||||
setOffset(0);
|
||||
|
||||
// маленькая задержка, чтобы браузер применил начальное состояние
|
||||
const id = setTimeout(() => {
|
||||
setAnimating(true);
|
||||
setOffset(finalOffset);
|
||||
}, 50);
|
||||
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}, [open, reward, items]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: 'rgba(15, 15, 20, 0.95)',
|
||||
borderRadius: '1vw',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography
|
||||
variant="h6"
|
||||
color="white"
|
||||
sx={{ textAlign: 'center', mb: 2 }}
|
||||
>
|
||||
Открытие кейса {caseName}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderRadius: '1vw',
|
||||
border: '2px solid rgba(255, 255, 255, 0.1)',
|
||||
px: 2,
|
||||
py: 3,
|
||||
width: `${CONTAINER_WIDTH}px`,
|
||||
maxWidth: '100%',
|
||||
mx: 'auto',
|
||||
}}
|
||||
>
|
||||
{/* Линия центра */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: `${LINE_X}px`,
|
||||
transform: 'translateX(-1px)',
|
||||
width: '2px',
|
||||
bgcolor: 'rgba(255, 77, 77, 0.8)',
|
||||
zIndex: 2,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Лента с предметами */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: `${ITEM_GAP}px`,
|
||||
transform: `translateX(-${offset}px)`,
|
||||
transition: animating
|
||||
? 'transform 3s cubic-bezier(0.1, 0.8, 0.2, 1)'
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
{sequence.map((item, index) => {
|
||||
const color = getRarityColor(item.weight);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
width: `120px`,
|
||||
height: '120px',
|
||||
borderRadius: '0.7vw',
|
||||
bgcolor: 'rgba(255, 255, 255, 0.04)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border:
|
||||
index === CENTER_INDEX
|
||||
? `2px solid ${color}`
|
||||
: `1px solid ${color}`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={`https://cdn.minecraft.popa-popa.ru/textures/${item.material.toLowerCase()}.png`}
|
||||
alt={item.material}
|
||||
sx={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
objectFit: 'contain',
|
||||
imageRendering: 'pixelated',
|
||||
mb: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: 'white',
|
||||
textAlign: 'center',
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
{item.meta?.display_name ||
|
||||
item.name ||
|
||||
item.material
|
||||
.replace(/_/g, ' ')
|
||||
.toLowerCase()
|
||||
.replace(/\b\w/g, (l) => l.toUpperCase())}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{winningName && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="white"
|
||||
sx={{ textAlign: 'center', mt: 2 }}
|
||||
>
|
||||
Вам выпало: <b>{winningName}</b>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
mt: 3,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={onClose}
|
||||
sx={{
|
||||
borderRadius: '20px',
|
||||
p: '0.5vw 2.5vw',
|
||||
color: 'white',
|
||||
backgroundColor: 'rgb(255, 77, 77)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 77, 77, 0.5)',
|
||||
},
|
||||
fontFamily: 'Benzin-Bold',
|
||||
}}
|
||||
>
|
||||
Закрыть
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,16 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { Typography } from '@mui/material';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Grid,
|
||||
Card,
|
||||
CardMedia,
|
||||
CardContent,
|
||||
Snackbar,
|
||||
Alert,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
} from '@mui/material';
|
||||
import CapeCard from '../components/CapeCard';
|
||||
import {
|
||||
Cape,
|
||||
@ -7,9 +18,43 @@ import {
|
||||
fetchCapesStore,
|
||||
purchaseCape,
|
||||
StoreCape,
|
||||
Case,
|
||||
CaseItem,
|
||||
fetchCases,
|
||||
fetchCase,
|
||||
openCase,
|
||||
Server,
|
||||
} from '../api';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FullScreenLoader } from '../components/FullScreenLoader';
|
||||
import { getPlayerServer } from '../utils/playerOnlineCheck';
|
||||
import CaseRoulette from '../components/CaseRoulette';
|
||||
|
||||
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[]>([]);
|
||||
@ -18,6 +63,39 @@ export default function Shop() {
|
||||
const [uuid, setUuid] = useState<string>('');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
// Кейсы
|
||||
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 ITEM_WIDTH = 110; // ширина "карточки" предмета в рулетке
|
||||
const ITEM_GAP = 8;
|
||||
const VISIBLE_ITEMS = 21; // сколько элементов в линии
|
||||
const CENTER_INDEX = Math.floor(VISIBLE_ITEMS / 2);
|
||||
|
||||
// Функция для загрузки плащей из магазина
|
||||
const loadStoreCapes = async () => {
|
||||
try {
|
||||
@ -44,12 +122,55 @@ export default function Shop() {
|
||||
try {
|
||||
await purchaseCape(username, cape_id);
|
||||
await loadUserCapes(username);
|
||||
setNotification({
|
||||
open: true,
|
||||
message: 'Плащ успешно куплен!',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка при покупке плаща:', error);
|
||||
setNotification({
|
||||
open: true,
|
||||
message:
|
||||
error instanceof Error ? error.message : 'Ошибка при покупке плаща',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Загружаем данные при монтировании
|
||||
// Загрузка кейсов
|
||||
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) {
|
||||
@ -60,22 +181,118 @@ export default function Shop() {
|
||||
|
||||
setLoading(true);
|
||||
|
||||
// Загружаем оба списка плащей
|
||||
Promise.all([loadStoreCapes(), loadUserCapes(config.username)]).finally(
|
||||
() => {
|
||||
Promise.all([
|
||||
loadStoreCapes(),
|
||||
loadUserCapes(config.username),
|
||||
loadCases(),
|
||||
])
|
||||
.catch((err) => console.error(err))
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Проверяем онлайн после того, как знаем username
|
||||
useEffect(() => {
|
||||
if (username) {
|
||||
checkPlayerStatus();
|
||||
}
|
||||
}, [username]);
|
||||
|
||||
// Фильтруем плащи, которые уже куплены пользователем
|
||||
const availableCapes = storeCapes.filter(
|
||||
(storeCape) =>
|
||||
!userCapes.some((userCape) => userCape.cape_id === storeCape.id),
|
||||
);
|
||||
|
||||
// Генерация массива предметов для рулетки
|
||||
const generateRouletteItems = (
|
||||
allItems: Case['items'],
|
||||
winningItemMaterial: string,
|
||||
): Case['items'] => {
|
||||
if (!allItems || allItems.length === 0) return [];
|
||||
|
||||
const result: Case['items'] = [];
|
||||
for (let i = 0; i < VISIBLE_ITEMS; i++) {
|
||||
const randomItem = allItems[Math.floor(Math.random() * allItems.length)];
|
||||
result.push(randomItem);
|
||||
}
|
||||
|
||||
// Принудительно ставим выигрышный предмет в центр
|
||||
const winningSource =
|
||||
allItems.find((i) => i.material === winningItemMaterial) || allItems[0];
|
||||
result[CENTER_INDEX] = winningSource;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const handleOpenCase = async (caseData: Case) => {
|
||||
if (!username) {
|
||||
setNotification({
|
||||
open: true,
|
||||
message: 'Не найдено имя игрока. Авторизуйтесь в лаунчере.',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOnline || !playerServer) {
|
||||
setNotification({
|
||||
open: true,
|
||||
message: 'Для открытия кейсов необходимо находиться на сервере в игре.',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isOpening) return;
|
||||
|
||||
try {
|
||||
setIsOpening(true);
|
||||
|
||||
// 1. получаем полный кейс
|
||||
const fullCase = await fetchCase(caseData.id);
|
||||
const caseItems: CaseItem[] = fullCase.items || [];
|
||||
setSelectedCase(fullCase);
|
||||
|
||||
// 2. открываем кейс на бэке
|
||||
const result = await openCase(fullCase.id, username, playerServer.id);
|
||||
|
||||
// 3. сохраняем данные для рулетки
|
||||
setRouletteCaseItems(caseItems);
|
||||
setRouletteReward(result.reward);
|
||||
setRouletteOpen(true);
|
||||
|
||||
// 4. уведомление
|
||||
setNotification({
|
||||
open: true,
|
||||
message: result.message || 'Кейс открыт!',
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
setIsOpening(false);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при открытии кейса:', error);
|
||||
setNotification({
|
||||
open: true,
|
||||
message:
|
||||
error instanceof Error ? error.message : 'Ошибка при открытии кейса',
|
||||
type: 'error',
|
||||
});
|
||||
setIsOpening(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseNotification = () => {
|
||||
setNotification((prev) => ({ ...prev, open: false }));
|
||||
};
|
||||
|
||||
const handleCloseRoulette = () => {
|
||||
setRouletteOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -88,9 +305,11 @@ export default function Shop() {
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<FullScreenLoader message="Загрузка..." />
|
||||
) : (
|
||||
{(loading || onlineCheckLoading) && (
|
||||
<FullScreenLoader message="Загрузка магазина..." />
|
||||
)}
|
||||
|
||||
{!loading && !onlineCheckLoading && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@ -102,6 +321,106 @@ export default function Shop() {
|
||||
gap: '2vw',
|
||||
}}
|
||||
>
|
||||
{/* Блок кейсов */}
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
Кейсы
|
||||
</Typography>
|
||||
|
||||
{!isOnline && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="error"
|
||||
sx={{ mb: 2, maxWidth: '600px' }}
|
||||
>
|
||||
Для открытия кейсов вам необходимо находиться на одном из серверов
|
||||
игры. Зайдите в игру и обновите страницу.
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{casesLoading ? (
|
||||
<FullScreenLoader fullScreen={false} message="Загрузка кейсов..." />
|
||||
) : cases.length > 0 ? (
|
||||
<Grid container spacing={2} sx={{ mb: 4 }}>
|
||||
{cases.map((c) => (
|
||||
<Grid item xs={12} sm={6} md={4} lg={3} key={c.id}>
|
||||
<Card
|
||||
sx={{
|
||||
bgcolor: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: '1vw',
|
||||
}}
|
||||
>
|
||||
{c.image_url && (
|
||||
<CardMedia
|
||||
component="img"
|
||||
image={c.image_url}
|
||||
alt={c.name}
|
||||
sx={{
|
||||
minWidth: '10vw',
|
||||
minHeight: '10vw',
|
||||
maxHeight: '10vw',
|
||||
objectFit: 'cover',
|
||||
p: '0.5vw',
|
||||
borderRadius: '1vw 1vw 0 0',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<CardContent>
|
||||
<Typography variant="h6" color="white">
|
||||
{c.name}
|
||||
</Typography>
|
||||
{c.description && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="white"
|
||||
sx={{ opacity: 0.7 }}
|
||||
>
|
||||
{c.description}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="body2" color="white" sx={{ mt: 1 }}>
|
||||
Цена: {c.price} монет
|
||||
</Typography>
|
||||
{typeof c.items_count === 'number' && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="white"
|
||||
sx={{ opacity: 0.7 }}
|
||||
>
|
||||
Предметов в кейсе: {c.items_count}
|
||||
</Typography>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{
|
||||
mt: '1vw',
|
||||
borderRadius: '20px',
|
||||
p: '0.3vw 0vw',
|
||||
color: 'white',
|
||||
backgroundColor: 'rgb(255, 77, 77)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 77, 77, 0.5)',
|
||||
},
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '1vw',
|
||||
}}
|
||||
disabled={!isOnline || isOpening}
|
||||
onClick={() => handleOpenCase(c)}
|
||||
>
|
||||
{isOpening && selectedCase?.id === c.id
|
||||
? 'Открываем...'
|
||||
: 'Открыть кейс'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography>Кейсы временно недоступны.</Typography>
|
||||
)}
|
||||
|
||||
{/* Блок плащей (как был) */}
|
||||
<Typography variant="h6">Доступные плащи</Typography>
|
||||
{availableCapes.length > 0 ? (
|
||||
<Box
|
||||
@ -126,6 +445,30 @@ export default function Shop() {
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Компонент с анимацией рулетки */}
|
||||
<CaseRoulette
|
||||
open={rouletteOpen}
|
||||
onClose={handleCloseRoulette}
|
||||
caseName={selectedCase?.name}
|
||||
items={rouletteCaseItems}
|
||||
reward={rouletteReward}
|
||||
/>
|
||||
|
||||
{/* Уведомления */}
|
||||
<Snackbar
|
||||
open={notification.open}
|
||||
autoHideDuration={6000}
|
||||
onClose={handleCloseNotification}
|
||||
>
|
||||
<Alert
|
||||
onClose={handleCloseNotification}
|
||||
severity={notification.type}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{notification.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user