add cape preview in Shop
This commit is contained in:
@ -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<string>('');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const [playerSkinUrl, setPlayerSkinUrl] = useState<string>('');
|
||||
|
||||
// Кейсы
|
||||
const [cases, setCases] = useState<Case[]>([]);
|
||||
const [casesLoading, setCasesLoading] = useState<boolean>(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() {
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2vw',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{(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',
|
||||
}}
|
||||
>
|
||||
{/* Блок кейсов */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1 }}>
|
||||
<Typography variant="h6">Кейсы</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 2,
|
||||
mb: 1,
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Typography variant="h6">Кейсы</Typography>
|
||||
|
||||
{!isOnline && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
width: '9em',
|
||||
height: '3em',
|
||||
borderRadius: '2.5vw',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '0.7em',
|
||||
background:
|
||||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
transition: 'transform 0.3s ease',
|
||||
'&:hover': {
|
||||
{!isOnline && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
width: '9em',
|
||||
height: '3em',
|
||||
borderRadius: '2.5vw',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '0.7em',
|
||||
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(); // обновляем ТОЛЬКО кейсы
|
||||
}}
|
||||
>
|
||||
Обновить
|
||||
</Button>
|
||||
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(); // обновляем ТОЛЬКО кейсы
|
||||
}}
|
||||
>
|
||||
Обновить
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{!isOnline ? (
|
||||
<Typography variant="body1" color="error" sx={{ mb: 2 }}>
|
||||
Для открытия кейсов вам необходимо находиться на одном из
|
||||
серверов игры. Зайдите в игру и нажмите кнопку «Обновить».
|
||||
</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}>
|
||||
<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={!isOnline || isOpening}
|
||||
onClick={() => handleOpenCase(c)}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography>Кейсы временно недоступны.</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{!isOnline ? (
|
||||
<Typography variant="body1" color="error" sx={{ mb: 2 }}>
|
||||
Для открытия кейсов вам необходимо находиться на одном из серверов
|
||||
игры. Зайдите в игру и нажмите кнопку «Обновить».
|
||||
</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={{
|
||||
position: 'relative',
|
||||
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%)',
|
||||
}}
|
||||
/>
|
||||
|
||||
{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}
|
||||
onClick={() => handleOpenCase(c)}
|
||||
>
|
||||
{isOpening && selectedCase?.id === c.id
|
||||
? 'Открываем...'
|
||||
: 'Открыть кейс'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography>Кейсы временно недоступны.</Typography>
|
||||
)}
|
||||
|
||||
{/* Блок плащей (как был) */}
|
||||
<Typography variant="h6">Доступные плащи</Typography>
|
||||
{availableCapes.length > 0 ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: '2vw',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{availableCapes.map((cape) => (
|
||||
<CapeCard
|
||||
key={cape.id}
|
||||
cape={cape}
|
||||
mode="shop"
|
||||
onAction={handlePurchaseCape}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography>У вас уже есть все доступные плащи!</Typography>
|
||||
)}
|
||||
|
||||
{/* Блок плащей */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="h6">Доступные плащи</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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user