From d1e0cd5de0248d282ec1bb679f00d169c67e748e Mon Sep 17 00:00:00 2001 From: KeshavAnandCode Date: Fri, 30 Jan 2026 00:21:03 -0600 Subject: [PATCH] Added Chat btwn teams for bonuses --- src/app/api/chat/[code]/route.ts | 62 ++++++++++++ src/app/api/game/[code]/route.ts | 2 +- src/app/play/page.tsx | 11 ++ src/components/Chat.tsx | 169 +++++++++++++++++++++++++++++++ src/lib/rooms.ts | 121 +++++++++++++++++++++- src/types/index.ts | 16 ++- 6 files changed, 373 insertions(+), 8 deletions(-) create mode 100644 src/app/api/chat/[code]/route.ts create mode 100644 src/components/Chat.tsx diff --git a/src/app/api/chat/[code]/route.ts b/src/app/api/chat/[code]/route.ts new file mode 100644 index 0000000..f2a22e4 --- /dev/null +++ b/src/app/api/chat/[code]/route.ts @@ -0,0 +1,62 @@ +import { NextResponse } from 'next/server'; +import { sendChatMessage, getChatMessages } from '@/lib/rooms'; +import { z, ZodError } from 'zod'; + +const chatMessageSchema = z.object({ + playerId: z.string(), + message: z.string().min(1).max(200), +}); + +const chatQuerySchema = z.object({ + teamId: z.coerce.number().int().min(1).max(2), +}); + +export async function POST( + req: Request, + { params }: { params: Promise<{ code: string }> } +) { + try { + const { code } = await params; + const body = await req.json(); + const { playerId, message } = chatMessageSchema.parse(body); + + const success = sendChatMessage(code, playerId, message); + + if (!success) { + return NextResponse.json({ error: 'Cannot send message' }, { status: 400 }); + } + + return NextResponse.json({ success: true }); + + } catch (error: unknown) { + if (error instanceof ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } + + return NextResponse.json({ error: 'Invalid request' }, { status: 400 }); + } +} + +export async function GET( + req: Request, + { params }: { params: Promise<{ code: string }> } +) { + try { + const { code } = await params; + const url = new URL(req.url); + const teamId = url.searchParams.get('teamId'); + + const { teamId: validatedTeamId } = chatQuerySchema.parse({ teamId }); + + const messages = getChatMessages(code, validatedTeamId); + + return NextResponse.json({ messages }); + + } catch (error: unknown) { + if (error instanceof ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } + + return NextResponse.json({ error: 'Invalid request' }, { status: 400 }); + } +} \ No newline at end of file diff --git a/src/app/api/game/[code]/route.ts b/src/app/api/game/[code]/route.ts index 415b484..cbaa946 100644 --- a/src/app/api/game/[code]/route.ts +++ b/src/app/api/game/[code]/route.ts @@ -100,4 +100,4 @@ export async function POST( return NextResponse.json({ error: 'Invalid request' }, { status: 400 }); } -} \ No newline at end of file +}2 \ No newline at end of file diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 63352cc..51e2151 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from "react"; import Cookies from "js-cookie"; +import Chat from "@/components/Chat"; export default function PlayPage() { const [name, setName] = useState(""); @@ -796,6 +797,16 @@ export default function PlayPage() { )} + {/* Team Chat - only during bonus for your team */} + {phase === "bonus" && gameState?.currentTeam === parseInt(teamId) && ( + + )} + {/* Leave Button */}
diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx new file mode 100644 index 0000000..3f8beea --- /dev/null +++ b/src/components/Chat.tsx @@ -0,0 +1,169 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { ChatMessage } from "@/types"; + +interface ChatProps { + roomCode: string; + playerId: string; + teamId: number; + isActive: boolean; // Only active during bonus for your team +} + +export default function Chat({ + roomCode, + playerId, + teamId, + isActive, +}: ChatProps) { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const messagesEndRef = useRef(null); + + // Fetch messages + useEffect(() => { + if (!isActive) { + setMessages([]); + return; + } + + const interval = setInterval(async () => { + const res = await fetch(`/api/chat/${roomCode}?teamId=${teamId}`); + if (res.ok) { + const data = await res.json(); + setMessages(data.messages || []); + } + }, 500); // Poll every 500ms + + return () => clearInterval(interval); + }, [roomCode, teamId, isActive]); + + // Auto-scroll to bottom + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + async function sendMessage(e: React.FormEvent) { + e.preventDefault(); + if (!input.trim() || !isActive) return; + + await fetch(`/api/chat/${roomCode}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ playerId, message: input }), + }); + + setInput(""); + } + + if (!isActive) return null; + + return ( +
+
+ 💬 +

+ Team Chat +

+
+ + {/* Messages */} +
+ {messages.length === 0 ? ( +

+ No messages yet... +

+ ) : ( +
+ {messages.map((msg) => ( +
+
+ + {msg.playerName}: + + + {msg.message} + +
+
+ ))} +
+
+ )} +
+ + {/* Input */} +
+ setInput(e.target.value)} + placeholder="Type a message..." + maxLength={200} + className="input" + style={{ + flex: 1, + padding: "10px 12px", + fontSize: "0.875rem", + background: "var(--ctp-surface1)", + border: "2px solid var(--ctp-surface2)", + borderRadius: "8px", + }} + /> + +
+
+ ); +} diff --git a/src/lib/rooms.ts b/src/lib/rooms.ts index faed201..9533577 100644 --- a/src/lib/rooms.ts +++ b/src/lib/rooms.ts @@ -1,5 +1,4 @@ -import { Room, Player, GameState } from '@/types'; - +import type { Room, Player, GameState, ChatMessage } from '@/types'; declare global { var roomStorage: Map | undefined; } @@ -34,7 +33,6 @@ function createInitialGameState(): GameState { totalQuestions: 20, }; } - export function createRoom(teamMode: 1 | 2 = 2) { const code = generateCode(); const moderatorId = generateId(); @@ -51,12 +49,15 @@ export function createRoom(teamMode: 1 | 2 = 2) { isActive: true, createdAt: new Date(), teamMode, + chatMessages: new Map([ // ADD THIS + [1, []], + [2, []], + ]), }); console.log('✅ Created room:', code, 'Mode:', teamMode === 1 ? 'Individual' : 'Team'); return { code, moderatorId }; } - export function joinRoom(code: string, name: string, teamId: 1 | 2) { const room = rooms.get(code); @@ -272,6 +273,68 @@ export function markTossupWrong(code: string, moderatorId: string) { return true; } +import { NextResponse } from 'next/server'; +import { z, ZodError } from 'zod'; + +const chatMessageSchema = z.object({ + playerId: z.string(), + message: z.string().min(1).max(200), +}); + +const chatQuerySchema = z.object({ + teamId: z.coerce.number().int().min(1).max(2), +}); + +export async function POST( + req: Request, + { params }: { params: Promise<{ code: string }> } +) { + try { + const { code } = await params; + const body = await req.json(); + const { playerId, message } = chatMessageSchema.parse(body); + + const success = sendChatMessage(code, playerId, message); + + if (!success) { + return NextResponse.json({ error: 'Cannot send message' }, { status: 400 }); + } + + return NextResponse.json({ success: true }); + + } catch (error: unknown) { + if (error instanceof ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } + + return NextResponse.json({ error: 'Invalid request' }, { status: 400 }); + } +} + +export async function GET( + req: Request, + { params }: { params: Promise<{ code: string }> } +) { + try { + const { code } = await params; + const url = new URL(req.url); + const teamId = url.searchParams.get('teamId'); + + const { teamId: validatedTeamId } = chatQuerySchema.parse({ teamId }); + + const messages = getChatMessages(code, validatedTeamId); + + return NextResponse.json({ messages }); + + } catch (error: unknown) { + if (error instanceof ZodError) { + return NextResponse.json({ error: error.issues[0].message }, { status: 400 }); + } + + return NextResponse.json({ error: 'Invalid request' }, { status: 400 }); + } +} + export function moveOn(code: string, moderatorId: string) { const room = rooms.get(code); if (!room || room.moderatorId !== moderatorId) return false; @@ -319,6 +382,56 @@ export function endGame(code: string, moderatorId: string) { return true; } +// Chat functions +export function sendChatMessage(code: string, playerId: string, message: string) { + const room = rooms.get(code); + if (!room) return false; + + const player = room.players.get(playerId); + if (!player) return false; + + // Only allow chat during bonus phase + if (room.gameState.phase !== 'bonus') return false; + + // Only allow chat for the team that has the bonus + if (room.gameState.currentTeam !== player.teamId) return false; + + const teamMessages = room.chatMessages.get(player.teamId) || []; + + const chatMessage: ChatMessage = { + id: generateId(), + playerId: player.id, + playerName: player.name, + message: message.trim().slice(0, 200), // Max 200 chars + timestamp: Date.now(), + }; + + teamMessages.push(chatMessage); + + // Keep only last 50 messages per team + if (teamMessages.length > 50) { + teamMessages.shift(); + } + + room.chatMessages.set(player.teamId, teamMessages); + + return true; +} + +export function getChatMessages(code: string, teamId: number) { + const room = rooms.get(code); + if (!room) return []; + + return room.chatMessages.get(teamId) || []; +} + +export function clearTeamChat(code: string, teamId: number) { + const room = rooms.get(code); + if (!room) return; + + room.chatMessages.set(teamId, []); +} + // Cleanup setInterval(() => { const now = Date.now(); diff --git a/src/types/index.ts b/src/types/index.ts index 0d7ce3d..e01e9e4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -41,9 +41,19 @@ export interface Room { code: string; moderatorId: string; players: Map; - teams: Map<1 | 2, Team>; + teams: Map; gameState: GameState; isActive: boolean; createdAt: Date; - teamMode: 1 | 2; // 1 = individual scoring, 2 = team mode -} \ No newline at end of file + teamMode: 1 | 2; + chatMessages: Map; // teamId -> messages +} + +export interface ChatMessage { + id: string; + playerId: string; + playerName: string; + message: string; + timestamp: number; +} +