diff --git a/src/renderer/api.ts b/src/renderer/api.ts index 6caa2d5..3b1fbd6 100644 --- a/src/renderer/api.ts +++ b/src/renderer/api.ts @@ -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 { + // вместо /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 { const response = await fetch(`${API_BASE_URL}/api/pranks/servers`); diff --git a/src/renderer/components/OnlinePlayersPanel.tsx b/src/renderer/components/OnlinePlayersPanel.tsx index e43e3f4..8d25f9e 100644 --- a/src/renderer/components/OnlinePlayersPanel.tsx +++ b/src/renderer/components/OnlinePlayersPanel.tsx @@ -134,13 +134,13 @@ export const OnlinePlayersPanel: React.FC = ({ ); } - if (!onlinePlayers.length) { - return ( - - Сейчас на серверах никого нет. - - ); - } + // if (!onlinePlayers.length) { + // return ( + // + // Сейчас на серверах никого нет. + // + // ); + // } const totalOnline = onlinePlayers.length; @@ -371,82 +371,119 @@ export const OnlinePlayersPanel: React.FC = ({ }, }} > - {filteredPlayers.map((p) => { - const isMe = p.username === currentUsername; + {filteredPlayers.length ? ( + filteredPlayers.map((p) => { + const isMe = p.username === currentUsername; - return ( - - - - + - {p.username} - + + + {p.username} + - {isMe && ( + {isMe && ( + + )} + + + - )} - - - - - {/* onlineSince можно потом красиво форматировать */} - - - ); - })} + + + ); + }) + ) : ( + + + Сейчас на сервере никого нет! + + + )} ); diff --git a/src/renderer/components/PlayerInventory.tsx b/src/renderer/components/PlayerInventory.tsx index cb31c22..eea1a6f 100644 --- a/src/renderer/components/PlayerInventory.tsx +++ b/src/renderer/components/PlayerInventory.tsx @@ -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; +}; + +const PlayerInventory = React.forwardRef( + ({ username, serverIp, onSellSuccess }, ref) => { const [loading, setLoading] = useState(false); const [inventoryItems, setInventoryItems] = useState( [], @@ -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 ( - + Ваш инвентарь - {error && ( @@ -223,7 +323,7 @@ export default function PlayerInventory({ )} {loading ? ( - + ) : ( <> {inventoryItems.length === 0 ? ( @@ -311,79 +411,148 @@ export default function PlayerInventory({ )} {/* Диалог для продажи предмета */} - - Продать предмет - + + + Продать предмет + + + + + + {selectedItem && ( - - - + {/* Верхняя карточка предмета */} + + - - {getItemDisplayName(selectedItem.material)} - + + + + {getItemDisplayName(selectedItem.material)} + + + + Доступно: {selectedItem.amount} + + - - Всего доступно: {selectedItem.amount} - - + {/* Поля */} - 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} /> 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 && ( - + {sellError} - + )} + + {/* Подсказка */} + + Цена указывается за весь лот! + )} - - + + + + ); -} +}) + +export default PlayerInventory; diff --git a/src/renderer/pages/Marketplace.tsx b/src/renderer/pages/Marketplace.tsx index 4417c0a..fb5ed37 100644 --- a/src/renderer/pages/Marketplace.tsx +++ b/src/renderer/pages/Marketplace.tsx @@ -1,5 +1,6 @@ // src/renderer/pages/Marketplace.tsx -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import * as React from 'react'; import { Box, Typography, @@ -11,17 +12,43 @@ import { Pagination, Tabs, Tab, + Select, + FormControl, + MenuItem, + Chip, + Divider, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, } from '@mui/material'; -import { isPlayerOnline, getPlayerServer } from '../utils/playerOnlineCheck'; -import { buyItem, fetchMarketplace, MarketplaceResponse, Server } from '../api'; -import PlayerInventory from '../components/PlayerInventory'; + +import { + buyItem, + fetchMarketplace, + MarketplaceResponse, + MarketplaceItemResponse, + Server, + fetchActiveServers, + fetchMyMarketplaceItems, + updateMarketplaceItemPrice, + cancelMarketplaceItemSale, + } from '../api'; + import PlayerInventory, { type PlayerInventoryHandle } from '../components/PlayerInventory'; import { FullScreenLoader } from '../components/FullScreenLoader'; import CustomNotification from '../components/Notifications/CustomNotification'; import type { NotificationPosition } from '../components/Notifications/CustomNotification'; -import * as React from 'react'; + +import { getPlayerServer } from '../utils/playerOnlineCheck'; import { playBuySound } from '../utils/sounds'; -import { translateServer } from '../utils/serverTranslator' +import { translateServer } from '../utils/serverTranslator'; import { isNotificationsEnabled, getNotifPositionFromSettings } from '../utils/notifications'; +import { NONAME } from 'dns'; +import GradientTextField from '../components/GradientTextField'; +import CloseIcon from '@mui/icons-material/Close'; +import IconButton from '@mui/material/IconButton'; +import type { SxProps, Theme } from '@mui/material/styles'; interface TabPanelProps { children?: React.ReactNode; @@ -31,7 +58,6 @@ interface TabPanelProps { function TabPanel(props: TabPanelProps) { const { children, value, index, ...other } = props; - return (
- {value === index && {children}} + {value === index && {children}}
); } +const GRADIENT = + 'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)'; + +const GLASS_BG = + '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)'; + +const GLASS_BORDER = '1px solid rgba(255,255,255,0.08)'; +const GLASS_SHADOW = '0 1.2vw 3.2vw rgba(0,0,0,0.55)'; + export default function Marketplace() { - const [loading, setLoading] = useState(true); const [marketLoading, setMarketLoading] = useState(false); + const [isOnline, setIsOnline] = useState(false); const [username, setUsername] = useState(''); const [playerServer, setPlayerServer] = useState(null); - const [marketItems, setMarketItems] = useState( - null, - ); + + const [marketItems, setMarketItems] = useState(null); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); + const [tabValue, setTabValue] = useState(0); + + // notifications const [notifOpen, setNotifOpen] = useState(false); const [notifMsg, setNotifMsg] = useState(''); const [notifSeverity, setNotifSeverity] = useState< @@ -68,90 +105,61 @@ export default function Marketplace() { horizontal: 'center', }); - // Функция для проверки онлайн-статуса игрока и определения сервера - const checkPlayerStatus = async () => { + // servers + const [servers, setServers] = useState([]); + const [selectedServer, setSelectedServer] = useState(null); + + // “тихий” лоадер статуса + const [statusLoading, setStatusLoading] = useState(false); + + const [myItems, setMyItems] = useState(null); + const [myLoading, setMyLoading] = useState(false); + const [myPage, setMyPage] = useState(1); + const [myTotalPages, setMyTotalPages] = useState(1); + + const [editPriceOpen, setEditPriceOpen] = useState(false); + const [editItem, setEditItem] = useState(null); + const [editPriceValue, setEditPriceValue] = useState(''); + + const [removeOpen, setRemoveOpen] = useState(false); + const [removeItem, setRemoveItem] = useState(null); + + const inventoryRef = useRef(null); + + const openEditPrice = (item: MarketplaceItemResponse) => { + setEditItem(item); + setEditPriceValue(String(item.price ?? '')); + setEditPriceOpen(true); + }; + + const openRemove = (item: MarketplaceItemResponse) => { + setRemoveItem(item); + setRemoveOpen(true); + }; + + const loadMyItems = async (serverIp: string, pageNumber: number) => { if (!username) return; try { - setLoading(true); - // Проверяем, онлайн ли игрок и получаем сервер, где он находится - const { online, server } = await getPlayerServer(username); - setIsOnline(online); - setPlayerServer(server); - - // Если игрок онлайн и на каком-то сервере, загружаем предметы рынка - if (online && server) { - await loadMarketItems(server.ip, 1); - } - } catch (error) { - console.error('Ошибка при проверке онлайн-статуса:', error); - setIsOnline(false); - setPlayerServer(null); + setMyLoading(true); + const data = await fetchMyMarketplaceItems(username, serverIp, pageNumber, 10); + setMyItems(data); + setMyPage(data.page); + setMyTotalPages(data.pages); + } catch (e) { + console.error('Ошибка при загрузке моих товаров:', e); + setMyItems(null); } finally { - setLoading(false); + setMyLoading(false); } }; - // Функция для загрузки предметов маркетплейса - const loadMarketItems = async (serverIp: string, pageNumber: number) => { - try { - setMarketLoading(true); - const marketData = await fetchMarketplace(serverIp, pageNumber, 10); // 10 предметов на страницу - setMarketItems(marketData); - setPage(marketData.page); - setTotalPages(marketData.pages); - } catch (error) { - console.error('Ошибка при загрузке предметов рынка:', error); - setMarketItems(null); - } finally { - setMarketLoading(false); - } - }; - - // Обработчик смены страницы - const handlePageChange = ( - _event: React.ChangeEvent, - newPage: number, - ) => { - if (playerServer) { - loadMarketItems(playerServer.ip, newPage); - } - }; - - // Обработчик смены вкладок - const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { - setTabValue(newValue); - }; - - // Обновляем функцию handleBuyItem в Marketplace.tsx - const handleBuyItem = async (itemId: string) => { - try { - if (username) { - const result = await buyItem(username, itemId); - - playBuySound(); - - showNotification( - result.message || - 'Предмет успешно куплен! Он будет добавлен в ваш инвентарь.', - 'success', - { vertical: 'bottom', horizontal: 'left' }, - ); - - // Обновляем список предметов - if (playerServer) { - loadMarketItems(playerServer.ip, page); - } - } - } catch (error) { - console.error('Ошибка при покупке предмета:', error); - showNotification( - error instanceof Error ? error.message : 'Ошибка при покупке предмета', - 'error', - { vertical: 'bottom', horizontal: 'left' }, - ); - } - }; + useEffect(() => { + if (tabValue !== 2) return; + if (!selectedServer) return; + loadMyItems(selectedServer.ip, 1); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tabValue, selectedServer, username]); const showNotification = ( message: React.ReactNode, @@ -165,328 +173,388 @@ export default function Marketplace() { setNotifOpen(true); }; - // Получаем имя пользователя из localStorage при монтировании компонента + // Получаем username из localStorage useEffect(() => { const savedConfig = localStorage.getItem('launcher_config'); if (savedConfig) { const config = JSON.parse(savedConfig); - if (config.username) { - setUsername(config.username); - } + if (config.username) setUsername(config.username); } }, []); - // Проверяем статус при изменении username + // load servers useEffect(() => { - if (username) { - checkPlayerStatus(); + const loadServers = async () => { + try { + const active = await fetchActiveServers(); + setServers(active); + if (active.length && !selectedServer) setSelectedServer(active[0]); + } catch (e) { + console.error('Не удалось загрузить список серверов:', e); + setServers([]); + setSelectedServer(null); + } + }; + + loadServers(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // check player status + const checkPlayerStatus = async () => { + if (!username) return; + + try { + setStatusLoading(true); + const { online, server } = await getPlayerServer(username); + setIsOnline(online); + setPlayerServer(server); + } catch (error) { + console.error('Ошибка при проверке онлайн-статуса:', error); + setIsOnline(false); + setPlayerServer(null); + } finally { + setStatusLoading(false); } + }; + + const handleSavePrice = async () => { + if (!username || !selectedServer || !editItem) return; + + const parsed = Number(editPriceValue); + if (!Number.isFinite(parsed) || parsed <= 0) { + showNotification('Цена должна быть числом больше 0', 'warning', { + vertical: 'bottom', + horizontal: 'left', + }); + return; + } + + try { + await updateMarketplaceItemPrice(username, editItem.id, parsed); + + showNotification('Цена обновлена', 'success', { + vertical: 'bottom', + horizontal: 'left', + }); + + setEditPriceOpen(false); + setEditItem(null); + + // обновляем “Мои товары” и общий рынок + await loadMyItems(selectedServer.ip, myPage); + await loadMarketItems(selectedServer.ip, page); + } catch (e) { + showNotification( + e instanceof Error ? e.message : 'Ошибка при изменении цены', + 'error', + { vertical: 'bottom', horizontal: 'left' }, + ); + } + }; + + const handleRemoveItem = async () => { + if (!username || !selectedServer || !removeItem) return; + + try { + await cancelMarketplaceItemSale(username, removeItem.id); + + showNotification('Товар снят с продажи', 'success', { + vertical: 'bottom', + horizontal: 'left', + }); + + setRemoveOpen(false); + setRemoveItem(null); + + // обновляем “Мои товары” и общий рынок + await loadMyItems(selectedServer.ip, myPage); + await loadMarketItems(selectedServer.ip, page); + } catch (e) { + showNotification( + e instanceof Error ? e.message : 'Ошибка при снятии товара', + 'error', + { vertical: 'bottom', horizontal: 'left' }, + ); + } + }; + + useEffect(() => { + if (username) checkPlayerStatus(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [username]); - // Показываем loader во время проверки - if (loading) { - return ( - - setNotifOpen(false)} - autoHideDuration={2500} - /> - - - ); - } + // market loader + const loadMarketItems = async (serverIp: string, pageNumber: number) => { + try { + setMarketLoading(true); + const marketData = await fetchMarketplace(serverIp, pageNumber, 10); + setMarketItems(marketData); + setPage(marketData.page); + setTotalPages(marketData.pages); + } catch (error) { + console.error('Ошибка при загрузке предметов рынка:', error); + setMarketItems(null); + } finally { + setMarketLoading(false); + } + }; - // Если игрок не онлайн - if (!isOnline) { - return ( - - - Доступ к рынку ограничен - - - Для доступа к рынку вам необходимо находиться на одном из серверов - игры. - - - Зайдите на любой сервер и обновите страницу. - - - + /> + ); + } + + if (isOnline && playerServer) { + return ( + + ); + } + + return ( + ); - } + }, [statusLoading, isOnline, playerServer]); return ( - - - - Рынок сервера{' '} - - - {translateServer(playerServer?.name ?? '')} - - - - {/* Вкладки */} - - - - - - - - - {/* Содержимое вкладки "Товары" */} - - {marketLoading ? ( - - - - ) : !marketItems || marketItems.items.length === 0 ? ( - - - На данный момент на рынке нет предметов. - - - - ) : ( - <> - - {marketItems.items.map((item) => ( - - - - - - {item.display_name || - item.material - .replace(/_/g, ' ') - .toLowerCase() - .replace(/\b\w/g, (l) => l.toUpperCase())} - - - Количество: {item.amount} - - - Цена: {item.price} монет - - - Продавец: {item.seller_name} - - - - - - ))} - - - {totalPages > 1 && ( - - - - )} - - )} - - - {/* Содержимое вкладки "Мой инвентарь" */} - - {playerServer && username ? ( - { - // После успешной продажи, обновляем список товаров - if (playerServer) { - loadMarketItems(playerServer.ip, 1); - } - - // Показываем уведомление - showNotification('Предмет успешно выставлен на продажу!', 'success', { - vertical: 'bottom', - horizontal: 'left', - }); - }} - /> - ) : ( - - Не удалось загрузить инвентарь. - - )} - + setNotifOpen(false)} autoHideDuration={2500} /> + + {/* EDIT PRICE */} + setEditPriceOpen(false)} + fullWidth + maxWidth="xs" + PaperProps={{ sx: GLASS_PAPER_SX }} + > + + Изменить цену + setEditPriceOpen(false)} sx={CLOSE_BTN_SX}> + + + + + + + Укажи новую цену в монетах + + + setEditPriceValue(e.target.value)} + inputProps={{ min: 1 }} + sx={PRICE_FIELD_SX} + /> + + {editItem && ( + + Текущая цена: {editItem.price} + + )} + + + + + + + + + + {/* DELETE */} + setRemoveOpen(false)} + fullWidth + maxWidth="xs" + PaperProps={{ sx: GLASS_PAPER_SX }} + > + + Снять товар с продажи? + setRemoveOpen(false)} sx={CLOSE_BTN_SX}> + + + + + + + Товар исчезнет с рынка и вернется вам в инвентарь! + + + {removeItem && ( + + + {removeItem.display_name || removeItem.material} + + + Цена: {removeItem.price} монет + + + )} + + + + + + + + + + {/* HEADER (glass) */} + + + + + Рынок сервера + + + {/* сервер селект (как в Settings/Shop) */} + + + + + {statusChip} + + + + + + + + {/* TABS */} + + + {['Товары', 'Мой инвентарь', 'Мои товары'].map((label) => ( + + ))} + + + + + {/* CONTENT */} + + {marketLoading ? ( + + + + ) : !marketItems || marketItems.items.length === 0 ? ( + + + На данный момент на рынке нет предметов. + + + + + ) : ( + <> + + {marketItems.items.map((item) => { + const title = + item.display_name || + item.material + .replace(/_/g, ' ') + .toLowerCase() + .replace(/\b\w/g, (l) => l.toUpperCase()); + + return ( + + + {/* top glow */} + + + + + + + + + + + + {title} + + + + Количество: {item.amount} + + + Цена: {item.price} монет + + + + Продавец: {item.seller_name} + + + + + + + + ); + })} + + + {totalPages > 1 && ( + + + + )} + + )} + + + {/* "Мой инвентарь" */} + + + {playerServer && username ? ( + { + if (selectedServer) loadMarketItems(selectedServer.ip, 1); + showNotification('Предмет успешно выставлен на продажу!', 'success', { + vertical: 'bottom', + horizontal: 'left', + }); + }} + /> + ) : ( + + Не удалось загрузить инвентарь. + + )} + + + + {/* "Мои товары" — пока заглушка (как было, логики у тебя в файле нет) */} + + {myLoading ? ( + + + + ) : !myItems || myItems.items.length === 0 ? ( + + + У вас нет выставленных товаров. + + + + + ) : ( + <> + + {myItems.items.map((item) => { + const title = + item.display_name || + item.material + .replace(/_/g, ' ') + .toLowerCase() + .replace(/\b\w/g, (l) => l.toUpperCase()); + + return ( + + + + + + + + + + + + + + {title} + + + + Количество: {item.amount} + + + Цена: {item.price} монет + + + + Сервер: {translateServer(selectedServer?.name ?? '')} + + + + + + + + + + + + ); + })} + + + {myTotalPages > 1 && ( + + selectedServer && loadMyItems(selectedServer.ip, p)} + sx={{ + '& .MuiPaginationItem-root': { + color: 'rgba(255,255,255,0.85)', + fontFamily: 'Benzin-Bold', + borderRadius: '999px', + border: '1px solid rgba(255,255,255,0.12)', + background: 'rgba(255,255,255,0.05)', + '&:hover': { background: 'rgba(255,255,255,0.08)' }, + }, + '& .MuiPaginationItem-root.Mui-selected': { + background: GRADIENT, + border: '1px solid rgba(255,255,255,0.14)', + color: '#fff', + }, + }} + /> + + )} + + )} + ); }