Compare commits
1
Commits
master
...
2a5eb07778
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a5eb07778 |
No files matched your search
Binary file not shown.
@@ -0,0 +1,35 @@
|
||||
.lobby-chat {
|
||||
margin-top: 12px;
|
||||
|
||||
.lobby-chat-messages {
|
||||
height: 140px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid rgba(127, 127, 127, 0.35);
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.9rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.lobby-chat-emojis {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0 3px;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.4;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(127, 127, 127, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { FormEvent, useEffect, useRef, useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { socket } from "../context/socket";
|
||||
import { getToken } from "../Utils";
|
||||
import "./LobbyChat.scss";
|
||||
|
||||
const EMOJIS = ['😀', '😂', '😎', '😢', '😡', '👍', '👎', '👋', '🎉', '🔥', '❤️', '🍕', '🍔', '🍺', '⏳'];
|
||||
|
||||
type LobbyChatMessage = { game: string, login: string, text: string };
|
||||
|
||||
/** Jednoduchý chat v lobby minihry — zprávy se drží jen po dobu zobrazení lobby. */
|
||||
export default function LobbyChat({ game }: { game: string }) {
|
||||
const [messages, setMessages] = useState<LobbyChatMessage[]>([]);
|
||||
const [text, setText] = useState('');
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage = (m: LobbyChatMessage) => {
|
||||
if (m.game === game) setMessages(prev => [...prev.slice(-99), m]);
|
||||
};
|
||||
socket.on('lobbychat:message', onMessage);
|
||||
return () => { socket.off('lobbychat:message', onMessage); };
|
||||
}, [game]);
|
||||
|
||||
useEffect(() => {
|
||||
listRef.current?.scrollTo({ top: listRef.current.scrollHeight });
|
||||
}, [messages]);
|
||||
|
||||
const submit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!text.trim()) return;
|
||||
socket.emit('lobbychat:send', { token: getToken(), game, text });
|
||||
setText('');
|
||||
};
|
||||
|
||||
const addEmoji = (emoji: string) => {
|
||||
setText(prev => (prev + emoji).slice(0, 200));
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="lobby-chat">
|
||||
<div className="lobby-chat-messages" ref={listRef}>
|
||||
{messages.length === 0 && <div className="text-muted">Zatím žádné zprávy…</div>}
|
||||
{messages.map((m, i) => <div key={i}><strong>{m.login}: </strong>{m.text}</div>)}
|
||||
</div>
|
||||
<div className="lobby-chat-emojis">
|
||||
{EMOJIS.map(e => <button key={e} type="button" onClick={() => addEmoji(e)}>{e}</button>)}
|
||||
</div>
|
||||
<Form onSubmit={submit}>
|
||||
<Form.Control
|
||||
ref={inputRef}
|
||||
size="sm"
|
||||
value={text}
|
||||
onChange={e => setText(e.target.value)}
|
||||
placeholder="Napište zprávu…"
|
||||
maxLength={200}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { useAuth } from "../context/auth";
|
||||
import { getToken } from "../Utils";
|
||||
import { useGameIcon } from "../games/themeIcons";
|
||||
import { useTouchDevice } from "../hooks/useTouchDevice";
|
||||
import LobbyChat from "./LobbyChat";
|
||||
import "./PongGame.scss";
|
||||
|
||||
// Typy musí odpovídat server/src/pongGame.ts (stav chodí přes websocket, ne přes OpenAPI)
|
||||
@@ -257,6 +258,7 @@ export default function PongGame() {
|
||||
{login === state?.host && state?.players.some(p => p.bot) && (
|
||||
<Button size="sm" variant="outline-secondary" onClick={() => send('pong:removeBot')}>Odebrat bota</Button>
|
||||
)}
|
||||
{isPlayer && <LobbyChat game="pong" />}
|
||||
</>
|
||||
);
|
||||
default:
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useAuth } from "../context/auth";
|
||||
import { getToken } from "../Utils";
|
||||
import { useGameIcon } from "../games/themeIcons";
|
||||
import { useTouchDevice } from "../hooks/useTouchDevice";
|
||||
import LobbyChat from "./LobbyChat";
|
||||
import "./ReactionDuel.scss";
|
||||
|
||||
// Typy musí odpovídat server/src/reactionDuel.ts (stav chodí přes websocket, ne přes OpenAPI)
|
||||
@@ -183,6 +184,7 @@ export default function ReactionDuel() {
|
||||
</ul>
|
||||
{state && state.players.length < state.minPlayers &&
|
||||
<p className="text-muted">Čeká se na další hráče…</p>}
|
||||
{isPlayer && <LobbyChat game="duel" />}
|
||||
</>
|
||||
);
|
||||
case 'waiting':
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getToken } from "../Utils";
|
||||
import { useGameIcon } from "../games/themeIcons";
|
||||
import { useTouchDevice } from "../hooks/useTouchDevice";
|
||||
import TouchButton from "./TouchButton";
|
||||
import LobbyChat from "./LobbyChat";
|
||||
import "./SnakeGame.scss";
|
||||
|
||||
// Typy musí odpovídat server/src/snakeGame.ts (stav chodí přes websocket, ne přes OpenAPI)
|
||||
@@ -279,6 +280,7 @@ export default function SnakeGame() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{isPlayer && <LobbyChat game="snake" />}
|
||||
</>
|
||||
);
|
||||
case 'countdown':
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
BIOMES, BotLevel, drawTanks, FLASH_MS, MuzzleFlash, PICKUPS, PickupType, tankColor, TankMode, TankPhase,
|
||||
TankPlayer, TankState, TEAM_COLORS, TEAM_NAMES,
|
||||
} from "../games/tankRender";
|
||||
import LobbyChat from "./LobbyChat";
|
||||
import "./TankGame.scss";
|
||||
|
||||
const OPEN_EVENT = 'luncher:open-tanks';
|
||||
@@ -426,6 +427,7 @@ export default function TankGame() {
|
||||
{state.hardcore && ' V aréně je tma: vidíte jen okolí tanku a kužel reflektoru ve směru věže, světlo zastaví zdi a bedny.'
|
||||
+ (state.teams ? ' Co osvítí spoluhráč, vidíte také.' : '') + ' Výbuchy a výstřely prosvítí i tmou.'}
|
||||
</p>
|
||||
{isPlayer && <LobbyChat game="tanks" />}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { TetrisSession, WIDTH, HEIGHT, pieceCells, Piece } from "../games/tetris
|
||||
import { useGameIcon } from "../games/themeIcons";
|
||||
import { useTouchDevice } from "../hooks/useTouchDevice";
|
||||
import TouchButton from "./TouchButton";
|
||||
import LobbyChat from "./LobbyChat";
|
||||
import "./TetrisGame.scss";
|
||||
|
||||
// Typy musí odpovídat server/src/tetrisGame.ts (stav chodí přes websocket, ne přes OpenAPI)
|
||||
@@ -554,6 +555,7 @@ export default function TetrisGame() {
|
||||
{state && state.players.length < state.minPlayers &&
|
||||
<p className="text-muted">Čeká se na další hráče… Pro hru o samotě použijte Trénink.</p>}
|
||||
{renderSettings()}
|
||||
{isPlayer && <LobbyChat game="tetris" />}
|
||||
</>
|
||||
);
|
||||
case 'countdown':
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
[
|
||||
"U oběda v restauraci si můžete zadat cenu (ikonka cenovky u vaší volby) — promítne se do měsíčního přehledu stravování a předvyplní se i v „Zaplatit za všechny“"
|
||||
"U oběda v restauraci si můžete zadat cenu (ikonka cenovky u vaší volby) — promítne se do měsíčního přehledu stravování a předvyplní se i v „Zaplatit za všechny“",
|
||||
"V lobby miniher (Pong, Reakční hra, Had, Tetris, Tanky) si hráči mohou psát v chatu, emotikony se vkládají jedním kliknutím 😉"
|
||||
]
|
||||
+17
-1
@@ -1,4 +1,4 @@
|
||||
import { Socket } from "socket.io";
|
||||
import { Server, Socket } from "socket.io";
|
||||
import { getLogin } from "./auth";
|
||||
|
||||
/** Sdílené pomůcky pro websocketové minihry (reakční hra, had). */
|
||||
@@ -60,3 +60,19 @@ export function gameHandler(socket: Socket, errorEvent: string, action: (login:
|
||||
|
||||
/** Minimální počet hráčů — lokálně lze hrát i sám. */
|
||||
export const minPlayersForEnv = (prod: number) => process.env.NODE_ENV === 'development' ? 1 : prod;
|
||||
|
||||
/**
|
||||
* Jednoduchý chat v lobby miniher — zprávy se nikde neukládají, server je jen
|
||||
* přepošle všem (klient si je filtruje podle hry).
|
||||
*/
|
||||
export function registerLobbyChat(io: Server) {
|
||||
io.on('connection', socket => {
|
||||
socket.on('lobbychat:send', gameHandler(socket, 'lobbychat:error', (login, payload) => {
|
||||
const { game, text } = (payload ?? {}) as { game?: unknown, text?: unknown };
|
||||
if (typeof game !== 'string' || typeof text !== 'string') return;
|
||||
const trimmed = text.trim().slice(0, 200);
|
||||
if (!trimmed) return;
|
||||
io.emit('lobbychat:message', { game: game.slice(0, 20), login, text: trimmed });
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { registerTetrisGame } from "./tetrisGame";
|
||||
import { registerPongGame } from "./pongGame";
|
||||
import { registerDrawingGame } from "./drawingGame";
|
||||
import { registerTankGame } from "./tankGame";
|
||||
import { registerLobbyChat } from "./minigames";
|
||||
|
||||
let io: Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>;
|
||||
let pubClient: ReturnType<typeof createClient>;
|
||||
@@ -40,6 +41,7 @@ export const initWebsocket = (server: any) => {
|
||||
registerPongGame(io);
|
||||
registerDrawingGame(io);
|
||||
registerTankGame(io);
|
||||
registerLobbyChat(io);
|
||||
return io;
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user