Merge branch 'feat/VersionsExplorer' of https://git.popa-popa.ru/DIKER/popa-launcher into feat/VersionsExplorer
This commit is contained in:
@ -165,26 +165,7 @@ export interface OnlinePlayersResponse {
|
||||
}
|
||||
|
||||
export interface MarketplaceResponse {
|
||||
items: [
|
||||
{
|
||||
_id: string;
|
||||
id: string;
|
||||
material: string;
|
||||
amount: number;
|
||||
price: number;
|
||||
seller_name: string;
|
||||
server_ip: string;
|
||||
display_name: string | null;
|
||||
lore: string | null;
|
||||
enchants: string | null;
|
||||
item_data: {
|
||||
slot: number;
|
||||
material: string;
|
||||
amount: number;
|
||||
};
|
||||
created_at: string;
|
||||
},
|
||||
];
|
||||
items: MarketplaceItemResponse[];
|
||||
total: number;
|
||||
page: number;
|
||||
pages: number;
|
||||
@ -1033,6 +1014,57 @@ export async function RequestPlayerInventory(
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// ===== Marketplace: мои лоты (изменить цену / снять с продажи) =====
|
||||
|
||||
export async function updateMarketplaceItemPrice(
|
||||
username: string,
|
||||
item_id: string,
|
||||
new_price: number,
|
||||
): Promise<{ status: string; message?: string }> {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/api/marketplace/items/${encodeURIComponent(item_id)}/price`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, new_price }),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let msg = 'Не удалось изменить цену';
|
||||
try {
|
||||
const err = await response.json();
|
||||
msg = err.message || err.detail || msg;
|
||||
} catch {}
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function cancelMarketplaceItemSale(
|
||||
username: string,
|
||||
item_id: string,
|
||||
): Promise<{ status: string; message?: string }> {
|
||||
const url = new URL(
|
||||
`${API_BASE_URL}/api/marketplace/items/${encodeURIComponent(item_id)}`,
|
||||
);
|
||||
url.searchParams.set('username', username);
|
||||
|
||||
const response = await fetch(url.toString(), { method: 'DELETE' });
|
||||
|
||||
if (!response.ok) {
|
||||
let msg = 'Не удалось снять товар с продажи';
|
||||
try {
|
||||
const err = await response.json();
|
||||
msg = err.message || err.detail || msg;
|
||||
} catch {}
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function buyItem(
|
||||
buyer_username: string,
|
||||
item_id: string,
|
||||
@ -1175,6 +1207,40 @@ export async function fetchMarketplace(
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function fetchMyMarketplaceItems(
|
||||
username: string,
|
||||
server_ip?: string | null,
|
||||
page = 1,
|
||||
limit = 20,
|
||||
): Promise<MarketplaceResponse> {
|
||||
// вместо /items/me используем /items/by-seller/{username}
|
||||
const url = new URL(`${API_BASE_URL}/api/marketplace/items/by-seller/${encodeURIComponent(username)}`);
|
||||
|
||||
if (server_ip) url.searchParams.set('server_ip', server_ip);
|
||||
url.searchParams.set('page', String(page));
|
||||
url.searchParams.set('limit', String(limit));
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
|
||||
// если на бэке “нет предметов” возвращают 404 — можно трактовать как пустой список
|
||||
if (response.status === 404) {
|
||||
return { items: [], total: 0, page, pages: 1 };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let msg = 'Не удалось получить ваши товары';
|
||||
try {
|
||||
const err = await response.json();
|
||||
msg = err.message || err.detail || msg;
|
||||
} catch {}
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// ===== Marketplace ===== \\
|
||||
|
||||
// Исправьте тип возвращаемого значения
|
||||
export async function fetchActiveServers(): Promise<Server[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/pranks/servers`);
|
||||
|
||||
@ -134,13 +134,13 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!onlinePlayers.length) {
|
||||
return (
|
||||
<Typography sx={{ mt: 2, color: 'rgba(255,255,255,0.75)', fontWeight: 700 }}>
|
||||
Сейчас на серверах никого нет.
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
// if (!onlinePlayers.length) {
|
||||
// return (
|
||||
// <Typography sx={{ mt: 2, color: 'rgba(255,255,255,0.75)', fontWeight: 700 }}>
|
||||
// Сейчас на серверах никого нет.
|
||||
// </Typography>
|
||||
// );
|
||||
// }
|
||||
|
||||
const totalOnline = onlinePlayers.length;
|
||||
|
||||
@ -371,82 +371,119 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
{filteredPlayers.map((p) => {
|
||||
const isMe = p.username === currentUsername;
|
||||
{filteredPlayers.length ? (
|
||||
filteredPlayers.map((p) => {
|
||||
const isMe = p.username === currentUsername;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
key={p.uuid}
|
||||
elevation={0}
|
||||
sx={{
|
||||
px: '1.1vw',
|
||||
py: '0.75vw',
|
||||
borderRadius: '1.1vw',
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '1vw',
|
||||
transition: 'transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.01)',
|
||||
borderColor: 'rgba(242,113,33,0.35)',
|
||||
boxShadow: '0 0.8vw 2.4vw rgba(0,0,0,0.45)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '0.8vw', minWidth: 0 }}>
|
||||
<HeadAvatar skinUrl={skinMap[p.uuid]} size={26} />
|
||||
<Typography
|
||||
return (
|
||||
<Paper
|
||||
key={p.uuid}
|
||||
elevation={0}
|
||||
sx={{
|
||||
px: '1.1vw',
|
||||
py: '0.75vw',
|
||||
borderRadius: '1.1vw',
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '1vw',
|
||||
transition:
|
||||
'transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.01)',
|
||||
borderColor: 'rgba(242,113,33,0.35)',
|
||||
boxShadow: '0 0.8vw 2.4vw rgba(0,0,0,0.45)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
color: 'rgba(255,255,255,0.92)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.8vw',
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{p.username}
|
||||
</Typography>
|
||||
<HeadAvatar skinUrl={skinMap[p.uuid]} size={26} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
color: 'rgba(255,255,255,0.92)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{p.username}
|
||||
</Typography>
|
||||
|
||||
{isMe && (
|
||||
{isMe && (
|
||||
<Chip
|
||||
label="Вы"
|
||||
size="small"
|
||||
sx={{
|
||||
height: '1.55rem',
|
||||
fontSize: '0.72rem',
|
||||
fontWeight: 900,
|
||||
color: 'white',
|
||||
borderRadius: '999px',
|
||||
backgroundImage: GRADIENT,
|
||||
boxShadow: '0 10px 22px rgba(0,0,0,0.45)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.6vw',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label="Вы"
|
||||
label={translateServer(p.serverName)}
|
||||
size="small"
|
||||
sx={{
|
||||
height: '1.55rem',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '0.72rem',
|
||||
fontWeight: 900,
|
||||
color: 'white',
|
||||
borderRadius: '999px',
|
||||
backgroundImage: GRADIENT,
|
||||
boxShadow: '0 10px 22px rgba(0,0,0,0.45)',
|
||||
color: 'rgba(255,255,255,0.88)',
|
||||
background:
|
||||
'linear-gradient(120deg, rgba(242,113,33,0.18), rgba(233,64,205,0.12), rgba(138,35,135,0.16))',
|
||||
border: '1px solid rgba(255,255,255,0.10)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '0.6vw', flexShrink: 0 }}>
|
||||
<Chip
|
||||
label={translateServer(p.serverName)}
|
||||
size="small"
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '0.72rem',
|
||||
borderRadius: '999px',
|
||||
color: 'rgba(255,255,255,0.88)',
|
||||
background:
|
||||
'linear-gradient(120deg, rgba(242,113,33,0.18), rgba(233,64,205,0.12), rgba(138,35,135,0.16))',
|
||||
border: '1px solid rgba(255,255,255,0.10)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
}}
|
||||
/>
|
||||
{/* onlineSince можно потом красиво форматировать */}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
py: '1.4vw',
|
||||
px: '1.1vw',
|
||||
borderRadius: '1.1vw',
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
border: '1px dashed rgba(255,255,255,0.14)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '0.95vw',
|
||||
color: 'rgba(255,255,255,0.78)',
|
||||
}}
|
||||
>
|
||||
Сейчас на сервере никого нет!
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// src/renderer/components/PlayerInventory.tsx
|
||||
import { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useImperativeHandle, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@ -22,6 +22,118 @@ import {
|
||||
PlayerInventoryItem,
|
||||
} from '../api';
|
||||
import { FullScreenLoader } from './FullScreenLoader';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
|
||||
const GRADIENT =
|
||||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)';
|
||||
|
||||
const GLASS_PAPER_SX = {
|
||||
borderRadius: '1.2vw',
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.14), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.12), transparent 55%), rgba(10,10,20,0.86)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
boxShadow: '0 1.2vw 3.2vw rgba(0,0,0,0.55)',
|
||||
color: 'white',
|
||||
backdropFilter: 'blur(16px)',
|
||||
} as const;
|
||||
|
||||
const DIALOG_TITLE_SX = {
|
||||
fontFamily: 'Benzin-Bold',
|
||||
pr: 6,
|
||||
position: 'relative',
|
||||
} as const;
|
||||
|
||||
const CLOSE_BTN_SX = {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 10,
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
background: 'rgba(255,255,255,0.06)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
'&:hover': { transform: 'scale(1.05)', background: 'rgba(255,255,255,0.10)' },
|
||||
transition: 'all 0.2s ease',
|
||||
} as const;
|
||||
|
||||
const DIVIDERS_SX = {
|
||||
borderColor: 'rgba(255,255,255,0.10)',
|
||||
} as const;
|
||||
|
||||
const INPUT_SX = {
|
||||
mt: 1.2,
|
||||
|
||||
'& .MuiInputLabel-root': {
|
||||
color: 'rgba(255,255,255,0.72)',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
letterSpacing: 0.3,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
'& .MuiInputLabel-root.Mui-focused': {
|
||||
color: 'rgba(255,255,255,0.92)',
|
||||
},
|
||||
|
||||
'& .MuiOutlinedInput-root': {
|
||||
position: 'relative',
|
||||
borderRadius: '1.1vw',
|
||||
overflow: 'hidden',
|
||||
|
||||
background:
|
||||
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.16), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.12), transparent 55%), rgba(10,10,20,0.92)',
|
||||
border: '1px solid rgba(255,255,255,0.10)',
|
||||
boxShadow: '0 1.2vw 3.0vw rgba(0,0,0,0.55)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
|
||||
'& .MuiOutlinedInput-notchedOutline': { border: 'none' },
|
||||
|
||||
'& input': {
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '1.0rem',
|
||||
padding: '1.0vw 1.0vw',
|
||||
color: 'rgba(255,255,255,0.95)',
|
||||
},
|
||||
|
||||
transition: 'transform 0.18s ease, filter 0.18s ease, border-color 0.18s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.01)',
|
||||
borderColor: 'rgba(255,255,255,0.14)',
|
||||
},
|
||||
'&.Mui-focused': {
|
||||
borderColor: 'rgba(255,255,255,0.18)',
|
||||
filter: 'brightness(1.03)',
|
||||
},
|
||||
|
||||
'&:after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: '0.18vw',
|
||||
borderRadius: '999px',
|
||||
background: GRADIENT,
|
||||
opacity: 0.92,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const PRIMARY_BTN_SX = {
|
||||
fontFamily: 'Benzin-Bold',
|
||||
color: '#fff',
|
||||
background: GRADIENT,
|
||||
borderRadius: '999px',
|
||||
px: '1.6vw',
|
||||
py: '0.65vw',
|
||||
boxShadow: '0 1.0vw 2.6vw rgba(0,0,0,0.45)',
|
||||
'&:hover': { filter: 'brightness(1.05)' },
|
||||
} as const;
|
||||
|
||||
const SECONDARY_BTN_SX = {
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
} as const;
|
||||
|
||||
interface PlayerInventoryProps {
|
||||
username: string;
|
||||
@ -29,11 +141,12 @@ interface PlayerInventoryProps {
|
||||
onSellSuccess?: () => void; // Callback для обновления маркетплейса после продажи
|
||||
}
|
||||
|
||||
export default function PlayerInventory({
|
||||
username,
|
||||
serverIp,
|
||||
onSellSuccess,
|
||||
}: PlayerInventoryProps) {
|
||||
export type PlayerInventoryHandle = {
|
||||
refresh: () => Promise<void>;
|
||||
};
|
||||
|
||||
const PlayerInventory = React.forwardRef<PlayerInventoryHandle, PlayerInventoryProps>(
|
||||
({ username, serverIp, onSellSuccess }, ref) => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [inventoryItems, setInventoryItems] = useState<PlayerInventoryItem[]>(
|
||||
[],
|
||||
@ -96,6 +209,12 @@ export default function PlayerInventory({
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
refresh: async () => {
|
||||
await fetchPlayerInventory();
|
||||
},
|
||||
}));
|
||||
|
||||
// Открываем диалог для продажи предмета
|
||||
const handleOpenSellDialog = (item: PlayerInventoryItem) => {
|
||||
setSelectedItem(item);
|
||||
@ -183,7 +302,7 @@ export default function PlayerInventory({
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: '1vw' }}>
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@ -195,25 +314,6 @@ export default function PlayerInventory({
|
||||
<Typography variant="h5" color="white">
|
||||
Ваш инвентарь
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={fetchPlayerInventory}
|
||||
disabled={loading}
|
||||
sx={{
|
||||
borderRadius: '20px',
|
||||
p: '10px 25px',
|
||||
color: 'white',
|
||||
borderColor: 'rgba(255, 77, 77, 1)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 77, 77, 1)',
|
||||
borderColor: 'rgba(255, 77, 77, 1)',
|
||||
},
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '1vw',
|
||||
}}
|
||||
>
|
||||
Обновить
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
@ -223,7 +323,7 @@ export default function PlayerInventory({
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<FullScreenLoader fullScreen={false} />
|
||||
<FullScreenLoader fullScreen={false} message="Загрузка инвентаря..." />
|
||||
) : (
|
||||
<>
|
||||
{inventoryItems.length === 0 ? (
|
||||
@ -311,79 +411,148 @@ export default function PlayerInventory({
|
||||
)}
|
||||
|
||||
{/* Диалог для продажи предмета */}
|
||||
<Dialog open={sellDialogOpen} onClose={handleCloseSellDialog}>
|
||||
<DialogTitle>Продать предмет</DialogTitle>
|
||||
<DialogContent>
|
||||
<Dialog
|
||||
open={sellDialogOpen}
|
||||
onClose={handleCloseSellDialog}
|
||||
fullWidth
|
||||
maxWidth="xs"
|
||||
PaperProps={{ sx: GLASS_PAPER_SX }}
|
||||
>
|
||||
<DialogTitle sx={DIALOG_TITLE_SX}>
|
||||
Продать предмет
|
||||
<IconButton onClick={handleCloseSellDialog} sx={CLOSE_BTN_SX}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent dividers sx={DIVIDERS_SX}>
|
||||
{selectedItem && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<CardMedia
|
||||
<Box sx={{ mt: 0.5 }}>
|
||||
{/* Верхняя карточка предмета */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1vw',
|
||||
p: '0.9vw',
|
||||
borderRadius: '1.1vw',
|
||||
border: '1px solid rgba(255,255,255,0.10)',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
mb: 1.1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
sx={{
|
||||
width: 50,
|
||||
height: 50,
|
||||
objectFit: 'contain',
|
||||
mr: 2,
|
||||
}}
|
||||
image={`https://cdn.minecraft.popa-popa.ru/textures/${selectedItem.material.toLowerCase()}.png`}
|
||||
src={`https://cdn.minecraft.popa-popa.ru/textures/${selectedItem.material.toLowerCase()}.png`}
|
||||
alt={selectedItem.material}
|
||||
draggable={false}
|
||||
style={{
|
||||
width: 54,
|
||||
height: 54,
|
||||
objectFit: 'contain',
|
||||
imageRendering: 'pixelated',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="h6">
|
||||
{getItemDisplayName(selectedItem.material)}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '1.05rem',
|
||||
lineHeight: 1.1,
|
||||
color: 'rgba(255,255,255,0.95)',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
title={getItemDisplayName(selectedItem.material)}
|
||||
>
|
||||
{getItemDisplayName(selectedItem.material)}
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.70)', fontWeight: 800, mt: 0.4 }}>
|
||||
Доступно: <span style={{ color: 'rgba(255,255,255,0.92)' }}>{selectedItem.amount}</span>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Всего доступно: {selectedItem.amount}
|
||||
</Typography>
|
||||
|
||||
{/* Поля */}
|
||||
<TextField
|
||||
label="Количество"
|
||||
type="number"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={amount}
|
||||
onChange={(e) =>
|
||||
setAmount(
|
||||
Math.min(
|
||||
parseInt(e.target.value) || 0,
|
||||
selectedItem.amount,
|
||||
),
|
||||
)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value);
|
||||
const safe = Number.isFinite(v) ? v : 0;
|
||||
setAmount(Math.min(Math.max(1, safe), selectedItem.amount));
|
||||
}}
|
||||
inputProps={{ min: 1, max: selectedItem.amount }}
|
||||
sx={INPUT_SX}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Цена (за всё)"
|
||||
type="number"
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={price}
|
||||
onChange={(e) => setPrice(parseInt(e.target.value) || 0)}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value);
|
||||
setPrice(Number.isFinite(v) ? v : 0);
|
||||
}}
|
||||
inputProps={{ min: 1 }}
|
||||
sx={INPUT_SX}
|
||||
/>
|
||||
|
||||
{sellError && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
mt: 1.2,
|
||||
p: '0.9vw',
|
||||
borderRadius: '1.0vw',
|
||||
border: '1px solid rgba(255,70,70,0.22)',
|
||||
background: 'rgba(255,70,70,0.12)',
|
||||
color: 'rgba(255,255,255,0.92)',
|
||||
fontWeight: 800,
|
||||
}}
|
||||
>
|
||||
{sellError}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Подсказка */}
|
||||
<Typography sx={{ mt: 1.1, color: 'rgba(255,255,255,0.60)', fontWeight: 700 }}>
|
||||
Цена указывается за весь лот!
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCloseSellDialog}>Отмена</Button>
|
||||
|
||||
<DialogActions sx={{ p: '1.2vw' }}>
|
||||
<Button onClick={handleCloseSellDialog} sx={SECONDARY_BTN_SX}>
|
||||
Отмена
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleSellItem}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disableRipple
|
||||
disabled={sellLoading}
|
||||
sx={{
|
||||
...PRIMARY_BTN_SX,
|
||||
...(sellLoading
|
||||
? {
|
||||
background: 'rgba(255,255,255,0.10)',
|
||||
boxShadow: 'none',
|
||||
color: 'rgba(255,255,255,0.55)',
|
||||
}
|
||||
: null),
|
||||
}}
|
||||
>
|
||||
{sellLoading ? <FullScreenLoader fullScreen={false} /> : 'Продать'}
|
||||
{sellLoading ? 'Выставляем…' : 'Продать'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
export default PlayerInventory;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user