add bonuses in shop
This commit is contained in:
@ -207,6 +207,190 @@ export interface MeResponse {
|
||||
is_admin: boolean;
|
||||
}
|
||||
|
||||
// ===== БОНУСЫ / ПРОКАЧКА =====
|
||||
|
||||
export interface UserBonus {
|
||||
id: string;
|
||||
bonus_type_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
effect_type: string;
|
||||
effect_value: number;
|
||||
level: number;
|
||||
purchased_at: string;
|
||||
can_upgrade: boolean;
|
||||
upgrade_price: number;
|
||||
is_active: boolean;
|
||||
is_permanent: boolean;
|
||||
expires_at?: string;
|
||||
image_url?: string;
|
||||
}
|
||||
|
||||
export type UserBonusesResponse = {
|
||||
bonuses: UserBonus[];
|
||||
};
|
||||
|
||||
export interface BonusType {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
effect_type: string;
|
||||
base_effect_value: number;
|
||||
effect_increment: number;
|
||||
price: number;
|
||||
upgrade_price: number;
|
||||
duration: number;
|
||||
max_level: number;
|
||||
image_url?: string;
|
||||
}
|
||||
|
||||
export type BonusTypesResponse = {
|
||||
bonuses: BonusType[];
|
||||
};
|
||||
|
||||
export async function fetchBonusTypes(): Promise<BonusType[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/bonuses/types`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Не удалось получить список прокачек');
|
||||
}
|
||||
|
||||
const data: BonusTypesResponse = await response.json();
|
||||
return data.bonuses || [];
|
||||
}
|
||||
|
||||
export async function fetchUserBonuses(username: string): Promise<UserBonus[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/bonuses/user/${username}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Не удалось получить бонусы игрока');
|
||||
}
|
||||
|
||||
const data: UserBonusesResponse = await response.json();
|
||||
return data.bonuses || [];
|
||||
}
|
||||
|
||||
export async function purchaseBonus(
|
||||
username: string,
|
||||
bonus_type_id: string,
|
||||
): Promise<{
|
||||
status: string;
|
||||
message: string;
|
||||
remaining_coins?: number;
|
||||
}> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/bonuses/purchase`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
bonus_type_id,
|
||||
}),
|
||||
});
|
||||
|
||||
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 {
|
||||
// оставляем дефолтное сообщение
|
||||
}
|
||||
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function upgradeBonus(
|
||||
username: string,
|
||||
bonus_id: string,
|
||||
): Promise<{
|
||||
status: string;
|
||||
message: string;
|
||||
bonus_id: string;
|
||||
new_level?: number;
|
||||
}> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/bonuses/upgrade`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
bonus_id,
|
||||
}),
|
||||
});
|
||||
|
||||
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 {
|
||||
// оставляем дефолтное сообщение
|
||||
}
|
||||
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function toggleBonusActivation(
|
||||
username: string,
|
||||
bonus_id: string,
|
||||
): Promise<{
|
||||
status: string;
|
||||
message: string;
|
||||
bonus_id: string;
|
||||
is_active: boolean;
|
||||
}> {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/api/bonuses/toggle-activation`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
bonus_id,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
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 {
|
||||
// оставляем дефолтное сообщение
|
||||
}
|
||||
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// ===== КЕЙСЫ =====
|
||||
|
||||
export interface CaseItemMeta {
|
||||
|
||||
310
src/renderer/components/BonusShopItem.tsx
Normal file
310
src/renderer/components/BonusShopItem.tsx
Normal file
@ -0,0 +1,310 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
CardMedia,
|
||||
} from '@mui/material';
|
||||
import CoinsDisplay from './CoinsDisplay';
|
||||
|
||||
export interface BonusShopItemProps {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
|
||||
level: number;
|
||||
effectValue: number;
|
||||
nextEffectValue?: number;
|
||||
|
||||
// цена покупки и улучшения
|
||||
price?: number;
|
||||
upgradePrice: number;
|
||||
canUpgrade: boolean;
|
||||
|
||||
mode?: 'buy' | 'upgrade';
|
||||
|
||||
isActive?: boolean;
|
||||
isPermanent?: boolean;
|
||||
|
||||
imageUrl?: string;
|
||||
|
||||
disabled?: boolean;
|
||||
|
||||
onBuy?: () => void;
|
||||
onUpgrade?: () => void;
|
||||
onToggleActive?: () => void;
|
||||
}
|
||||
|
||||
export const BonusShopItem: React.FC<BonusShopItemProps> = ({
|
||||
name,
|
||||
description,
|
||||
level,
|
||||
effectValue,
|
||||
nextEffectValue,
|
||||
price,
|
||||
upgradePrice,
|
||||
canUpgrade,
|
||||
mode,
|
||||
isActive = true,
|
||||
isPermanent = false,
|
||||
imageUrl,
|
||||
disabled,
|
||||
onBuy,
|
||||
onUpgrade,
|
||||
onToggleActive,
|
||||
}) => {
|
||||
const isBuyMode = mode === 'buy' || level === 0;
|
||||
const buttonText = isBuyMode
|
||||
? 'Купить'
|
||||
: canUpgrade
|
||||
? 'Улучшить'
|
||||
: 'Макс. уровень';
|
||||
const displayedPrice = isBuyMode ? (price ?? upgradePrice) : upgradePrice;
|
||||
|
||||
const buttonDisabled =
|
||||
disabled ||
|
||||
(isBuyMode
|
||||
? !onBuy || displayedPrice === undefined
|
||||
: !canUpgrade || !onUpgrade);
|
||||
|
||||
const handlePrimaryClick = () => {
|
||||
if (buttonDisabled) return;
|
||||
if (isBuyMode && onBuy) onBuy();
|
||||
else if (!isBuyMode && onUpgrade) onUpgrade();
|
||||
};
|
||||
|
||||
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: '0.9vw',
|
||||
pb: 0,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</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',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h6"
|
||||
color="white"
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '1.05rem',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="white"
|
||||
sx={{
|
||||
opacity: 0.7,
|
||||
fontSize: '0.8rem',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
Уровень: {level}
|
||||
</Typography>
|
||||
|
||||
{description && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="white"
|
||||
sx={{
|
||||
opacity: 0.75,
|
||||
fontSize: '0.85rem',
|
||||
mb: 1.4,
|
||||
minHeight: 40,
|
||||
maxHeight: 40,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box sx={{ mb: 1.2 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="white"
|
||||
sx={{ opacity: 0.8, fontSize: '0.8rem' }}
|
||||
>
|
||||
Текущий эффект: <b>{effectValue.toLocaleString('ru-RU')}</b>
|
||||
</Typography>
|
||||
|
||||
{typeof nextEffectValue === 'number' &&
|
||||
!isBuyMode &&
|
||||
canUpgrade && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="white"
|
||||
sx={{ opacity: 0.8, fontSize: '0.8rem', mt: 0.4 }}
|
||||
>
|
||||
Следующий уровень:{' '}
|
||||
<b>{nextEffectValue.toLocaleString('ru-RU')}</b>
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={isActive ? 'success.main' : 'warning.main'}
|
||||
sx={{ fontSize: '0.78rem' }}
|
||||
>
|
||||
{isActive ? 'Бонус активен' : 'Бонус не активен'}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="white"
|
||||
sx={{ opacity: 0.8, fontSize: '0.85rem' }}
|
||||
>
|
||||
{isBuyMode ? 'Цена покупки' : 'Цена улучшения'}
|
||||
</Typography>
|
||||
{displayedPrice !== undefined && (
|
||||
<CoinsDisplay
|
||||
value={displayedPrice}
|
||||
size="small"
|
||||
autoUpdate={false}
|
||||
showTooltip={true}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{!isBuyMode && onToggleActive && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
mb: 1,
|
||||
borderRadius: '999px',
|
||||
textTransform: 'none',
|
||||
fontSize: '0.75rem',
|
||||
}}
|
||||
onClick={onToggleActive}
|
||||
disabled={disabled}
|
||||
>
|
||||
{isActive ? 'Выключить' : 'Включить'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{
|
||||
mt: 0.5,
|
||||
borderRadius: '999px',
|
||||
py: '0.45vw',
|
||||
color: 'white',
|
||||
background: !buttonDisabled
|
||||
? 'linear-gradient(135deg, rgb(0, 160, 90), rgb(0, 200, 140))'
|
||||
: 'linear-gradient(135deg, rgba(120,120,120,0.9), rgba(80,80,80,0.9))',
|
||||
'&:hover': {
|
||||
background: !buttonDisabled
|
||||
? 'linear-gradient(135deg, rgba(0, 160, 90, 0.85), rgba(0, 200, 140, 0.9))'
|
||||
: 'linear-gradient(135deg, rgba(120,120,120,0.9), rgba(80,80,80,0.9))',
|
||||
},
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '0.9rem',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.8,
|
||||
}}
|
||||
disabled={buttonDisabled}
|
||||
onClick={handlePrimaryClick}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default BonusShopItem;
|
||||
@ -1,17 +1,4 @@
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Grid,
|
||||
Card,
|
||||
CardMedia,
|
||||
CardContent,
|
||||
Snackbar,
|
||||
Alert,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
} from '@mui/material';
|
||||
import CapeCard from '../components/CapeCard';
|
||||
import { Box, Typography, Button, Grid, Snackbar, Alert } from '@mui/material';
|
||||
import {
|
||||
Cape,
|
||||
fetchCapes,
|
||||
@ -25,12 +12,19 @@ import {
|
||||
openCase,
|
||||
Server,
|
||||
fetchPlayer,
|
||||
BonusType,
|
||||
UserBonus,
|
||||
fetchBonusTypes,
|
||||
fetchUserBonuses,
|
||||
purchaseBonus,
|
||||
upgradeBonus,
|
||||
toggleBonusActivation,
|
||||
} 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 BonusShopItem from '../components/BonusShopItem';
|
||||
import ShopItem from '../components/ShopItem';
|
||||
|
||||
function getRarityByWeight(
|
||||
@ -68,6 +62,13 @@ export default function Shop() {
|
||||
|
||||
const [playerSkinUrl, setPlayerSkinUrl] = useState<string>('');
|
||||
|
||||
// Прокачка
|
||||
|
||||
const [bonusTypes, setBonusTypes] = useState<BonusType[]>([]);
|
||||
const [userBonuses, setUserBonuses] = useState<UserBonus[]>([]);
|
||||
const [bonusesLoading, setBonusesLoading] = useState<boolean>(false);
|
||||
const [processingBonusIds, setProcessingBonusIds] = useState<string[]>([]);
|
||||
|
||||
// Кейсы
|
||||
const [cases, setCases] = useState<Case[]>([]);
|
||||
const [casesLoading, setCasesLoading] = useState<boolean>(false);
|
||||
@ -96,6 +97,30 @@ export default function Shop() {
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
const loadBonuses = async (username: string) => {
|
||||
try {
|
||||
setBonusesLoading(true);
|
||||
const [types, user] = await Promise.all([
|
||||
fetchBonusTypes(),
|
||||
fetchUserBonuses(username),
|
||||
]);
|
||||
setBonusTypes(types);
|
||||
setUserBonuses(user);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при получении прокачек:', error);
|
||||
setNotification({
|
||||
open: true,
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Ошибка при загрузке прокачки',
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
setBonusesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadPlayerSkin = async (uuid: string) => {
|
||||
try {
|
||||
const player = await fetchPlayer(uuid);
|
||||
@ -196,6 +221,7 @@ export default function Shop() {
|
||||
loadUserCapes(config.username),
|
||||
loadCases(),
|
||||
loadPlayerSkin(config.uuid),
|
||||
loadBonuses(config.username),
|
||||
])
|
||||
.catch((err) => console.error(err))
|
||||
.finally(() => {
|
||||
@ -212,6 +238,95 @@ export default function Shop() {
|
||||
}
|
||||
}, [username]);
|
||||
|
||||
const withProcessing = async (id: string, fn: () => Promise<void>) => {
|
||||
setProcessingBonusIds((prev) => [...prev, id]);
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
setProcessingBonusIds((prev) => prev.filter((x) => x !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const handlePurchaseBonus = async (bonusTypeId: string) => {
|
||||
if (!username) {
|
||||
setNotification({
|
||||
open: true,
|
||||
message: 'Не найдено имя игрока. Авторизуйтесь в лаунчере.',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await withProcessing(bonusTypeId, async () => {
|
||||
try {
|
||||
const res = await purchaseBonus(username, bonusTypeId);
|
||||
setNotification({
|
||||
open: true,
|
||||
message: res.message || 'Прокачка успешно куплена!',
|
||||
type: 'success',
|
||||
});
|
||||
await loadBonuses(username);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при покупке прокачки:', error);
|
||||
setNotification({
|
||||
open: true,
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Ошибка при покупке прокачки',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpgradeBonus = async (bonusId: string) => {
|
||||
if (!username) return;
|
||||
|
||||
await withProcessing(bonusId, async () => {
|
||||
try {
|
||||
await upgradeBonus(username, bonusId);
|
||||
setNotification({
|
||||
open: true,
|
||||
message: 'Бонус улучшен!',
|
||||
type: 'success',
|
||||
});
|
||||
await loadBonuses(username);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при улучшении бонуса:', error);
|
||||
setNotification({
|
||||
open: true,
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Ошибка при улучшении бонуса',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleBonusActivation = async (bonusId: string) => {
|
||||
if (!username) return;
|
||||
|
||||
await withProcessing(bonusId, async () => {
|
||||
try {
|
||||
await toggleBonusActivation(username, bonusId);
|
||||
await loadBonuses(username);
|
||||
} catch (error) {
|
||||
console.error('Ошибка при переключении бонуса:', error);
|
||||
setNotification({
|
||||
open: true,
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Ошибка при переключении бонуса',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Фильтруем плащи, которые уже куплены пользователем
|
||||
const availableCapes = storeCapes.filter(
|
||||
(storeCape) =>
|
||||
@ -312,6 +427,87 @@ export default function Shop() {
|
||||
paddingRight: '5vw',
|
||||
}}
|
||||
>
|
||||
{/* Блок прокачки */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Прокачка</Typography>
|
||||
|
||||
{bonusesLoading ? (
|
||||
<FullScreenLoader
|
||||
fullScreen={false}
|
||||
message="Загрузка прокачки..."
|
||||
/>
|
||||
) : bonusTypes.length > 0 ? (
|
||||
<Grid container spacing={2} sx={{ mb: 4 }}>
|
||||
{bonusTypes.map((bt) => {
|
||||
const userBonus = userBonuses.find(
|
||||
(ub) => ub.bonus_type_id === bt.id,
|
||||
);
|
||||
const owned = !!userBonus;
|
||||
|
||||
const level = owned ? userBonus!.level : 0;
|
||||
const effectValue = owned
|
||||
? userBonus!.effect_value
|
||||
: bt.base_effect_value;
|
||||
const nextEffectValue =
|
||||
owned && userBonus!.can_upgrade
|
||||
? bt.base_effect_value +
|
||||
userBonus!.level * bt.effect_increment
|
||||
: undefined;
|
||||
|
||||
const isActive = owned ? userBonus!.is_active : false;
|
||||
const isPermanent = owned
|
||||
? userBonus!.is_permanent
|
||||
: bt.duration === 0;
|
||||
|
||||
const cardId = owned ? userBonus!.id : bt.id;
|
||||
const processing = processingBonusIds.includes(cardId);
|
||||
|
||||
return (
|
||||
<Grid item xs={12} sm={6} md={4} lg={3} key={bt.id}>
|
||||
<BonusShopItem
|
||||
id={cardId}
|
||||
name={bt.name}
|
||||
description={bt.description}
|
||||
imageUrl={bt.image_url}
|
||||
level={level}
|
||||
effectValue={effectValue}
|
||||
nextEffectValue={nextEffectValue}
|
||||
price={bt.price}
|
||||
upgradePrice={bt.upgrade_price}
|
||||
canUpgrade={userBonus?.can_upgrade ?? false}
|
||||
mode={owned ? 'upgrade' : 'buy'}
|
||||
isActive={isActive}
|
||||
isPermanent={isPermanent}
|
||||
disabled={processing}
|
||||
onBuy={
|
||||
!owned ? () => handlePurchaseBonus(bt.id) : undefined
|
||||
}
|
||||
onUpgrade={
|
||||
owned
|
||||
? () => handleUpgradeBonus(userBonus!.id)
|
||||
: undefined
|
||||
}
|
||||
onToggleActive={
|
||||
owned
|
||||
? () => handleToggleBonusActivation(userBonus!.id)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
) : (
|
||||
<Typography>Прокачка временно недоступна.</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Блок кейсов */}
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
Reference in New Issue
Block a user