diff --git a/client/.yarn/install-state.gz b/client/.yarn/install-state.gz
new file mode 100644
index 0000000..e8c8b42
Binary files /dev/null and b/client/.yarn/install-state.gz differ
diff --git a/client/src/components/LobbyChat.scss b/client/src/components/LobbyChat.scss
new file mode 100644
index 0000000..b691314
--- /dev/null
+++ b/client/src/components/LobbyChat.scss
@@ -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);
+ }
+ }
+ }
+}
diff --git a/client/src/components/LobbyChat.tsx b/client/src/components/LobbyChat.tsx
new file mode 100644
index 0000000..235ff47
--- /dev/null
+++ b/client/src/components/LobbyChat.tsx
@@ -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([]);
+ const [text, setText] = useState('');
+ const listRef = useRef(null);
+ const inputRef = useRef(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 (
+
+
+ {messages.length === 0 &&
Zatím žádné zprávy…
}
+ {messages.map((m, i) =>
{m.login}: {m.text}
)}
+
+
+ {EMOJIS.map(e => )}
+
+
setText(e.target.value)}
+ placeholder="Napište zprávu…"
+ maxLength={200}
+ onKeyDown={e => e.stopPropagation()}
+ />
+
+
+ );
+}
diff --git a/client/src/components/PongGame.tsx b/client/src/components/PongGame.tsx
index a8d0926..2ba1ed5 100644
--- a/client/src/components/PongGame.tsx
+++ b/client/src/components/PongGame.tsx
@@ -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) && (
)}
+ {isPlayer && }
>
);
default:
diff --git a/client/src/components/ReactionDuel.tsx b/client/src/components/ReactionDuel.tsx
index 239d473..2b9537c 100644
--- a/client/src/components/ReactionDuel.tsx
+++ b/client/src/components/ReactionDuel.tsx
@@ -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() {
{state && state.players.length < state.minPlayers &&
Čeká se na další hráče…
}
+ {isPlayer && }
>
);
case 'waiting':
diff --git a/client/src/components/SnakeGame.tsx b/client/src/components/SnakeGame.tsx
index 711825b..83a2df7 100644
--- a/client/src/components/SnakeGame.tsx
+++ b/client/src/components/SnakeGame.tsx
@@ -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() {
))}
+ {isPlayer && }
>
);
case 'countdown':
diff --git a/client/src/components/TankGame.tsx b/client/src/components/TankGame.tsx
index 35a0b56..6643637 100644
--- a/client/src/components/TankGame.tsx
+++ b/client/src/components/TankGame.tsx
@@ -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.'}
+ {isPlayer && }
>
);
diff --git a/client/src/components/TetrisGame.tsx b/client/src/components/TetrisGame.tsx
index 6ad23e9..33473e6 100644
--- a/client/src/components/TetrisGame.tsx
+++ b/client/src/components/TetrisGame.tsx
@@ -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 &&
Čeká se na další hráče… Pro hru o samotě použijte Trénink.
}
{renderSettings()}
+ {isPlayer && }
>
);
case 'countdown':
diff --git a/server/changelogs/2026-10-21.json b/server/changelogs/2026-10-21.json
index 7e32580..7e877a6 100644
--- a/server/changelogs/2026-10-21.json
+++ b/server/changelogs/2026-10-21.json
@@ -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 😉"
]
diff --git a/server/src/minigames.ts b/server/src/minigames.ts
index 032769b..a8581ad 100644
--- a/server/src/minigames.ts
+++ b/server/src/minigames.ts
@@ -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 });
+ }));
+ });
+}
diff --git a/server/src/websocket.ts b/server/src/websocket.ts
index 4429d48..aef9277 100644
--- a/server/src/websocket.ts
+++ b/server/src/websocket.ts
@@ -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;
let pubClient: ReturnType;
@@ -40,6 +41,7 @@ export const initWebsocket = (server: any) => {
registerPongGame(io);
registerDrawingGame(io);
registerTankGame(io);
+ registerLobbyChat(io);
return io;
};