From 2aef9699d72c2820e306ff9752795c8cf9f8ac12 Mon Sep 17 00:00:00 2001 From: Marwan Date: Sat, 4 Apr 2026 04:48:53 -0400 Subject: [PATCH 1/2] linting --- .../sessions/[sessionId]/questions/route.ts | 4 +- src/app/api/sessions/[sessionId]/route.ts | 2 +- src/app/room/classChat/ChatHeader.tsx | 212 ++++++++-------- src/app/room/classChat/ChatInput.tsx | 139 +++++++---- src/app/room/classChat/ManageTAsModal.tsx | 231 ++++++++++++++++++ src/app/room/classChat/index.tsx | 44 +++- src/lib/questionValidation.ts | 9 +- src/lib/redisKeys.ts | 7 +- src/socket/handlers/questionHandlers.ts | 48 ++-- 9 files changed, 509 insertions(+), 187 deletions(-) create mode 100644 src/app/room/classChat/ManageTAsModal.tsx diff --git a/src/app/api/sessions/[sessionId]/questions/route.ts b/src/app/api/sessions/[sessionId]/questions/route.ts index b46e398..7c60e28 100644 --- a/src/app/api/sessions/[sessionId]/questions/route.ts +++ b/src/app/api/sessions/[sessionId]/questions/route.ts @@ -210,8 +210,8 @@ export async function POST(request: NextRequest, { params }: RouteParams) { return NextResponse.json({ error: sessionValidation.error }, { status: statusCode }); } - // 7. Check rate limit using shared validation - const isRateLimited = await checkQuestionRateLimit(authorId); + // 7. Check rate limit using shared validation (scoped to this session) + const isRateLimited = await checkQuestionRateLimit(authorId, sessionId); if (isRateLimited) { return NextResponse.json( { error: "Rate limit exceeded. Please wait before asking another question." }, diff --git a/src/app/api/sessions/[sessionId]/route.ts b/src/app/api/sessions/[sessionId]/route.ts index 6fc1831..a828def 100644 --- a/src/app/api/sessions/[sessionId]/route.ts +++ b/src/app/api/sessions/[sessionId]/route.ts @@ -52,7 +52,7 @@ export async function GET(_req: Request, { params }: RouteParams) { } } - return NextResponse.json({ status: session.status }); + return NextResponse.json({ status: session.status, courseId: session.courseId }); } catch (error) { console.error("[Sessions API] Failed to fetch session status:", error); return NextResponse.json({ error: "An error occurred." }, { status: 500 }); diff --git a/src/app/room/classChat/ChatHeader.tsx b/src/app/room/classChat/ChatHeader.tsx index 07ec92c..6513aa2 100644 --- a/src/app/room/classChat/ChatHeader.tsx +++ b/src/app/room/classChat/ChatHeader.tsx @@ -3,14 +3,15 @@ import { Input } from "@/components/ui/input"; import { useContext, useState } from "react"; import { - ArrowLeft, PanelRightClose, Users, GraduationCap, Search, X, ArrowBigRight, + UserPlus, } from "lucide-react"; +import ManageTAsModal from "./ManageTAsModal"; import { useMediaQuery } from "@/hooks/use-media-query"; import { SlideUpdateContext } from "../SlideUpdateContext"; import type { Role } from "@/utils/types"; @@ -67,114 +68,129 @@ export default function ChatHeader({ }: ChatHeaderProps) { const { sessionTitle } = useRoom(); const [isSearchExpanded, setIsSearchExpanded] = useState(false); + const [showTAModal, setShowTAModal] = useState(false); return ( -
-
- {isSearchExpanded ? ( -
-
- - onSearchChange(e.target.value)} - onBlur={() => { - if (!searchQuery) setIsSearchExpanded(false); - }} - /> - {searchQuery && ( - - )} -
- -
- ) : ( - <> -
- - {sessionTitle && ( -

- {sessionTitle} -

- )} -
- -
- )} +
+ +
+ ) : ( + <> +
+ + {sessionTitle && ( +

+ {sessionTitle} +

+ )} +
- {/* Answer mode toggle — professors only */} - {role === "PROFESSOR" && ( +
- )} - - Back - - -
- - )} - - + + {/* Answer mode toggle — professors only */} + {role === "PROFESSOR" && ( + + )} + {role === "PROFESSOR" && ( + + )} + + Back + + + + + )} + + + + {showTAModal && setShowTAModal(false)} />} + ); } diff --git a/src/app/room/classChat/ChatInput.tsx b/src/app/room/classChat/ChatInput.tsx index b7088a7..7a0b798 100644 --- a/src/app/room/classChat/ChatInput.tsx +++ b/src/app/room/classChat/ChatInput.tsx @@ -1,8 +1,8 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Textarea } from "@/components/ui/textarea"; -import { Ghost, User, Send } from "lucide-react"; +import { Ghost, User, Send, Clock } from "lucide-react"; const MIN_LENGTH = 5; @@ -13,6 +13,8 @@ interface ChatInputProps { onClearError?: () => void; isAnonymous: boolean; onAnonymousChange: (val: boolean) => void; + timeoutUntil?: number | null; + onTimeoutExpired?: () => void; } export default function ChatInput({ @@ -22,9 +24,40 @@ export default function ChatInput({ onClearError, isAnonymous, onAnonymousChange, + timeoutUntil, + onTimeoutExpired, }: ChatInputProps) { const [content, setContent] = useState(""); const [localError, setLocalError] = useState(null); + const [secsLeft, setSecsLeft] = useState(0); + + // Derived from secsLeft (managed by the effect below) so Date.now() is never + // called directly during render, keeping the component pure. + // Also guard on timeoutUntil being set so a stale secsLeft value from a + // previous timeout doesn't incorrectly mark the input as timed-out. + const isTimedOut = !!timeoutUntil && secsLeft > 0; + + useEffect(() => { + if (!timeoutUntil) return; + + const update = () => { + const remaining = Math.ceil((timeoutUntil - Date.now()) / 1000); + if (remaining <= 0) { + setSecsLeft(0); + onTimeoutExpired?.(); + } else { + setSecsLeft(remaining); + } + }; + + update(); + const id = setInterval(update, 1000); + return () => clearInterval(id); + }, [timeoutUntil, onTimeoutExpired]); + + const mins = Math.floor(secsLeft / 60); + const secs = secsLeft % 60; + const countdownLabel = `${mins}:${String(secs).padStart(2, "0")}`; const error = serverError ?? localError; @@ -36,7 +69,7 @@ export default function ChatInput({ const handleSubmit = () => { const trimmed = content.trim(); - if (disabled) return; + if (disabled || isTimedOut) return; if (!trimmed) return; if (trimmed.length < MIN_LENGTH) { setLocalError(`Question must be at least ${MIN_LENGTH} characters.`); @@ -59,51 +92,63 @@ export default function ChatInput({
-