add info for item
This commit is contained in:
@ -174,6 +174,7 @@ export interface MarketplaceResponse {
|
||||
export interface MarketplaceItemResponse {
|
||||
_id: string;
|
||||
id: string;
|
||||
description: string;
|
||||
material: string;
|
||||
amount: number;
|
||||
price: number;
|
||||
@ -186,6 +187,10 @@ export interface MarketplaceItemResponse {
|
||||
slot: number;
|
||||
material: string;
|
||||
amount: number;
|
||||
meta?: {
|
||||
enchants?: Record<string, number>;
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@ -50,6 +50,8 @@ import CloseIcon from '@mui/icons-material/Close';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { getWsBaseUrl } from '../realtime/wsBase';
|
||||
import { formatEnchants } from '../utils/itemTranslator';
|
||||
import { translateMetaKey, formatMetaValue } from '../utils/itemTranslator';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
@ -133,15 +135,31 @@ export default function Marketplace() {
|
||||
const [editPriceOpen, setEditPriceOpen] = useState(false);
|
||||
const [editItem, setEditItem] = useState<MarketplaceItemResponse | null>(null);
|
||||
const [editPriceValue, setEditPriceValue] = useState<string>('');
|
||||
const [description, setDescription] = useState('');
|
||||
|
||||
const [removeOpen, setRemoveOpen] = useState(false);
|
||||
const [removeItem, setRemoveItem] = useState<MarketplaceItemResponse | null>(null);
|
||||
|
||||
const inventoryRef = useRef<PlayerInventoryHandle | null>(null);
|
||||
|
||||
const [metaDialogOpen, setMetaDialogOpen] = useState(false);
|
||||
const [metaItem, setMetaItem] = useState<MarketplaceItemResponse | null>(null);
|
||||
const [metaSearch, setMetaSearch] = useState('');
|
||||
|
||||
const openMetaDialog = (item: MarketplaceItemResponse) => {
|
||||
setMetaItem(item);
|
||||
setMetaDialogOpen(true);
|
||||
};
|
||||
|
||||
const closeMetaDialog = () => {
|
||||
setMetaDialogOpen(false);
|
||||
setMetaItem(null);
|
||||
};
|
||||
|
||||
const openEditPrice = (item: MarketplaceItemResponse) => {
|
||||
setEditItem(item);
|
||||
setEditPriceValue(String(item.price ?? ''));
|
||||
setDescription(item.description ?? '');
|
||||
setEditPriceOpen(true);
|
||||
};
|
||||
|
||||
@ -319,14 +337,26 @@ export default function Marketplace() {
|
||||
const handleSavePrice = async () => {
|
||||
if (!username || !selectedServer || !editItem) return;
|
||||
|
||||
const parsed = Number(editPriceValue);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
const price = Number(editPriceValue);
|
||||
if (!price || price <= 0) {
|
||||
showNotification('Цена должна быть числом больше 0', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// ДОДЕЛАТЬ - ИЗМЕНЕНИЕ ОПИСАНИЯ
|
||||
// const payload: any = {
|
||||
// itemId: editItem.id,
|
||||
// price,
|
||||
// };
|
||||
|
||||
// if (description.trim() !== '') {
|
||||
// payload.description = description.trim();
|
||||
// } else {
|
||||
// payload.description = null;
|
||||
// }
|
||||
|
||||
try {
|
||||
await updateMarketplaceItemPrice(username, editItem.id, parsed);
|
||||
await updateMarketplaceItemPrice(username, editItem.id, price);
|
||||
|
||||
showNotification('Цена обновлена', 'success');
|
||||
|
||||
@ -413,6 +443,20 @@ export default function Marketplace() {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const metaKeyMatchesSearch = (key: string, search: string) => {
|
||||
if (!search) return true;
|
||||
|
||||
const s = search.toLowerCase();
|
||||
|
||||
const original = key.toLowerCase();
|
||||
const translated = translateMetaKey(key).toLowerCase();
|
||||
|
||||
return (
|
||||
original.includes(s) ||
|
||||
translated.includes(s)
|
||||
);
|
||||
};
|
||||
|
||||
const handleBuyItem = async (itemId: string) => {
|
||||
try {
|
||||
if (!username) return;
|
||||
@ -502,10 +546,10 @@ export default function Marketplace() {
|
||||
color: 'rgba(255,255,255,0.95)',
|
||||
},
|
||||
|
||||
transition: 'transform 0.18s ease, filter 0.18s ease, border-color 0.18s ease',
|
||||
transition: 'transform 0.18s ease, filter 0.18s ease, border-color 0.25s ease, box-shadow 0.25s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.01)',
|
||||
borderColor: 'rgba(255,255,255,0.14)',
|
||||
borderColor: 'rgba(200, 33, 242, 0.35)',
|
||||
boxShadow: '0 1.2vw 3.2vw rgba(53, 3, 66, 0.45)',
|
||||
},
|
||||
'&.Mui-focused': {
|
||||
borderColor: 'rgba(255,255,255,0.18)',
|
||||
@ -579,6 +623,26 @@ export default function Marketplace() {
|
||||
}
|
||||
};
|
||||
|
||||
const getItemMeta = (item: MarketplaceItemResponse) => {
|
||||
return item.item_data?.meta ?? null;
|
||||
};
|
||||
|
||||
const extractEnchants = (meta: Record<string, any>) => {
|
||||
return meta?.enchants && typeof meta.enchants === 'object'
|
||||
? meta.enchants
|
||||
: null;
|
||||
};
|
||||
|
||||
const hasItemMeta = (item: MarketplaceItemResponse) => {
|
||||
const meta = item.item_data;
|
||||
if (!meta) return false;
|
||||
|
||||
// базовые поля, которые НЕ считаем метой
|
||||
const baseKeys = ['slot', 'material', 'amount'];
|
||||
|
||||
return Object.keys(meta).some((key) => !baseKeys.includes(key));
|
||||
};
|
||||
|
||||
|
||||
const statusChip = useMemo(() => {
|
||||
if (statusLoading) {
|
||||
@ -676,7 +740,6 @@ export default function Marketplace() {
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Новая цена"
|
||||
value={editPriceValue}
|
||||
@ -685,6 +748,15 @@ export default function Marketplace() {
|
||||
sx={PRICE_FIELD_SX}
|
||||
/>
|
||||
|
||||
{/* <TextField
|
||||
fullWidth
|
||||
label="Описание (необязательно)"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
inputProps={{ min: 1 }}
|
||||
sx={PRICE_FIELD_SX}
|
||||
/> */}
|
||||
|
||||
{editItem && (
|
||||
<Box sx={{ mt: 1.2, color: 'rgba(255,255,255,0.62)', fontWeight: 700 }}>
|
||||
Текущая цена: <b style={{ color: 'rgba(255,255,255,0.9)' }}>{editItem.price}</b>
|
||||
@ -802,6 +874,180 @@ export default function Marketplace() {
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* FULL INFO ITEM */}
|
||||
<Dialog
|
||||
open={metaDialogOpen}
|
||||
onClose={closeMetaDialog}
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
PaperProps={{ sx: GLASS_PAPER_SX }}
|
||||
>
|
||||
<DialogTitle sx={DIALOG_TITLE_SX}>
|
||||
Информация о предмете
|
||||
<IconButton onClick={closeMetaDialog} sx={CLOSE_BTN_SX}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent dividers sx={DIALOG_DIVIDERS_SX}>
|
||||
{metaItem && (() => {
|
||||
const meta = getItemMeta(metaItem);
|
||||
if (!meta) return null;
|
||||
|
||||
const enchants = formatEnchants(meta.enchants);
|
||||
|
||||
const filteredMeta = Object.entries(meta).filter(([key]) =>
|
||||
key !== 'enchants' &&
|
||||
metaKeyMatchesSearch(key, metaSearch)
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1vw' }}>
|
||||
|
||||
{/* IMAGE + TITLE */}
|
||||
<Box sx={{ display: 'flex', gap: '1vw', alignItems: 'center' }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '1vw',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
background: 'rgba(0,0,0,0.35)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`https://cdn.minecraft.popa-popa.ru/textures/${metaItem.material.toLowerCase()}.png`}
|
||||
alt={metaItem.material}
|
||||
style={{ width: '70%', imageRendering: 'pixelated' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography sx={{ fontFamily: 'Benzin-Bold', fontSize: '1.2rem' }}>
|
||||
{metaItem.display_name || metaItem.material}
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.7)', fontSize: '0.9rem' }}>
|
||||
Количество: {metaItem.amount} · Цена: {metaItem.price}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{mt: '0.7vw'}} />
|
||||
|
||||
{/* DESCRIPTION (optional) */}
|
||||
{metaItem.description && (
|
||||
<Typography>
|
||||
Описание
|
||||
<Box
|
||||
sx={{
|
||||
mt: 0.6,
|
||||
p: '0.8vw',
|
||||
borderRadius: '0.8vw',
|
||||
background: 'rgba(255,255,255,0.06)',
|
||||
border: '1px solid rgba(255,255,255,0.10)',
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.85)', fontSize: '0.9rem' }}>
|
||||
{metaItem.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{mt: '2vw'}} />
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* SEARCH */}
|
||||
<TextField
|
||||
placeholder="Поиск по метаданным..."
|
||||
value={metaSearch}
|
||||
onChange={(e) => setMetaSearch(e.target.value)}
|
||||
fullWidth
|
||||
sx={PRICE_FIELD_SX}
|
||||
/>
|
||||
|
||||
<Divider sx={{mt: '0.7vw'}} />
|
||||
|
||||
{/* ENCHANTS */}
|
||||
{enchants && (
|
||||
<>
|
||||
{enchants.length > 0 &&
|
||||
<Typography sx={{ fontFamily: 'Benzin-Bold', fontSize: '1rem' }}>
|
||||
Зачарования
|
||||
</Typography>
|
||||
}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: '0.6vw' }}>
|
||||
{enchants.map((e) => (
|
||||
<Chip
|
||||
key={e.label}
|
||||
label={`${e.label} ${e.level}`}
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
background: 'rgba(156,255,198,0.15)',
|
||||
border: '1px solid rgba(156,255,198,0.25)',
|
||||
color: 'rgba(156,255,198,0.95)',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* OTHER META */}
|
||||
{filteredMeta.length > 0 && (
|
||||
<>
|
||||
<Typography sx={{ fontFamily: 'Benzin-Bold', fontSize: '1rem' }}>
|
||||
Свойства
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '0.4vw' }}>
|
||||
{filteredMeta.map(([key, value]) => (
|
||||
<Box
|
||||
key={key}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: '1vw',
|
||||
fontSize: '0.85rem',
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
borderRadius: '0.6vw',
|
||||
px: '0.8vw',
|
||||
py: '0.4vw',
|
||||
}}
|
||||
>
|
||||
<span style={{ opacity: 0.7 }}>{translateMetaKey(key)}</span>
|
||||
<span style={{ fontWeight: 600 }}>{formatMetaValue(value)}</span>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})()}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ p: '1.2vw' }}>
|
||||
<Button
|
||||
onClick={closeMetaDialog}
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
color: '#fff',
|
||||
borderRadius: '999px',
|
||||
px: '1.6vw',
|
||||
py: '0.6vw',
|
||||
background: GRADIENT,
|
||||
}}
|
||||
>
|
||||
Закрыть
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* HEADER (glass) */}
|
||||
<Box
|
||||
sx={{
|
||||
@ -1114,6 +1360,7 @@ export default function Marketplace() {
|
||||
<Box sx={{ position: 'relative', p: '1.0vw', pb: 0 }}>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative', // 👈 важно
|
||||
borderRadius: '1.4vw',
|
||||
overflow: 'hidden',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
@ -1124,6 +1371,36 @@ export default function Marketplace() {
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{/* INFO BUTTON */}
|
||||
{hasItemMeta(item) && (
|
||||
<IconButton
|
||||
onClick={() => openMetaDialog(item)}
|
||||
size="small"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '0.5vw',
|
||||
right: '0.5vw',
|
||||
width: '1.8vw',
|
||||
height: '1.8vw',
|
||||
borderRadius: '50%',
|
||||
zIndex: 2,
|
||||
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
background: 'rgba(0,0,0,0.45)',
|
||||
border: '1px solid rgba(255,255,255,0.18)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
|
||||
'&:hover': {
|
||||
background: 'rgba(255,255,255,0.15)',
|
||||
transform: 'scale(1.05)',
|
||||
},
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
>
|
||||
i
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<CardMedia
|
||||
component="img"
|
||||
sx={{
|
||||
@ -1252,11 +1529,11 @@ export default function Marketplace() {
|
||||
p: '1.6vw',
|
||||
}}
|
||||
>
|
||||
{playerServer && username ? (
|
||||
{ username ? (
|
||||
<PlayerInventory
|
||||
ref={inventoryRef}
|
||||
username={username}
|
||||
serverIp={playerServer.ip}
|
||||
username={'Yana'}
|
||||
serverIp={'minecraft.survival.popa-popa.ru'}
|
||||
onSellSuccess={() => {
|
||||
if (selectedServer) loadMarketItems(selectedServer.ip, 1);
|
||||
showNotification('Предмет успешно выставлен на продажу!', 'success');
|
||||
@ -1270,7 +1547,6 @@ export default function Marketplace() {
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
{/* "Мои товары" — пока заглушка (как было, логики у тебя в файле нет) */}
|
||||
<TabPanel value={tabValue} index={2}>
|
||||
{myLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: '6vh' }}>
|
||||
@ -1491,3 +1767,41 @@ export default function Marketplace() {
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemMetaTooltip({ meta }: { meta: Record<string, any> }) {
|
||||
return (
|
||||
<Box sx={{ minWidth: 220 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '0.85rem',
|
||||
mb: 0.6,
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
Информация о предмете
|
||||
</Typography>
|
||||
|
||||
{Object.entries(meta).map(([key, value]) => (
|
||||
<Box
|
||||
key={key}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: '0.8vw',
|
||||
fontSize: '0.75rem',
|
||||
color: 'rgba(255,255,255,0.75)',
|
||||
}}
|
||||
>
|
||||
<span>{key}</span>
|
||||
<span style={{ opacity: 0.9 }}>
|
||||
{typeof value === 'object'
|
||||
? JSON.stringify(value)
|
||||
: String(value)}
|
||||
</span>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
106
src/renderer/utils/itemTranslator.ts
Normal file
106
src/renderer/utils/itemTranslator.ts
Normal file
@ -0,0 +1,106 @@
|
||||
// utils/itemTranslator.ts
|
||||
|
||||
/* ----------------------------- */
|
||||
/* ENCHANT TRANSLATIONS */
|
||||
/* ----------------------------- */
|
||||
|
||||
export const ENCHANT_TRANSLATIONS: Record<string, string> = {
|
||||
sharpness: 'Острота',
|
||||
smite: 'Небесная кара',
|
||||
bane_of_arthropods: 'Бич членистоногих',
|
||||
efficiency: 'Эффективность',
|
||||
unbreaking: 'Прочность',
|
||||
fortune: 'Удача',
|
||||
silk_touch: 'Шёлковое касание',
|
||||
power: 'Сила',
|
||||
punch: 'Отдача',
|
||||
flame: 'Огонь',
|
||||
infinity: 'Бесконечность',
|
||||
protection: 'Защита',
|
||||
fire_protection: 'Огнестойкость',
|
||||
blast_protection: 'Взрывоустойчивость',
|
||||
projectile_protection: 'Защита от снарядов',
|
||||
feather_falling: 'Невесомость',
|
||||
respiration: 'Подводное дыхание',
|
||||
aqua_affinity: 'Подводник',
|
||||
thorns: 'Шипы',
|
||||
depth_strider: 'Подводная ходьба',
|
||||
frost_walker: 'Ледоход',
|
||||
mending: 'Починка',
|
||||
binding_curse: 'Проклятие несъёмности',
|
||||
vanishing_curse: 'Проклятие утраты',
|
||||
looting: 'Добыча',
|
||||
sweeping: 'Разящий клинок',
|
||||
fire_aspect: 'Заговор огня',
|
||||
knockback: 'Отдача',
|
||||
luck_of_the_sea: 'Морская удача',
|
||||
lure: 'Приманка',
|
||||
};
|
||||
|
||||
/* ----------------------------- */
|
||||
/* GENERIC META TRANSLATIONS */
|
||||
/* ----------------------------- */
|
||||
|
||||
export const META_TRANSLATIONS: Record<string, string> = {
|
||||
durability: 'Прочность',
|
||||
max_durability: 'Максимальная прочность',
|
||||
custom_model_data: 'Кастомная модель',
|
||||
unbreakable: 'Неразрушимый',
|
||||
repair_cost: 'Стоимость починки',
|
||||
hide_flags: 'Скрытые флаги',
|
||||
rarity: 'Редкость',
|
||||
damage: 'Урон',
|
||||
attack_speed: 'Скорость атаки',
|
||||
armor: 'Броня',
|
||||
armor_toughness: 'Твёрдость брони',
|
||||
knockback_resistance: 'Сопротивление отталкиванию',
|
||||
glowing: 'Подсветка',
|
||||
};
|
||||
|
||||
/* ----------------------------- */
|
||||
/* FORMATTERS */
|
||||
/* ----------------------------- */
|
||||
|
||||
export function translateEnchant(key: string): string {
|
||||
return ENCHANT_TRANSLATIONS[key.toLowerCase()] ?? beautifyKey(key);
|
||||
}
|
||||
|
||||
export function translateMetaKey(key: string): string {
|
||||
return META_TRANSLATIONS[key.toLowerCase()] ?? beautifyKey(key);
|
||||
}
|
||||
|
||||
export function beautifyKey(key: string): string {
|
||||
return key
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
}
|
||||
|
||||
/* ----------------------------- */
|
||||
/* VALUE FORMATTERS */
|
||||
/* ----------------------------- */
|
||||
|
||||
export function formatMetaValue(value: any): string {
|
||||
if (typeof value === 'boolean') return value ? 'Да' : 'Нет';
|
||||
if (typeof value === 'number') return String(value);
|
||||
if (typeof value === 'string') return value;
|
||||
|
||||
if (Array.isArray(value)) return value.join(', ');
|
||||
if (typeof value === 'object') return 'Сложное значение';
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/* ----------------------------- */
|
||||
/* ENCHANT LIST HELPER */
|
||||
/* ----------------------------- */
|
||||
|
||||
export function formatEnchants(
|
||||
enchants?: Record<string, number>,
|
||||
): { label: string; level: number }[] {
|
||||
if (!enchants || typeof enchants !== 'object') return [];
|
||||
|
||||
return Object.entries(enchants).map(([key, level]) => ({
|
||||
label: translateEnchant(key),
|
||||
level,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user