diff --git a/src/renderer/api.ts b/src/renderer/api.ts index 9ea5c24..6e8de04 100644 --- a/src/renderer/api.ts +++ b/src/renderer/api.ts @@ -207,6 +207,8 @@ export interface MeResponse { is_admin: boolean; } +// ===== КЕЙСЫ ===== + export interface CaseItemMeta { display_name?: string | null; lore?: string[] | null; diff --git a/src/renderer/components/CapePreview.tsx b/src/renderer/components/CapePreview.tsx new file mode 100644 index 0000000..8745bfc --- /dev/null +++ b/src/renderer/components/CapePreview.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Box } from '@mui/material'; + +interface CapePreviewProps { + imageUrl: string; + alt?: string; +} + +export const CapePreview: React.FC = ({ + imageUrl, + alt = 'Плащ', +}) => { + return ( + + + + ); +}; diff --git a/src/renderer/components/PlayerPreviewModal.tsx b/src/renderer/components/PlayerPreviewModal.tsx new file mode 100644 index 0000000..2c326b7 --- /dev/null +++ b/src/renderer/components/PlayerPreviewModal.tsx @@ -0,0 +1,79 @@ +// src/renderer/components/CapePreviewModal.tsx +import React from 'react'; +import { + Dialog, + DialogContent, + IconButton, + Box, + Typography, +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import SkinViewer from './SkinViewer'; + +interface CapePreviewModalProps { + open: boolean; + onClose: () => void; + capeUrl: string; + skinUrl?: string; +} + +const CapePreviewModal: React.FC = ({ + open, + onClose, + capeUrl, + skinUrl, +}) => { + return ( + + + + + + + + + Предпросмотр плаща + + + + + + + ); +}; + +export default CapePreviewModal; diff --git a/src/renderer/components/ShopItem.tsx b/src/renderer/components/ShopItem.tsx new file mode 100644 index 0000000..5fff5a6 --- /dev/null +++ b/src/renderer/components/ShopItem.tsx @@ -0,0 +1,296 @@ +// src/renderer/components/ShopItem.tsx +import React, { useState } from 'react'; +import { + Card, + CardMedia, + CardContent, + Box, + Typography, + Button, + IconButton, +} from '@mui/material'; +import CoinsDisplay from './CoinsDisplay'; +import { CapePreview } from './CapePreview'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import CapePreviewModal from './PlayerPreviewModal'; + +export type ShopItemType = 'case' | 'cape'; + +export interface ShopItemProps { + type: ShopItemType; + + id: string; + name: string; + description?: string; + imageUrl?: string; + price?: number; + + // только для кейсов + itemsCount?: number; + isOpening?: boolean; + + // для препросмотра плаща + playerSkinUrl?: string; + + // для обоих + disabled?: boolean; + onClick: () => void; +} + +export default function ShopItem({ + type, + name, + description, + imageUrl, + price, + itemsCount, + isOpening, + disabled, + playerSkinUrl, + onClick, +}: ShopItemProps) { + const badgeLabel = type === 'case' ? 'Кейс' : 'Плащ'; + + const buttonText = + type === 'case' ? (isOpening ? 'Открываем...' : 'Открыть кейс') : 'Купить'; + + const [previewOpen, setPreviewOpen] = useState(false); + + return ( + + {/* верхний “свет” */} + + + {imageUrl && ( + + {type === 'case' ? ( + /* как было для кейсов */ + + + + ) : ( + // ✅ здесь используем CapePreview + + )} + + {/* кнопка предпросмотра плаща */} + {type === 'cape' && ( + + { + e.stopPropagation(); + setPreviewOpen(true); + }} + sx={{ + p: 0.3, + color: 'white', + bgcolor: 'rgba(0,0,0,0.4)', + '&:hover': { + bgcolor: 'rgba(0,0,0,0.7)', + }, + }} + > + + + + )} + + )} + + + + {name} + + + {description && ( + + {description} + + )} + + {typeof price === 'number' && ( + + + Цена + + + + )} + + {type === 'case' && typeof itemsCount === 'number' && ( + + Предметов в кейсе: {itemsCount} + + )} + + + + + {type === 'cape' && imageUrl && ( + setPreviewOpen(false)} + capeUrl={imageUrl} + skinUrl={playerSkinUrl} + /> + )} + + ); +} diff --git a/src/renderer/pages/Shop.tsx b/src/renderer/pages/Shop.tsx index 5e25306..1a5b12a 100644 --- a/src/renderer/pages/Shop.tsx +++ b/src/renderer/pages/Shop.tsx @@ -24,12 +24,14 @@ import { fetchCase, openCase, Server, + fetchPlayer, } from '../api'; import { useEffect, useState } from 'react'; import { FullScreenLoader } from '../components/FullScreenLoader'; import { getPlayerServer } from '../utils/playerOnlineCheck'; import CaseRoulette from '../components/CaseRoulette'; import CoinsDisplay from '../components/CoinsDisplay'; +import ShopItem from '../components/ShopItem'; function getRarityByWeight( weight?: number, @@ -64,6 +66,8 @@ export default function Shop() { const [uuid, setUuid] = useState(''); const [loading, setLoading] = useState(false); + const [playerSkinUrl, setPlayerSkinUrl] = useState(''); + // Кейсы const [cases, setCases] = useState([]); const [casesLoading, setCasesLoading] = useState(false); @@ -92,8 +96,15 @@ export default function Shop() { type: 'success', }); - const VISIBLE_ITEMS = 21; // сколько элементов в линии - const CENTER_INDEX = Math.floor(VISIBLE_ITEMS / 2); + const loadPlayerSkin = async (uuid: string) => { + try { + const player = await fetchPlayer(uuid); + setPlayerSkinUrl(player.skin_url); + } catch (error) { + console.error('Ошибка при получении скина игрока:', error); + setPlayerSkinUrl(''); + } + }; // Функция для загрузки плащей из магазина const loadStoreCapes = async () => { @@ -184,6 +195,7 @@ export default function Shop() { loadStoreCapes(), loadUserCapes(config.username), loadCases(), + loadPlayerSkin(config.uuid), ]) .catch((err) => console.error(err)) .finally(() => { @@ -206,27 +218,6 @@ export default function Shop() { !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({ @@ -296,12 +287,11 @@ export default function Shop() { {(loading || onlineCheckLoading) && ( @@ -313,269 +303,123 @@ export default function Shop() { sx={{ display: 'flex', flexDirection: 'column', - flexWrap: 'wrap', - alignContent: 'flex-start', width: '90%', height: '80%', gap: '2vw', + overflow: 'auto', + paddingBottom: '7vh', + paddingLeft: '5vw', + paddingRight: '5vw', }} > {/* Блок кейсов */} - - Кейсы + + + Кейсы - {!isOnline && ( - + color: 'white', + border: 'none', + transition: 'transform 0.3s ease', + '&:hover': { + background: + 'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)', + transform: 'scale(1.05)', + boxShadow: '0 4px 15px rgba(242, 113, 33, 0.4)', + }, + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)', + }} + onClick={() => { + checkPlayerStatus(); // обновляем онлайн-статус + loadCases(); // обновляем ТОЛЬКО кейсы + }} + > + Обновить + + )} + + + {!isOnline ? ( + + Для открытия кейсов вам необходимо находиться на одном из + серверов игры. Зайдите в игру и нажмите кнопку «Обновить». + + ) : casesLoading ? ( + + ) : cases.length > 0 ? ( + + {cases.map((c) => ( + + handleOpenCase(c)} + /> + + ))} + + ) : ( + Кейсы временно недоступны. )} - {!isOnline ? ( - - Для открытия кейсов вам необходимо находиться на одном из серверов - игры. Зайдите в игру и нажмите кнопку «Обновить». - - ) : casesLoading ? ( - - ) : cases.length > 0 ? ( - - {cases.map((c) => ( - - {/* НОВАЯ КАРТОЧКА */} - - {/* верхний “свет” */} - - - {c.image_url && ( - - - - - - {/* маленький бейдж сверху картинки */} - - Кейс - - - )} - - - - {c.name} - - - {c.description && ( - - {c.description} - - )} - - - - Цена - - - - - {typeof c.items_count === 'number' && ( - - Предметов в кейсе: {c.items_count} - - )} - - - - - - ))} - - ) : ( - Кейсы временно недоступны. - )} - {/* Блок плащей (как был) */} - Доступные плащи - {availableCapes.length > 0 ? ( - - {availableCapes.map((cape) => ( - - ))} - - ) : ( - У вас уже есть все доступные плащи! - )} + + {/* Блок плащей */} + + Доступные плащи + + {availableCapes.length > 0 ? ( + + {availableCapes.map((cape) => ( + + handlePurchaseCape(cape.id)} + /> + + ))} + + ) : ( + У вас уже есть все доступные плащи! + )} + )}