add websocket

This commit is contained in:
aurinex
2025-12-22 16:42:37 +05:00
parent 4b8e535c58
commit 6caa563b41
5 changed files with 318 additions and 60 deletions

View File

@ -4,7 +4,7 @@ interface HeadAvatarProps {
skinUrl?: string;
size?: number;
style?: React.CSSProperties;
version?: number; // ✅ добавили
version?: number;
}
const DEFAULT_SKIN =
@ -14,15 +14,17 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
skinUrl,
size = 24,
style,
version = 0, // ✅ дефолт
version = 0,
...canvasProps
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const requestIdRef = useRef(0);
useEffect(() => {
const baseUrl = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
requestIdRef.current += 1;
const requestId = requestIdRef.current;
// ✅ cache-bust: чтобы браузер НЕ отдавал старую картинку
const baseUrl = skinUrl?.trim() ? skinUrl : DEFAULT_SKIN;
const finalSkinUrl = `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}v=${version}`;
const canvas = canvasRef.current;
@ -33,6 +35,9 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
img.src = finalSkinUrl;
img.onload = () => {
// ✅ игнорим старые onload
if (requestIdRef.current !== requestId) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
@ -47,9 +52,16 @@ export const HeadAvatar: React.FC<HeadAvatarProps> = ({
};
img.onerror = (e) => {
if (requestIdRef.current !== requestId) return;
console.error('Не удалось загрузить скин для HeadAvatar:', e);
};
}, [skinUrl, size, version]); // ✅ version добавили
return () => {
// ✅ гарантированно “убиваем” обработчики старого запроса
img.onload = null;
img.onerror = null;
};
}, [skinUrl, size, version]);
return (
<canvas

View File

@ -49,6 +49,7 @@ 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';
import { getWsBaseUrl } from '../realtime/wsBase';
interface TabPanelProps {
children?: React.ReactNode;
@ -80,6 +81,18 @@ const GLASS_BG =
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)';
function upsertIntoList<T extends { id: string }>(list: T[], item: T) {
const idx = list.findIndex((x) => x.id === item.id);
if (idx === -1) return [item, ...list];
const copy = list.slice();
copy[idx] = item;
return copy;
}
function removeFromList<T extends { id: string }>(list: T[], id: string) {
return list.filter((x) => x.id !== id);
}
export default function Marketplace() {
const [marketLoading, setMarketLoading] = useState<boolean>(false);
@ -154,6 +167,91 @@ export default function Marketplace() {
}
};
type MarketListedPayload = { serverIp: string; item: MarketplaceItemResponse };
type MarketSoldPayload = { serverIp: string; itemId: string };
type MarketPricePayload = { serverIp: string; item: MarketplaceItemResponse };
type MarketCancelledPayload = { serverIp: string; itemId: string };
useEffect(() => {
if (!selectedServer) return;
const serverIp = selectedServer.ip;
const base = 'wss://minecraft.api.popa-popa.ru'
const wsUrl = `${base}/ws/marketplace?server_ip=${encodeURIComponent(serverIp)}`;
const ws = new WebSocket(wsUrl);
const ping = window.setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.send('ping');
}, 25000);
ws.onmessage = (ev) => {
let msg: any;
try {
msg = JSON.parse(ev.data);
} catch {
return;
}
if (msg.server_ip !== serverIp && msg.serverIp !== serverIp) return;
switch (msg.event) {
case 'market:item_listed': {
const item: MarketplaceItemResponse = msg.item;
setMarketItems((prev) => {
if (!prev) return prev;
if (prev.page !== 1) return prev;
return { ...prev, items: upsertIntoList(prev.items, item) };
});
if (item?.seller_name === username) {
setMyItems((prev) => {
if (!prev) return prev;
if (prev.page !== 1) return prev;
return { ...prev, items: upsertIntoList(prev.items, item) };
});
}
break;
}
case 'market:item_sold': {
const itemId: string = msg.item_id ?? msg.itemId;
setMarketItems((prev) => (prev ? { ...prev, items: removeFromList(prev.items, itemId) } : prev));
setMyItems((prev) => (prev ? { ...prev, items: removeFromList(prev.items, itemId) } : prev));
break;
}
case 'market:item_cancelled': {
const itemId: string = msg.item_id ?? msg.itemId;
setMarketItems((prev) => (prev ? { ...prev, items: removeFromList(prev.items, itemId) } : prev));
setMyItems((prev) => (prev ? { ...prev, items: removeFromList(prev.items, itemId) } : prev));
break;
}
case 'market:item_price_updated': {
const item: MarketplaceItemResponse = msg.item;
setMarketItems((prev) => (prev ? { ...prev, items: upsertIntoList(prev.items, item) } : prev));
setMyItems((prev) => (prev ? { ...prev, items: upsertIntoList(prev.items, item) } : prev));
break;
}
}
};
ws.onerror = (e) => {
console.error('Marketplace WS error:', e);
};
ws.onclose = () => {
window.clearInterval(ping);
};
return () => {
window.clearInterval(ping);
ws.close();
};
}, [selectedServer?.ip, username]);
useEffect(() => {
if (tabValue !== 2) return;
if (!selectedServer) return;
@ -285,7 +383,7 @@ export default function Marketplace() {
const loadMarketItems = async (serverIp: string, pageNumber: number) => {
try {
setMarketLoading(true);
const marketData = await fetchMarketplace(serverIp, pageNumber, 10);
const marketData = await fetchMarketplace(serverIp, pageNumber, 12);
setMarketItems(marketData);
setPage(marketData.page);
setTotalPages(marketData.pages);
@ -988,6 +1086,7 @@ export default function Marketplace() {
width: '100%',
height: '100%',
display: 'flex',
ml: '25%',
flexDirection: 'column',
background: 'rgba(20,20,20,0.78)',
borderRadius: '2.0vw',

View File

@ -0,0 +1,14 @@
export function getWsBaseUrl(): string {
// 1) если ты пробрасываешь конфиг в window
const w = window as any;
if (w.__ENV__?.WS_BASE) return String(w.__ENV__.WS_BASE);
// 2) если открыто с https/http — строим ws/wss автоматически
if (typeof window !== 'undefined' && window.location?.origin) {
const origin = window.location.origin; // http(s)://host
return origin.replace(/^http/, 'ws');
}
// 3) дефолт
return 'wss://minecraft.api.popa-popa.ru';
}