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 {
|
export interface MarketplaceResponse {
|
||||||
items: [
|
items: MarketplaceItemResponse[];
|
||||||
{
|
|
||||||
_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;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
total: number;
|
total: number;
|
||||||
page: number;
|
page: number;
|
||||||
pages: number;
|
pages: number;
|
||||||
@ -1033,6 +1014,57 @@ export async function RequestPlayerInventory(
|
|||||||
return await response.json();
|
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(
|
export async function buyItem(
|
||||||
buyer_username: string,
|
buyer_username: string,
|
||||||
item_id: string,
|
item_id: string,
|
||||||
@ -1175,6 +1207,40 @@ export async function fetchMarketplace(
|
|||||||
return await response.json();
|
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[]> {
|
export async function fetchActiveServers(): Promise<Server[]> {
|
||||||
const response = await fetch(`${API_BASE_URL}/api/pranks/servers`);
|
const response = await fetch(`${API_BASE_URL}/api/pranks/servers`);
|
||||||
|
|||||||
@ -134,13 +134,13 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!onlinePlayers.length) {
|
// if (!onlinePlayers.length) {
|
||||||
return (
|
// return (
|
||||||
<Typography sx={{ mt: 2, color: 'rgba(255,255,255,0.75)', fontWeight: 700 }}>
|
// <Typography sx={{ mt: 2, color: 'rgba(255,255,255,0.75)', fontWeight: 700 }}>
|
||||||
Сейчас на серверах никого нет.
|
// Сейчас на серверах никого нет.
|
||||||
</Typography>
|
// </Typography>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
const totalOnline = onlinePlayers.length;
|
const totalOnline = onlinePlayers.length;
|
||||||
|
|
||||||
@ -371,82 +371,119 @@ export const OnlinePlayersPanel: React.FC<OnlinePlayersPanelProps> = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{filteredPlayers.map((p) => {
|
{filteredPlayers.length ? (
|
||||||
const isMe = p.username === currentUsername;
|
filteredPlayers.map((p) => {
|
||||||
|
const isMe = p.username === currentUsername;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
key={p.uuid}
|
key={p.uuid}
|
||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
px: '1.1vw',
|
px: '1.1vw',
|
||||||
py: '0.75vw',
|
py: '0.75vw',
|
||||||
borderRadius: '1.1vw',
|
borderRadius: '1.1vw',
|
||||||
background: 'rgba(255,255,255,0.04)',
|
background: 'rgba(255,255,255,0.04)',
|
||||||
border: '1px solid rgba(255,255,255,0.08)',
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '1vw',
|
gap: '1vw',
|
||||||
transition: 'transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease',
|
transition:
|
||||||
'&:hover': {
|
'transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease',
|
||||||
transform: 'scale(1.01)',
|
'&:hover': {
|
||||||
borderColor: 'rgba(242,113,33,0.35)',
|
transform: 'scale(1.01)',
|
||||||
boxShadow: '0 0.8vw 2.4vw rgba(0,0,0,0.45)',
|
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} />
|
<Box
|
||||||
<Typography
|
|
||||||
sx={{
|
sx={{
|
||||||
fontFamily: 'Benzin-Bold',
|
display: 'flex',
|
||||||
color: 'rgba(255,255,255,0.92)',
|
alignItems: 'center',
|
||||||
overflow: 'hidden',
|
gap: '0.8vw',
|
||||||
textOverflow: 'ellipsis',
|
minWidth: 0,
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{p.username}
|
<HeadAvatar skinUrl={skinMap[p.uuid]} size={26} />
|
||||||
</Typography>
|
<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
|
<Chip
|
||||||
label="Вы"
|
label={translateServer(p.serverName)}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
height: '1.55rem',
|
fontFamily: 'Benzin-Bold',
|
||||||
fontSize: '0.72rem',
|
fontSize: '0.72rem',
|
||||||
fontWeight: 900,
|
|
||||||
color: 'white',
|
|
||||||
borderRadius: '999px',
|
borderRadius: '999px',
|
||||||
backgroundImage: GRADIENT,
|
color: 'rgba(255,255,255,0.88)',
|
||||||
boxShadow: '0 10px 22px rgba(0,0,0,0.45)',
|
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>
|
</Paper>
|
||||||
|
);
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: '0.6vw', flexShrink: 0 }}>
|
})
|
||||||
<Chip
|
) : (
|
||||||
label={translateServer(p.serverName)}
|
<Box
|
||||||
size="small"
|
sx={{
|
||||||
sx={{
|
py: '1.4vw',
|
||||||
fontFamily: 'Benzin-Bold',
|
px: '1.1vw',
|
||||||
fontSize: '0.72rem',
|
borderRadius: '1.1vw',
|
||||||
borderRadius: '999px',
|
background: 'rgba(255,255,255,0.03)',
|
||||||
color: 'rgba(255,255,255,0.88)',
|
border: '1px dashed rgba(255,255,255,0.14)',
|
||||||
background:
|
textAlign: 'center',
|
||||||
'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)',
|
<Typography
|
||||||
}}
|
sx={{
|
||||||
/>
|
fontFamily: 'Benzin-Bold',
|
||||||
{/* onlineSince можно потом красиво форматировать */}
|
fontSize: '0.95vw',
|
||||||
</Box>
|
color: 'rgba(255,255,255,0.78)',
|
||||||
</Paper>
|
}}
|
||||||
);
|
>
|
||||||
})}
|
Сейчас на сервере никого нет!
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// src/renderer/components/PlayerInventory.tsx
|
// src/renderer/components/PlayerInventory.tsx
|
||||||
import { useEffect, useState } from 'react';
|
import React, { useEffect, useImperativeHandle, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
@ -22,6 +22,118 @@ import {
|
|||||||
PlayerInventoryItem,
|
PlayerInventoryItem,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { FullScreenLoader } from './FullScreenLoader';
|
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 {
|
interface PlayerInventoryProps {
|
||||||
username: string;
|
username: string;
|
||||||
@ -29,11 +141,12 @@ interface PlayerInventoryProps {
|
|||||||
onSellSuccess?: () => void; // Callback для обновления маркетплейса после продажи
|
onSellSuccess?: () => void; // Callback для обновления маркетплейса после продажи
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PlayerInventory({
|
export type PlayerInventoryHandle = {
|
||||||
username,
|
refresh: () => Promise<void>;
|
||||||
serverIp,
|
};
|
||||||
onSellSuccess,
|
|
||||||
}: PlayerInventoryProps) {
|
const PlayerInventory = React.forwardRef<PlayerInventoryHandle, PlayerInventoryProps>(
|
||||||
|
({ username, serverIp, onSellSuccess }, ref) => {
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [inventoryItems, setInventoryItems] = useState<PlayerInventoryItem[]>(
|
const [inventoryItems, setInventoryItems] = useState<PlayerInventoryItem[]>(
|
||||||
[],
|
[],
|
||||||
@ -96,6 +209,12 @@ export default function PlayerInventory({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
refresh: async () => {
|
||||||
|
await fetchPlayerInventory();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
// Открываем диалог для продажи предмета
|
// Открываем диалог для продажи предмета
|
||||||
const handleOpenSellDialog = (item: PlayerInventoryItem) => {
|
const handleOpenSellDialog = (item: PlayerInventoryItem) => {
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
@ -183,7 +302,7 @@ export default function PlayerInventory({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ mt: '1vw' }}>
|
<Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -195,25 +314,6 @@ export default function PlayerInventory({
|
|||||||
<Typography variant="h5" color="white">
|
<Typography variant="h5" color="white">
|
||||||
Ваш инвентарь
|
Ваш инвентарь
|
||||||
</Typography>
|
</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>
|
</Box>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@ -223,7 +323,7 @@ export default function PlayerInventory({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<FullScreenLoader fullScreen={false} />
|
<FullScreenLoader fullScreen={false} message="Загрузка инвентаря..." />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{inventoryItems.length === 0 ? (
|
{inventoryItems.length === 0 ? (
|
||||||
@ -311,79 +411,148 @@ export default function PlayerInventory({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Диалог для продажи предмета */}
|
{/* Диалог для продажи предмета */}
|
||||||
<Dialog open={sellDialogOpen} onClose={handleCloseSellDialog}>
|
<Dialog
|
||||||
<DialogTitle>Продать предмет</DialogTitle>
|
open={sellDialogOpen}
|
||||||
<DialogContent>
|
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 && (
|
{selectedItem && (
|
||||||
<Box sx={{ mt: 1 }}>
|
<Box sx={{ mt: 0.5 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
{/* Верхняя карточка предмета */}
|
||||||
<CardMedia
|
<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"
|
component="img"
|
||||||
sx={{
|
src={`https://cdn.minecraft.popa-popa.ru/textures/${selectedItem.material.toLowerCase()}.png`}
|
||||||
width: 50,
|
|
||||||
height: 50,
|
|
||||||
objectFit: 'contain',
|
|
||||||
mr: 2,
|
|
||||||
}}
|
|
||||||
image={`https://cdn.minecraft.popa-popa.ru/textures/${selectedItem.material.toLowerCase()}.png`}
|
|
||||||
alt={selectedItem.material}
|
alt={selectedItem.material}
|
||||||
|
draggable={false}
|
||||||
|
style={{
|
||||||
|
width: 54,
|
||||||
|
height: 54,
|
||||||
|
objectFit: 'contain',
|
||||||
|
imageRendering: 'pixelated',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography variant="h6">
|
|
||||||
{getItemDisplayName(selectedItem.material)}
|
<Box sx={{ minWidth: 0 }}>
|
||||||
</Typography>
|
<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>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="body2" gutterBottom>
|
{/* Поля */}
|
||||||
Всего доступно: {selectedItem.amount}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Количество"
|
label="Количество"
|
||||||
type="number"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="dense"
|
|
||||||
value={amount}
|
value={amount}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setAmount(
|
const v = Number(e.target.value);
|
||||||
Math.min(
|
const safe = Number.isFinite(v) ? v : 0;
|
||||||
parseInt(e.target.value) || 0,
|
setAmount(Math.min(Math.max(1, safe), selectedItem.amount));
|
||||||
selectedItem.amount,
|
}}
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
inputProps={{ min: 1, max: selectedItem.amount }}
|
inputProps={{ min: 1, max: selectedItem.amount }}
|
||||||
|
sx={INPUT_SX}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Цена (за всё)"
|
label="Цена (за всё)"
|
||||||
type="number"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="dense"
|
|
||||||
value={price}
|
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 }}
|
inputProps={{ min: 1 }}
|
||||||
|
sx={INPUT_SX}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{sellError && (
|
{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}
|
{sellError}
|
||||||
</Alert>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Подсказка */}
|
||||||
|
<Typography sx={{ mt: 1.1, color: 'rgba(255,255,255,0.60)', fontWeight: 700 }}>
|
||||||
|
Цена указывается за весь лот!
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={handleCloseSellDialog}>Отмена</Button>
|
<DialogActions sx={{ p: '1.2vw' }}>
|
||||||
|
<Button onClick={handleCloseSellDialog} sx={SECONDARY_BTN_SX}>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSellItem}
|
onClick={handleSellItem}
|
||||||
variant="contained"
|
disableRipple
|
||||||
color="primary"
|
|
||||||
disabled={sellLoading}
|
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>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
|
|
||||||
|
export default PlayerInventory;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user