diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 967d137..54f3ec2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,18 +14,17 @@ datasource db { } model User { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? - email String @unique + email String @unique password String? image String? emailVerified DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - chatrooms Chatroom[] - accounts Account[] - sessions Session[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + accounts Account[] + chatrooms Chatroom[] + sessions Session[] @@map("users") } @@ -36,15 +35,14 @@ model Account { type String provider String providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text + refresh_token String? + access_token String? expires_at Int? token_type String? scope String? - id_token String? @db.Text + id_token String? session_state String? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) @@map("accounts") @@ -55,8 +53,7 @@ model Session { sessionToken String @unique userId String expires DateTime - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("sessions") } @@ -72,32 +69,31 @@ model VerificationToken { model Chatroom { id String @id @default(uuid()) - roomUrl String? @unique title String createdAt DateTime @default(now()) + roomUrl String? @unique userId String? + user User? @relation(fields: [userId], references: [id]) messages Message[] - user User? @relation(fields: [userId], references: [id]) - + @@unique([title, userId], name: "unique_title_per_user") @@map("chatrooms") } model Message { id String @id @default(uuid()) - chatroomId String alias String message String timestamp DateTime @default(now()) + chatroomId String linkPreviews LinkPreview[] - - chatroom Chatroom @relation(fields: [chatroomId], references: [id]) + chatroom Chatroom @relation(fields: [chatroomId], references: [id]) @@map("messages") } model LinkPreview { - id String @id @default(uuid()) + id String @id @default(uuid()) messageId String url String title String? @@ -107,8 +103,7 @@ model LinkPreview { favicon String? domain String? createdAt DateTime @default(now()) - - message Message @relation(fields: [messageId], references: [id], onDelete: Cascade) + message Message @relation(fields: [messageId], references: [id], onDelete: Cascade) @@map("link_previews") } 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/dashboard/page.tsx b/src/app/dashboard/page.tsx index 08efa56..1005610 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -90,7 +90,6 @@ export default function Home() { setShowRoomForm(false); } catch (error) { console.error("Error creating room:", error); - alert("Failed to create room. Please try again."); } finally { setCreatingRoom(false); } diff --git a/src/components/RoomCreationForm.tsx b/src/components/RoomCreationForm.tsx index 4179e06..217c4d1 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 { + 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

+ )} +