diff --git a/chat-system/styles/globals.css b/chat-system/styles/globals.css index 5f7b4736..7b4b3578 100644 --- a/chat-system/styles/globals.css +++ b/chat-system/styles/globals.css @@ -78,3 +78,8 @@ button { ::-webkit-scrollbar-thumb:hover { background: rgba(170, 191, 255, 0.62); } + +html { scroll-behavior: smooth; } +*:focus-visible { outline: 2px solid #6366f1; outline-offset: 2px; border-radius: 4px; } +.skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: 8px; } +@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } diff --git a/src/app/api/invites/[token]/join/route.ts b/src/app/api/invites/[token]/join/route.ts index de740688..442c18ad 100644 --- a/src/app/api/invites/[token]/join/route.ts +++ b/src/app/api/invites/[token]/join/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { currentUser } from "@clerk/nextjs/server"; -import { and, eq, sql } from "drizzle-orm"; +import { and, eq, or, sql } from "drizzle-orm"; import { db } from "@/configs/db"; import { @@ -78,27 +78,58 @@ export async function POST( ); } - if ( - inviteData.maxUses !== null && - inviteData.usedCount >= inviteData.maxUses - ) { + const joinResult = await db.transaction(async (tx) => { + const [existingMember] = await tx + .select() + .from(membershipsTable) + .where( + and( + eq(membershipsTable.userEmail, email), + eq(membershipsTable.classroomId, inviteData.classroomId), + ), + ); + + if (existingMember) { + return { alreadyMember: true as const }; + } + + const [updatedInvite] = await tx + .update(classroomInvitesTable) + .set({ + usedCount: sql`${classroomInvitesTable.usedCount} + 1`, + }) + .where( + and( + eq(classroomInvitesTable.id, inviteData.inviteId), + or( + sql`${classroomInvitesTable.maxUses} IS NULL`, + sql`${classroomInvitesTable.usedCount} < ${classroomInvitesTable.maxUses}`, + ), + ), + ) + .returning({ id: classroomInvitesTable.id }); + + if (!updatedInvite) { + return { inviteExpired: true as const }; + } + + await tx.insert(membershipsTable).values({ + userEmail: email, + classroomId: inviteData.classroomId, + role: "student", + }); + + return { success: true as const }; + }); + + if ("inviteExpired" in joinResult) { return NextResponse.json( { error: "This invite link has reached its usage limit" }, { status: 410 }, ); } - const [existingMember] = await db - .select() - .from(membershipsTable) - .where( - and( - eq(membershipsTable.userEmail, email), - eq(membershipsTable.classroomId, inviteData.classroomId), - ), - ); - - if (existingMember) { + if ("alreadyMember" in joinResult) { return NextResponse.json({ success: true, alreadyMember: true, @@ -111,19 +142,6 @@ export async function POST( }); } - await db.insert(membershipsTable).values({ - userEmail: email, - classroomId: inviteData.classroomId, - role: "student", - }); - - await db - .update(classroomInvitesTable) - .set({ - usedCount: sql`${classroomInvitesTable.usedCount} + 1`, - }) - .where(eq(classroomInvitesTable.id, inviteData.inviteId)); - return NextResponse.json({ success: true, classroom: {