From 40741d43f6c9d720c488c521a209ff243bf6e8d1 Mon Sep 17 00:00:00 2001 From: Aishah Date: Wed, 9 Jul 2025 14:23:20 +0100 Subject: [PATCH 1/7] feat: add room name validation API endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add /api/rooms/check-name endpoint for real-time name availability checking - Update room creation API to validate duplicate titles per user - Return 409 status code with user-friendly error message for duplicates Refs #11. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/app/api/rooms/check-name/route.ts | 62 +++++++++++++++++++++++++++ src/app/api/rooms/route.ts | 15 +++++++ 2 files changed, 77 insertions(+) create mode 100644 src/app/api/rooms/check-name/route.ts diff --git a/src/app/api/rooms/check-name/route.ts b/src/app/api/rooms/check-name/route.ts new file mode 100644 index 0000000..42dca12 --- /dev/null +++ b/src/app/api/rooms/check-name/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function POST(request: NextRequest) { + try { + // Check authentication + const session = await getServerSession(authOptions); + if (!session?.user?.email) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Get user from database + const user = await prisma.user.findUnique({ + where: { email: session.user.email }, + }); + + if (!user) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + // Parse and validate input + const { title } = await request.json(); + + if (!title || typeof title !== "string") { + return NextResponse.json( + { error: "Title is required and must be a string" }, + { status: 400 } + ); + } + + const trimmedTitle = title.trim(); + + if (trimmedTitle.length === 0) { + return NextResponse.json( + { error: "Title cannot be empty" }, + { status: 400 } + ); + } + + // Check if user already has a room with this title + const existingRoom = await prisma.chatroom.findFirst({ + where: { + title: trimmedTitle, + userId: user.id, + }, + }); + + return NextResponse.json({ + available: !existingRoom, + title: trimmedTitle, + }); + + } catch (error) { + console.error("Error checking room name availability:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/rooms/route.ts b/src/app/api/rooms/route.ts index d3573b5..1c60680 100644 --- a/src/app/api/rooms/route.ts +++ b/src/app/api/rooms/route.ts @@ -74,6 +74,21 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "User not found" }, { status: 404 }); } + // Check if user already has a room with this title + const existingRoomWithTitle = await prisma.chatroom.findFirst({ + where: { + title: title.trim(), + userId: user.id, + }, + }); + + if (existingRoomWithTitle) { + return NextResponse.json( + { error: "You already have a room with this name" }, + { status: 409 }, + ); + } + // Generate a unique room URL with collision handling let roomUrl: string; let attempts = 0; From cdeecd2602fb397f02aa45515cf3f7e7ce5c2493 Mon Sep 17 00:00:00 2001 From: Aishah Date: Wed, 9 Jul 2025 14:27:45 +0100 Subject: [PATCH 2/7] feat: add real-time room name validation to frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create roomNameValidation helper for API calls - Add debounced validation to RoomCreationForm component - Show real-time feedback with visual indicators (green/red borders) - Disable submit button when name is unavailable or validating - Display validation messages for better user experience Refs #11. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/components/RoomCreationForm.tsx | 72 +++++++++++++++++++++++++++-- src/lib/roomNameValidation.ts | 37 +++++++++++++++ 2 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 src/lib/roomNameValidation.ts diff --git a/src/components/RoomCreationForm.tsx b/src/components/RoomCreationForm.tsx index 4179e06..4f03df5 100644 --- a/src/components/RoomCreationForm.tsx +++ b/src/components/RoomCreationForm.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useState, useEffect, useCallback } from "react"; +import { checkRoomNameAvailability } from "@/lib/roomNameValidation"; interface RoomCreationFormProps { onSubmit: (title: string) => Promise; @@ -12,17 +13,62 @@ export default function RoomCreationForm({ isLoading = false, }: RoomCreationFormProps) { const [roomTitle, setRoomTitle] = useState(""); + const [isValidating, setIsValidating] = useState(false); + const [validationError, setValidationError] = useState(null); + const [isAvailable, setIsAvailable] = useState(null); + + // Debounced validation function + const validateRoomName = useCallback(async (title: string) => { + if (!title.trim()) { + setValidationError(null); + setIsAvailable(null); + return; + } + + setIsValidating(true); + setValidationError(null); + + try { + const result = await checkRoomNameAvailability(title.trim()); + + if (result.error) { + setValidationError(result.error); + setIsAvailable(false); + } else { + setValidationError(result.available ? null : "You already have a room with this name"); + setIsAvailable(result.available); + } + } catch (error) { + setValidationError("Failed to check room name availability"); + setIsAvailable(false); + } finally { + setIsValidating(false); + } + }, []); + + // Debounce validation + useEffect(() => { + const timeoutId = setTimeout(() => { + validateRoomName(roomTitle); + }, 500); + + return () => clearTimeout(timeoutId); + }, [roomTitle, validateRoomName]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!roomTitle.trim()) return; + if (!roomTitle.trim() || isAvailable === false) return; await onSubmit(roomTitle.trim()); setRoomTitle(""); + setValidationError(null); + setIsAvailable(null); }; const handleCancel = () => { setRoomTitle(""); + setValidationError(null); + setIsAvailable(null); onCancel(); }; @@ -46,16 +92,34 @@ export default function RoomCreationForm({ value={roomTitle} onChange={(e) => setRoomTitle(e.target.value)} placeholder="Enter a title for your room..." - className="text-text bg-card w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:border-transparent" + className={`text-text bg-card w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:border-transparent ${ + validationError + ? "border-red-500 focus:ring-red-500" + : isAvailable === true + ? "border-green-500 focus:ring-green-500" + : "border-border focus:ring-yellow-500" + }`} required disabled={isLoading} aria-busy={isLoading} /> + {/* Validation feedback */} +
+ {isValidating && ( +

Checking availability...

+ )} + {validationError && ( +

{validationError}

+ )} + {isAvailable === true && !isValidating && ( +

✓ Room name is available

+ )} +