add cape preview in Shop
This commit is contained in:
@ -207,6 +207,8 @@ export interface MeResponse {
|
|||||||
is_admin: boolean;
|
is_admin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== КЕЙСЫ =====
|
||||||
|
|
||||||
export interface CaseItemMeta {
|
export interface CaseItemMeta {
|
||||||
display_name?: string | null;
|
display_name?: string | null;
|
||||||
lore?: string[] | null;
|
lore?: string[] | null;
|
||||||
|
|||||||
39
src/renderer/components/CapePreview.tsx
Normal file
39
src/renderer/components/CapePreview.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
|
||||||
|
interface CapePreviewProps {
|
||||||
|
imageUrl: string;
|
||||||
|
alt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CapePreview: React.FC<CapePreviewProps> = ({
|
||||||
|
imageUrl,
|
||||||
|
alt = 'Плащ',
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
height: 140, // фиксированная область под плащ
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={imageUrl}
|
||||||
|
alt={alt}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
imageRendering: 'pixelated',
|
||||||
|
|
||||||
|
// Берём старый "зум" из CapeCard — плащ становится большим,
|
||||||
|
// а лишнее обрезается контейнером.
|
||||||
|
transform: 'scale(2.9) translateX(0px) translateY(0px)',
|
||||||
|
transformOrigin: 'top left',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
79
src/renderer/components/PlayerPreviewModal.tsx
Normal file
79
src/renderer/components/PlayerPreviewModal.tsx
Normal file
@ -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<CapePreviewModalProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
capeUrl,
|
||||||
|
skinUrl,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||||
|
<DialogContent
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'rgba(5, 5, 15, 0.96)',
|
||||||
|
position: 'relative',
|
||||||
|
p: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
onClick={onClose}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Предпросмотр плаща
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<SkinViewer
|
||||||
|
width={350}
|
||||||
|
height={450}
|
||||||
|
capeUrl={capeUrl} // скин возьмётся дефолтный из SkinViewer
|
||||||
|
skinUrl={skinUrl}
|
||||||
|
autoRotate={true}
|
||||||
|
walkingSpeed={0.5}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CapePreviewModal;
|
||||||
296
src/renderer/components/ShopItem.tsx
Normal file
296
src/renderer/components/ShopItem.tsx
Normal file
@ -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 (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 280,
|
||||||
|
height: 420, // ✅ ФИКСИРОВАННАЯ ВЫСОТА
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
|
||||||
|
bgcolor: 'rgba(5, 5, 15, 0.96)',
|
||||||
|
borderRadius: '20px',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||||
|
boxShadow: '0 18px 45px rgba(0, 0, 0, 0.8)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition:
|
||||||
|
'transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease',
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'translateY(-6px)',
|
||||||
|
boxShadow: '0 26px 60px rgba(0, 0, 0, 0.95)',
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.18)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* верхний “свет” */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
background:
|
||||||
|
'radial-gradient(circle at top, rgba(255,255,255,0.13), transparent 55%)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{imageUrl && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
p: type === 'case' ? '0.9vw' : 0,
|
||||||
|
pb: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{type === 'case' ? (
|
||||||
|
/* как было для кейсов */
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderRadius: '16px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.12)',
|
||||||
|
background:
|
||||||
|
'linear-gradient(135deg, rgba(40,40,80,0.9), rgba(15,15,35,0.9))',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
image={imageUrl}
|
||||||
|
alt={name}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '11vw',
|
||||||
|
minHeight: '140px',
|
||||||
|
objectFit: 'cover',
|
||||||
|
filter: 'saturate(1.1)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
// ✅ здесь используем CapePreview
|
||||||
|
<CapePreview imageUrl={imageUrl} alt={name} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* кнопка предпросмотра плаща */}
|
||||||
|
{type === 'cape' && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: type === 'case' ? '1.2vw' : 8,
|
||||||
|
right: type === 'case' ? '1.6vw' : 8,
|
||||||
|
px: 1.2,
|
||||||
|
py: 0.4,
|
||||||
|
borderRadius: '999px',
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 0.6,
|
||||||
|
bgcolor: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.4)',
|
||||||
|
color: 'white',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
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)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VisibilityIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CardContent
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
pt: imageUrl ? '0.9vw' : '1.4vw',
|
||||||
|
pb: '1.3vw',
|
||||||
|
|
||||||
|
flexGrow: 1, // ✅ растягивает середину
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between', // ✅ кнопка уезжает вниз
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
color="white"
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
fontSize: '1.05rem',
|
||||||
|
mb: 0.8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="white"
|
||||||
|
sx={{
|
||||||
|
opacity: 0.75,
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
mb: 1.5,
|
||||||
|
|
||||||
|
minHeight: 40,
|
||||||
|
maxHeight: 40, // ✅ фикс высоты
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{typeof price === 'number' && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="white"
|
||||||
|
sx={{ opacity: 0.8, fontSize: '0.85rem' }}
|
||||||
|
>
|
||||||
|
Цена
|
||||||
|
</Typography>
|
||||||
|
<CoinsDisplay
|
||||||
|
value={price}
|
||||||
|
size="small"
|
||||||
|
autoUpdate={false}
|
||||||
|
showTooltip={true}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === 'case' && typeof itemsCount === 'number' && (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="white"
|
||||||
|
sx={{ opacity: 0.6, fontSize: '0.8rem', mb: 1.4 }}
|
||||||
|
>
|
||||||
|
Предметов в кейсе: {itemsCount}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
mt: 0.5,
|
||||||
|
borderRadius: '999px',
|
||||||
|
py: '0.45vw',
|
||||||
|
color: 'white',
|
||||||
|
background:
|
||||||
|
type === 'case'
|
||||||
|
? 'linear-gradient(135deg, rgb(255, 77, 77), rgb(255, 120, 100))'
|
||||||
|
: 'linear-gradient(135deg, rgb(0, 160, 90), rgb(0, 200, 140))',
|
||||||
|
'&:hover': {
|
||||||
|
background:
|
||||||
|
type === 'case'
|
||||||
|
? 'linear-gradient(135deg, rgba(255, 77, 77, 0.85), rgba(255, 120, 100, 0.9))'
|
||||||
|
: 'linear-gradient(135deg, rgba(0, 160, 90, 0.85), rgba(0, 200, 140, 0.9))',
|
||||||
|
},
|
||||||
|
fontFamily: 'Benzin-Bold',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 0.8,
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
{type === 'cape' && imageUrl && (
|
||||||
|
<CapePreviewModal
|
||||||
|
open={previewOpen}
|
||||||
|
onClose={() => setPreviewOpen(false)}
|
||||||
|
capeUrl={imageUrl}
|
||||||
|
skinUrl={playerSkinUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -24,12 +24,14 @@ import {
|
|||||||
fetchCase,
|
fetchCase,
|
||||||
openCase,
|
openCase,
|
||||||
Server,
|
Server,
|
||||||
|
fetchPlayer,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FullScreenLoader } from '../components/FullScreenLoader';
|
import { FullScreenLoader } from '../components/FullScreenLoader';
|
||||||
import { getPlayerServer } from '../utils/playerOnlineCheck';
|
import { getPlayerServer } from '../utils/playerOnlineCheck';
|
||||||
import CaseRoulette from '../components/CaseRoulette';
|
import CaseRoulette from '../components/CaseRoulette';
|
||||||
import CoinsDisplay from '../components/CoinsDisplay';
|
import CoinsDisplay from '../components/CoinsDisplay';
|
||||||
|
import ShopItem from '../components/ShopItem';
|
||||||
|
|
||||||
function getRarityByWeight(
|
function getRarityByWeight(
|
||||||
weight?: number,
|
weight?: number,
|
||||||
@ -64,6 +66,8 @@ export default function Shop() {
|
|||||||
const [uuid, setUuid] = useState<string>('');
|
const [uuid, setUuid] = useState<string>('');
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [playerSkinUrl, setPlayerSkinUrl] = useState<string>('');
|
||||||
|
|
||||||
// Кейсы
|
// Кейсы
|
||||||
const [cases, setCases] = useState<Case[]>([]);
|
const [cases, setCases] = useState<Case[]>([]);
|
||||||
const [casesLoading, setCasesLoading] = useState<boolean>(false);
|
const [casesLoading, setCasesLoading] = useState<boolean>(false);
|
||||||
@ -92,8 +96,15 @@ export default function Shop() {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
});
|
});
|
||||||
|
|
||||||
const VISIBLE_ITEMS = 21; // сколько элементов в линии
|
const loadPlayerSkin = async (uuid: string) => {
|
||||||
const CENTER_INDEX = Math.floor(VISIBLE_ITEMS / 2);
|
try {
|
||||||
|
const player = await fetchPlayer(uuid);
|
||||||
|
setPlayerSkinUrl(player.skin_url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при получении скина игрока:', error);
|
||||||
|
setPlayerSkinUrl('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Функция для загрузки плащей из магазина
|
// Функция для загрузки плащей из магазина
|
||||||
const loadStoreCapes = async () => {
|
const loadStoreCapes = async () => {
|
||||||
@ -184,6 +195,7 @@ export default function Shop() {
|
|||||||
loadStoreCapes(),
|
loadStoreCapes(),
|
||||||
loadUserCapes(config.username),
|
loadUserCapes(config.username),
|
||||||
loadCases(),
|
loadCases(),
|
||||||
|
loadPlayerSkin(config.uuid),
|
||||||
])
|
])
|
||||||
.catch((err) => console.error(err))
|
.catch((err) => console.error(err))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -206,27 +218,6 @@ export default function Shop() {
|
|||||||
!userCapes.some((userCape) => userCape.cape_id === storeCape.id),
|
!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) => {
|
const handleOpenCase = async (caseData: Case) => {
|
||||||
if (!username) {
|
if (!username) {
|
||||||
setNotification({
|
setNotification({
|
||||||
@ -296,12 +287,11 @@ export default function Shop() {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '2vw',
|
gap: '2vw',
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(loading || onlineCheckLoading) && (
|
{(loading || onlineCheckLoading) && (
|
||||||
@ -313,15 +303,26 @@ export default function Shop() {
|
|||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignContent: 'flex-start',
|
|
||||||
width: '90%',
|
width: '90%',
|
||||||
height: '80%',
|
height: '80%',
|
||||||
gap: '2vw',
|
gap: '2vw',
|
||||||
|
overflow: 'auto',
|
||||||
|
paddingBottom: '7vh',
|
||||||
|
paddingLeft: '5vw',
|
||||||
|
paddingRight: '5vw',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Блок кейсов */}
|
{/* Блок кейсов */}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1 }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: 2,
|
||||||
|
mb: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
<Typography variant="h6">Кейсы</Typography>
|
<Typography variant="h6">Кейсы</Typography>
|
||||||
|
|
||||||
{!isOnline && (
|
{!isOnline && (
|
||||||
@ -359,224 +360,67 @@ export default function Shop() {
|
|||||||
|
|
||||||
{!isOnline ? (
|
{!isOnline ? (
|
||||||
<Typography variant="body1" color="error" sx={{ mb: 2 }}>
|
<Typography variant="body1" color="error" sx={{ mb: 2 }}>
|
||||||
Для открытия кейсов вам необходимо находиться на одном из серверов
|
Для открытия кейсов вам необходимо находиться на одном из
|
||||||
игры. Зайдите в игру и нажмите кнопку «Обновить».
|
серверов игры. Зайдите в игру и нажмите кнопку «Обновить».
|
||||||
</Typography>
|
</Typography>
|
||||||
) : 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 }}>
|
||||||
{cases.map((c) => (
|
{cases.map((c) => (
|
||||||
<Grid item xs={12} sm={6} md={4} lg={3} key={c.id}>
|
<Grid item xs={12} sm={6} md={4} lg={3} key={c.id}>
|
||||||
{/* НОВАЯ КАРТОЧКА */}
|
<ShopItem
|
||||||
<Card
|
type="case"
|
||||||
sx={{
|
id={c.id}
|
||||||
position: 'relative',
|
name={c.name}
|
||||||
bgcolor: 'rgba(5, 5, 15, 0.96)',
|
description={c.description}
|
||||||
borderRadius: '20px',
|
imageUrl={c.image_url}
|
||||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
price={c.price}
|
||||||
boxShadow: '0 18px 45px rgba(0, 0, 0, 0.8)',
|
itemsCount={c.items_count}
|
||||||
overflow: 'hidden',
|
isOpening={isOpening && selectedCase?.id === c.id}
|
||||||
transition:
|
|
||||||
'transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease',
|
|
||||||
'&:hover': {
|
|
||||||
transform: 'translateY(-6px)',
|
|
||||||
boxShadow: '0 26px 60px rgba(0, 0, 0, 0.95)',
|
|
||||||
borderColor: 'rgba(255, 255, 255, 0.18)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* верхний “свет” */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
inset: 0,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
background:
|
|
||||||
'radial-gradient(circle at top, rgba(255,255,255,0.13), transparent 55%)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{c.image_url && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
p: '0.9vw',
|
|
||||||
pb: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
borderRadius: '16px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.12)',
|
|
||||||
background:
|
|
||||||
'linear-gradient(135deg, rgba(40,40,80,0.9), rgba(15,15,35,0.9))',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CardMedia
|
|
||||||
component="img"
|
|
||||||
image={c.image_url}
|
|
||||||
alt={c.name}
|
|
||||||
sx={{
|
|
||||||
width: '100%',
|
|
||||||
height: '11vw',
|
|
||||||
minHeight: '140px',
|
|
||||||
objectFit: 'cover',
|
|
||||||
filter: 'saturate(1.1)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* маленький бейдж сверху картинки */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '1.2vw',
|
|
||||||
left: '1.6vw',
|
|
||||||
px: 1.2,
|
|
||||||
py: 0.4,
|
|
||||||
borderRadius: '999px',
|
|
||||||
fontSize: '0.7rem',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 0.6,
|
|
||||||
bgcolor: 'rgba(0, 0, 0, 0.6)',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.4)',
|
|
||||||
color: 'white',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Кейс
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<CardContent
|
|
||||||
sx={{
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: 1,
|
|
||||||
pt: c.image_url ? '0.9vw' : '1.4vw',
|
|
||||||
pb: '1.3vw',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
color="white"
|
|
||||||
sx={{
|
|
||||||
fontFamily: 'Benzin-Bold',
|
|
||||||
fontSize: '1.05rem',
|
|
||||||
mb: 0.8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{c.name}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{c.description && (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="white"
|
|
||||||
sx={{
|
|
||||||
opacity: 0.75,
|
|
||||||
fontSize: '0.85rem',
|
|
||||||
mb: 1.5,
|
|
||||||
minHeight: '40px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{c.description}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
mb: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="white"
|
|
||||||
sx={{ opacity: 0.8, fontSize: '0.85rem' }}
|
|
||||||
>
|
|
||||||
Цена
|
|
||||||
</Typography>
|
|
||||||
<CoinsDisplay
|
|
||||||
value={c.price}
|
|
||||||
size="small"
|
|
||||||
autoUpdate={false}
|
|
||||||
showTooltip={true}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{typeof c.items_count === 'number' && (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="white"
|
|
||||||
sx={{ opacity: 0.6, fontSize: '0.8rem', mb: 1.4 }}
|
|
||||||
>
|
|
||||||
Предметов в кейсе: {c.items_count}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
fullWidth
|
|
||||||
sx={{
|
|
||||||
mt: 0.5,
|
|
||||||
borderRadius: '999px',
|
|
||||||
py: '0.45vw',
|
|
||||||
color: 'white',
|
|
||||||
background:
|
|
||||||
'linear-gradient(135deg, rgb(255, 77, 77), rgb(255, 120, 100))',
|
|
||||||
'&:hover': {
|
|
||||||
background:
|
|
||||||
'linear-gradient(135deg, rgba(255, 77, 77, 0.85), rgba(255, 120, 100, 0.9))',
|
|
||||||
},
|
|
||||||
fontFamily: 'Benzin-Bold',
|
|
||||||
fontSize: '0.9rem',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 0.8,
|
|
||||||
}}
|
|
||||||
disabled={!isOnline || isOpening}
|
disabled={!isOnline || isOpening}
|
||||||
onClick={() => handleOpenCase(c)}
|
onClick={() => handleOpenCase(c)}
|
||||||
>
|
/>
|
||||||
{isOpening && selectedCase?.id === c.id
|
|
||||||
? 'Открываем...'
|
|
||||||
: 'Открыть кейс'}
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
<Typography>Кейсы временно недоступны.</Typography>
|
<Typography>Кейсы временно недоступны.</Typography>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Блок плащей (как был) */}
|
{/* Блок плащей (как был) */}
|
||||||
|
|
||||||
|
{/* Блок плащей */}
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
<Typography variant="h6">Доступные плащи</Typography>
|
<Typography variant="h6">Доступные плащи</Typography>
|
||||||
|
|
||||||
{availableCapes.length > 0 ? (
|
{availableCapes.length > 0 ? (
|
||||||
<Box
|
<Grid container spacing={2}>
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: '2vw',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{availableCapes.map((cape) => (
|
{availableCapes.map((cape) => (
|
||||||
<CapeCard
|
<Grid item xs={12} sm={6} md={4} lg={3} key={cape.id}>
|
||||||
key={cape.id}
|
<ShopItem
|
||||||
cape={cape}
|
type="cape"
|
||||||
mode="shop"
|
id={cape.id}
|
||||||
onAction={handlePurchaseCape}
|
name={cape.name}
|
||||||
|
description={cape.description}
|
||||||
|
imageUrl={cape.image_url}
|
||||||
|
price={cape.price}
|
||||||
|
disabled={false}
|
||||||
|
playerSkinUrl={playerSkinUrl}
|
||||||
|
onClick={() => handlePurchaseCape(cape.id)}
|
||||||
/>
|
/>
|
||||||
|
</Grid>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
<Typography>У вас уже есть все доступные плащи!</Typography>
|
<Typography>У вас уже есть все доступные плащи!</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Компонент с анимацией рулетки */}
|
{/* Компонент с анимацией рулетки */}
|
||||||
|
|||||||
Reference in New Issue
Block a user