diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index c4005ce8..4a8b1b0c 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -254,7 +254,13 @@
"Bash(mcp__sqlite__describe_table:*)",
"Bash(git diff:*)",
"Bash(git show:*)",
- "Bash(npx tsx:*)"
+ "Bash(npx tsx:*)",
+ "Bash(xargs ls:*)",
+ "Bash(mcp__sqlite__list_tables)",
+ "WebFetch(domain:developer.chrome.com)",
+ "Bash(claude mcp add:*)",
+ "Bash(claude mcp:*)",
+ "Bash(git rev-parse:*)"
],
"deny": [],
"ask": []
diff --git a/apps/web/src/app/api/classrooms/[classroomId]/enrollment-requests/[requestId]/approve/route.ts b/apps/web/src/app/api/classrooms/[classroomId]/enrollment-requests/[requestId]/approve/route.ts
index 32f4dc11..6e6ed946 100644
--- a/apps/web/src/app/api/classrooms/[classroomId]/enrollment-requests/[requestId]/approve/route.ts
+++ b/apps/web/src/app/api/classrooms/[classroomId]/enrollment-requests/[requestId]/approve/route.ts
@@ -1,7 +1,25 @@
+import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
+import { db, schema } from '@/db'
import { approveEnrollmentRequest, getTeacherClassroom } from '@/lib/classroom'
import { getViewerId } from '@/lib/viewer'
+/**
+ * Get or create user record for a viewerId (guestId)
+ */
+async function getOrCreateUser(viewerId: string) {
+ let user = await db.query.users.findFirst({
+ where: eq(schema.users.guestId, viewerId),
+ })
+
+ if (!user) {
+ const [newUser] = await db.insert(schema.users).values({ guestId: viewerId }).returning()
+ user = newUser
+ }
+
+ return user
+}
+
interface RouteParams {
params: Promise<{ classroomId: string; requestId: string }>
}
@@ -16,14 +34,15 @@ export async function POST(req: NextRequest, { params }: RouteParams) {
try {
const { classroomId, requestId } = await params
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
// Verify user is the teacher of this classroom
- const classroom = await getTeacherClassroom(viewerId)
+ const classroom = await getTeacherClassroom(user.id)
if (!classroom || classroom.id !== classroomId) {
return NextResponse.json({ error: 'Not authorized' }, { status: 403 })
}
- const result = await approveEnrollmentRequest(requestId, viewerId, 'teacher')
+ const result = await approveEnrollmentRequest(requestId, user.id, 'teacher')
return NextResponse.json({
request: result.request,
diff --git a/apps/web/src/app/api/classrooms/[classroomId]/enrollment-requests/[requestId]/deny/route.ts b/apps/web/src/app/api/classrooms/[classroomId]/enrollment-requests/[requestId]/deny/route.ts
index 7d1b1d88..cd7c5ab5 100644
--- a/apps/web/src/app/api/classrooms/[classroomId]/enrollment-requests/[requestId]/deny/route.ts
+++ b/apps/web/src/app/api/classrooms/[classroomId]/enrollment-requests/[requestId]/deny/route.ts
@@ -1,7 +1,25 @@
+import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
+import { db, schema } from '@/db'
import { denyEnrollmentRequest, getTeacherClassroom } from '@/lib/classroom'
import { getViewerId } from '@/lib/viewer'
+/**
+ * Get or create user record for a viewerId (guestId)
+ */
+async function getOrCreateUser(viewerId: string) {
+ let user = await db.query.users.findFirst({
+ where: eq(schema.users.guestId, viewerId),
+ })
+
+ if (!user) {
+ const [newUser] = await db.insert(schema.users).values({ guestId: viewerId }).returning()
+ user = newUser
+ }
+
+ return user
+}
+
interface RouteParams {
params: Promise<{ classroomId: string; requestId: string }>
}
@@ -16,14 +34,15 @@ export async function POST(req: NextRequest, { params }: RouteParams) {
try {
const { classroomId, requestId } = await params
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
// Verify user is the teacher of this classroom
- const classroom = await getTeacherClassroom(viewerId)
+ const classroom = await getTeacherClassroom(user.id)
if (!classroom || classroom.id !== classroomId) {
return NextResponse.json({ error: 'Not authorized' }, { status: 403 })
}
- const request = await denyEnrollmentRequest(requestId, viewerId, 'teacher')
+ const request = await denyEnrollmentRequest(requestId, user.id, 'teacher')
return NextResponse.json({ request })
} catch (error) {
diff --git a/apps/web/src/app/api/classrooms/[classroomId]/enrollment-requests/route.ts b/apps/web/src/app/api/classrooms/[classroomId]/enrollment-requests/route.ts
index 89bdd139..c2a15bfa 100644
--- a/apps/web/src/app/api/classrooms/[classroomId]/enrollment-requests/route.ts
+++ b/apps/web/src/app/api/classrooms/[classroomId]/enrollment-requests/route.ts
@@ -1,4 +1,6 @@
+import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
+import { db, schema } from '@/db'
import {
createEnrollmentRequest,
getPendingRequestsForClassroom,
@@ -7,6 +9,22 @@ import {
} from '@/lib/classroom'
import { getViewerId } from '@/lib/viewer'
+/**
+ * Get or create user record for a viewerId (guestId)
+ */
+async function getOrCreateUser(viewerId: string) {
+ let user = await db.query.users.findFirst({
+ where: eq(schema.users.guestId, viewerId),
+ })
+
+ if (!user) {
+ const [newUser] = await db.insert(schema.users).values({ guestId: viewerId }).returning()
+ user = newUser
+ }
+
+ return user
+}
+
interface RouteParams {
params: Promise<{ classroomId: string }>
}
@@ -21,9 +39,10 @@ export async function GET(req: NextRequest, { params }: RouteParams) {
try {
const { classroomId } = await params
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
// Verify user is the teacher of this classroom
- const classroom = await getTeacherClassroom(viewerId)
+ const classroom = await getTeacherClassroom(user.id)
if (!classroom || classroom.id !== classroomId) {
return NextResponse.json({ error: 'Not authorized' }, { status: 403 })
}
@@ -48,6 +67,7 @@ export async function POST(req: NextRequest, { params }: RouteParams) {
try {
const { classroomId } = await params
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
const body = await req.json()
if (!body.playerId) {
@@ -55,9 +75,9 @@ export async function POST(req: NextRequest, { params }: RouteParams) {
}
// Determine role: is user the teacher or a parent?
- const classroom = await getTeacherClassroom(viewerId)
+ const classroom = await getTeacherClassroom(user.id)
const isTeacher = classroom?.id === classroomId
- const parentCheck = await isParent(viewerId, body.playerId)
+ const parentCheck = await isParent(user.id, body.playerId)
if (!isTeacher && !parentCheck) {
return NextResponse.json(
@@ -71,7 +91,7 @@ export async function POST(req: NextRequest, { params }: RouteParams) {
const request = await createEnrollmentRequest({
classroomId,
playerId: body.playerId,
- requestedBy: viewerId,
+ requestedBy: user.id,
requestedByRole,
})
diff --git a/apps/web/src/app/api/classrooms/[classroomId]/enrollments/[playerId]/route.ts b/apps/web/src/app/api/classrooms/[classroomId]/enrollments/[playerId]/route.ts
index fd77774e..c34bf220 100644
--- a/apps/web/src/app/api/classrooms/[classroomId]/enrollments/[playerId]/route.ts
+++ b/apps/web/src/app/api/classrooms/[classroomId]/enrollments/[playerId]/route.ts
@@ -1,7 +1,25 @@
+import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
+import { db, schema } from '@/db'
import { unenrollStudent, getTeacherClassroom, isParent } from '@/lib/classroom'
import { getViewerId } from '@/lib/viewer'
+/**
+ * Get or create user record for a viewerId (guestId)
+ */
+async function getOrCreateUser(viewerId: string) {
+ let user = await db.query.users.findFirst({
+ where: eq(schema.users.guestId, viewerId),
+ })
+
+ if (!user) {
+ const [newUser] = await db.insert(schema.users).values({ guestId: viewerId }).returning()
+ user = newUser
+ }
+
+ return user
+}
+
interface RouteParams {
params: Promise<{ classroomId: string; playerId: string }>
}
@@ -16,11 +34,12 @@ export async function DELETE(req: NextRequest, { params }: RouteParams) {
try {
const { classroomId, playerId } = await params
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
// Check authorization: must be teacher of classroom OR parent of student
- const classroom = await getTeacherClassroom(viewerId)
+ const classroom = await getTeacherClassroom(user.id)
const isTeacher = classroom?.id === classroomId
- const parentCheck = await isParent(viewerId, playerId)
+ const parentCheck = await isParent(user.id, playerId)
if (!isTeacher && !parentCheck) {
return NextResponse.json(
diff --git a/apps/web/src/app/api/classrooms/[classroomId]/enrollments/route.ts b/apps/web/src/app/api/classrooms/[classroomId]/enrollments/route.ts
index ec5dcc7e..c532cb76 100644
--- a/apps/web/src/app/api/classrooms/[classroomId]/enrollments/route.ts
+++ b/apps/web/src/app/api/classrooms/[classroomId]/enrollments/route.ts
@@ -1,7 +1,25 @@
+import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
+import { db, schema } from '@/db'
import { getEnrolledStudents, getTeacherClassroom } from '@/lib/classroom'
import { getViewerId } from '@/lib/viewer'
+/**
+ * Get or create user record for a viewerId (guestId)
+ */
+async function getOrCreateUser(viewerId: string) {
+ let user = await db.query.users.findFirst({
+ where: eq(schema.users.guestId, viewerId),
+ })
+
+ if (!user) {
+ const [newUser] = await db.insert(schema.users).values({ guestId: viewerId }).returning()
+ user = newUser
+ }
+
+ return user
+}
+
interface RouteParams {
params: Promise<{ classroomId: string }>
}
@@ -16,9 +34,10 @@ export async function GET(req: NextRequest, { params }: RouteParams) {
try {
const { classroomId } = await params
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
// Verify user is the teacher of this classroom
- const classroom = await getTeacherClassroom(viewerId)
+ const classroom = await getTeacherClassroom(user.id)
if (!classroom || classroom.id !== classroomId) {
return NextResponse.json({ error: 'Not authorized' }, { status: 403 })
}
diff --git a/apps/web/src/app/api/classrooms/[classroomId]/presence/[playerId]/route.ts b/apps/web/src/app/api/classrooms/[classroomId]/presence/[playerId]/route.ts
index 41a53f5f..62eed5e4 100644
--- a/apps/web/src/app/api/classrooms/[classroomId]/presence/[playerId]/route.ts
+++ b/apps/web/src/app/api/classrooms/[classroomId]/presence/[playerId]/route.ts
@@ -1,7 +1,25 @@
+import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
+import { db, schema } from '@/db'
import { leaveSpecificClassroom, getTeacherClassroom, isParent } from '@/lib/classroom'
import { getViewerId } from '@/lib/viewer'
+/**
+ * Get or create user record for a viewerId (guestId)
+ */
+async function getOrCreateUser(viewerId: string) {
+ let user = await db.query.users.findFirst({
+ where: eq(schema.users.guestId, viewerId),
+ })
+
+ if (!user) {
+ const [newUser] = await db.insert(schema.users).values({ guestId: viewerId }).returning()
+ user = newUser
+ }
+
+ return user
+}
+
interface RouteParams {
params: Promise<{ classroomId: string; playerId: string }>
}
@@ -16,11 +34,12 @@ export async function DELETE(req: NextRequest, { params }: RouteParams) {
try {
const { classroomId, playerId } = await params
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
// Check authorization: must be teacher of classroom OR parent of student
- const classroom = await getTeacherClassroom(viewerId)
+ const classroom = await getTeacherClassroom(user.id)
const isTeacher = classroom?.id === classroomId
- const parentCheck = await isParent(viewerId, playerId)
+ const parentCheck = await isParent(user.id, playerId)
if (!isTeacher && !parentCheck) {
return NextResponse.json(
diff --git a/apps/web/src/app/api/classrooms/[classroomId]/presence/route.ts b/apps/web/src/app/api/classrooms/[classroomId]/presence/route.ts
index e56bc141..8d28cf59 100644
--- a/apps/web/src/app/api/classrooms/[classroomId]/presence/route.ts
+++ b/apps/web/src/app/api/classrooms/[classroomId]/presence/route.ts
@@ -1,4 +1,6 @@
+import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
+import { db, schema } from '@/db'
import {
enterClassroom,
getClassroomPresence,
@@ -7,6 +9,22 @@ import {
} from '@/lib/classroom'
import { getViewerId } from '@/lib/viewer'
+/**
+ * Get or create user record for a viewerId (guestId)
+ */
+async function getOrCreateUser(viewerId: string) {
+ let user = await db.query.users.findFirst({
+ where: eq(schema.users.guestId, viewerId),
+ })
+
+ if (!user) {
+ const [newUser] = await db.insert(schema.users).values({ guestId: viewerId }).returning()
+ user = newUser
+ }
+
+ return user
+}
+
interface RouteParams {
params: Promise<{ classroomId: string }>
}
@@ -21,9 +39,10 @@ export async function GET(req: NextRequest, { params }: RouteParams) {
try {
const { classroomId } = await params
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
// Verify user is the teacher of this classroom
- const classroom = await getTeacherClassroom(viewerId)
+ const classroom = await getTeacherClassroom(user.id)
if (!classroom || classroom.id !== classroomId) {
return NextResponse.json({ error: 'Not authorized' }, { status: 403 })
}
@@ -55,6 +74,7 @@ export async function POST(req: NextRequest, { params }: RouteParams) {
try {
const { classroomId } = await params
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
const body = await req.json()
if (!body.playerId) {
@@ -62,9 +82,9 @@ export async function POST(req: NextRequest, { params }: RouteParams) {
}
// Check authorization: must be teacher of classroom OR parent of student
- const classroom = await getTeacherClassroom(viewerId)
+ const classroom = await getTeacherClassroom(user.id)
const isTeacher = classroom?.id === classroomId
- const parentCheck = await isParent(viewerId, body.playerId)
+ const parentCheck = await isParent(user.id, body.playerId)
if (!isTeacher && !parentCheck) {
return NextResponse.json(
@@ -76,7 +96,7 @@ export async function POST(req: NextRequest, { params }: RouteParams) {
const result = await enterClassroom({
playerId: body.playerId,
classroomId,
- enteredBy: viewerId,
+ enteredBy: user.id,
})
if (!result.success) {
diff --git a/apps/web/src/app/api/classrooms/[classroomId]/route.ts b/apps/web/src/app/api/classrooms/[classroomId]/route.ts
index ebe277ec..48b28647 100644
--- a/apps/web/src/app/api/classrooms/[classroomId]/route.ts
+++ b/apps/web/src/app/api/classrooms/[classroomId]/route.ts
@@ -1,4 +1,6 @@
+import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
+import { db, schema } from '@/db'
import {
deleteClassroom,
getClassroom,
@@ -7,6 +9,22 @@ import {
} from '@/lib/classroom'
import { getViewerId } from '@/lib/viewer'
+/**
+ * Get or create user record for a viewerId (guestId)
+ */
+async function getOrCreateUser(viewerId: string) {
+ let user = await db.query.users.findFirst({
+ where: eq(schema.users.guestId, viewerId),
+ })
+
+ if (!user) {
+ const [newUser] = await db.insert(schema.users).values({ guestId: viewerId }).returning()
+ user = newUser
+ }
+
+ return user
+}
+
interface RouteParams {
params: Promise<{ classroomId: string }>
}
@@ -45,11 +63,12 @@ export async function PATCH(req: NextRequest, { params }: RouteParams) {
try {
const { classroomId } = await params
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
const body = await req.json()
// Handle code regeneration separately
if (body.regenerateCode) {
- const newCode = await regenerateClassroomCode(classroomId, viewerId)
+ const newCode = await regenerateClassroomCode(classroomId, user.id)
if (!newCode) {
return NextResponse.json(
{ error: 'Not authorized or classroom not found' },
@@ -69,7 +88,7 @@ export async function PATCH(req: NextRequest, { params }: RouteParams) {
return NextResponse.json({ error: 'No valid updates provided' }, { status: 400 })
}
- const classroom = await updateClassroom(classroomId, viewerId, updates)
+ const classroom = await updateClassroom(classroomId, user.id, updates)
if (!classroom) {
return NextResponse.json({ error: 'Not authorized or classroom not found' }, { status: 403 })
@@ -92,8 +111,9 @@ export async function DELETE(req: NextRequest, { params }: RouteParams) {
try {
const { classroomId } = await params
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
- const success = await deleteClassroom(classroomId, viewerId)
+ const success = await deleteClassroom(classroomId, user.id)
if (!success) {
return NextResponse.json({ error: 'Not authorized or classroom not found' }, { status: 403 })
diff --git a/apps/web/src/app/practice/PracticeClient.tsx b/apps/web/src/app/practice/PracticeClient.tsx
index 1a9e7c5e..65151efb 100644
--- a/apps/web/src/app/practice/PracticeClient.tsx
+++ b/apps/web/src/app/practice/PracticeClient.tsx
@@ -3,7 +3,7 @@
import { useRouter } from 'next/navigation'
import { useCallback, useMemo, useState } from 'react'
import { Z_INDEX } from '@/constants/zIndex'
-import { ClassroomDashboard, CreateClassroomForm } from '@/components/classroom'
+import { ClassroomDashboard, CreateClassroomForm, EnrollChildFlow } from '@/components/classroom'
import { PageWithNav } from '@/components/PageWithNav'
import { StudentFilterBar } from '@/components/practice/StudentFilterBar'
import { StudentSelector, type StudentWithProgress } from '@/components/practice'
@@ -34,6 +34,7 @@ export function PracticeClient({ initialPlayers }: PracticeClientProps) {
// Classroom state - check if user is a teacher
const { data: classroom, isLoading: isLoadingClassroom } = useMyClassroom()
const [showCreateClassroom, setShowCreateClassroom] = useState(false)
+ const [showEnrollChild, setShowEnrollChild] = useState(false)
// Filter state
const [searchQuery, setSearchQuery] = useState('')
@@ -190,6 +191,15 @@ export function PracticeClient({ initialPlayers }: PracticeClientProps) {
setShowCreateClassroom(false)
}, [])
+ // Handle enrollment flow
+ const handleEnrollChild = useCallback(() => {
+ setShowEnrollChild(true)
+ }, [])
+
+ const handleCloseEnrollChild = useCallback(() => {
+ setShowEnrollChild(false)
+ }, [])
+
// If user is a teacher, show the classroom dashboard
if (classroom) {
return (
@@ -235,6 +245,31 @@ export function PracticeClient({ initialPlayers }: PracticeClientProps) {
)
}
+ // Show enroll child flow if requested
+ if (showEnrollChild) {
+ return (
+