From 2202716f563053624dbe5c6abb969a3b0d452fd1 Mon Sep 17 00:00:00 2001
From: Thomas Hallock
Date: Mon, 22 Dec 2025 13:07:40 -0600
Subject: [PATCH] feat(classroom): implement teacher classroom dashboard (Phase
3)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add teacher classroom functionality allowing users to create a classroom
and manage students:
- Add classroom query keys for React Query cache management
- Create useClassroom hooks (useMyClassroom, useClassroomByCode,
useCreateClassroom, useIsTeacher)
- Add classroom UI components:
- ClassroomCodeShare: Display join code with copy button
- CreateClassroomForm: Form to create a classroom
- ClassroomTab: Live classroom view (empty state for Phase 6)
- StudentManagerTab: Enrolled students list (empty state for Phase 4)
- ClassroomDashboard: Main teacher dashboard with tabs
- Integrate into PracticeClient with conditional routing:
- Teachers see ClassroomDashboard with own children shown separately
- Parents see normal student list with "Become a Teacher" option
- Fix API route to remove non-existent 'image' field from User type
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5
---
.../app/api/classrooms/code/[code]/route.ts | 1 -
apps/web/src/app/api/classrooms/mine/route.ts | 21 +-
apps/web/src/app/api/classrooms/route.ts | 24 +-
apps/web/src/app/api/family/link/route.ts | 5 +-
apps/web/src/app/api/players/route.ts | 5 +-
apps/web/src/app/practice/PracticeClient.tsx | 88 +++++++
.../classroom/ClassroomCodeShare.tsx | 149 ++++++++++++
.../classroom/ClassroomDashboard.tsx | 227 ++++++++++++++++++
.../src/components/classroom/ClassroomTab.tsx | 116 +++++++++
.../classroom/CreateClassroomForm.tsx | 182 ++++++++++++++
.../classroom/StudentManagerTab.tsx | 117 +++++++++
apps/web/src/components/classroom/index.ts | 5 +
apps/web/src/hooks/useClassroom.ts | 129 +++++-----
apps/web/src/lib/queryKeys.ts | 10 +
14 files changed, 1007 insertions(+), 72 deletions(-)
create mode 100644 apps/web/src/components/classroom/ClassroomCodeShare.tsx
create mode 100644 apps/web/src/components/classroom/ClassroomDashboard.tsx
create mode 100644 apps/web/src/components/classroom/ClassroomTab.tsx
create mode 100644 apps/web/src/components/classroom/CreateClassroomForm.tsx
create mode 100644 apps/web/src/components/classroom/StudentManagerTab.tsx
create mode 100644 apps/web/src/components/classroom/index.ts
diff --git a/apps/web/src/app/api/classrooms/code/[code]/route.ts b/apps/web/src/app/api/classrooms/code/[code]/route.ts
index bba7b92e..05f9954f 100644
--- a/apps/web/src/app/api/classrooms/code/[code]/route.ts
+++ b/apps/web/src/app/api/classrooms/code/[code]/route.ts
@@ -32,7 +32,6 @@ export async function GET(req: NextRequest, { params }: RouteParams) {
? {
id: classroom.teacher.id,
name: classroom.teacher.name,
- image: classroom.teacher.image,
}
: null,
})
diff --git a/apps/web/src/app/api/classrooms/mine/route.ts b/apps/web/src/app/api/classrooms/mine/route.ts
index 67198318..652ce9b2 100644
--- a/apps/web/src/app/api/classrooms/mine/route.ts
+++ b/apps/web/src/app/api/classrooms/mine/route.ts
@@ -1,7 +1,25 @@
+import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
+import { db, schema } from '@/db'
import { 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
+}
+
/**
* GET /api/classrooms/mine
* Get current user's classroom (if teacher)
@@ -11,8 +29,9 @@ import { getViewerId } from '@/lib/viewer'
export async function GET() {
try {
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
- const classroom = await getTeacherClassroom(viewerId)
+ const classroom = await getTeacherClassroom(user.id)
if (!classroom) {
return NextResponse.json({ error: 'No classroom found' }, { status: 404 })
diff --git a/apps/web/src/app/api/classrooms/route.ts b/apps/web/src/app/api/classrooms/route.ts
index f061c6ee..92f03d9b 100644
--- a/apps/web/src/app/api/classrooms/route.ts
+++ b/apps/web/src/app/api/classrooms/route.ts
@@ -1,7 +1,25 @@
+import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
+import { db, schema } from '@/db'
import { createClassroom, 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
+}
+
/**
* GET /api/classrooms
* Get current user's classroom (alias for /api/classrooms/mine)
@@ -11,8 +29,9 @@ import { getViewerId } from '@/lib/viewer'
export async function GET() {
try {
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
- const classroom = await getTeacherClassroom(viewerId)
+ const classroom = await getTeacherClassroom(user.id)
return NextResponse.json({ classroom })
} catch (error) {
@@ -31,6 +50,7 @@ export async function GET() {
export async function POST(req: NextRequest) {
try {
const viewerId = await getViewerId()
+ const user = await getOrCreateUser(viewerId)
const body = await req.json()
if (!body.name) {
@@ -38,7 +58,7 @@ export async function POST(req: NextRequest) {
}
const result = await createClassroom({
- teacherId: viewerId,
+ teacherId: user.id,
name: body.name,
})
diff --git a/apps/web/src/app/api/family/link/route.ts b/apps/web/src/app/api/family/link/route.ts
index dd60abf1..2911e082 100644
--- a/apps/web/src/app/api/family/link/route.ts
+++ b/apps/web/src/app/api/family/link/route.ts
@@ -13,10 +13,7 @@ async function getOrCreateUser(viewerId: string) {
})
if (!user) {
- const [newUser] = await db
- .insert(schema.users)
- .values({ guestId: viewerId })
- .returning()
+ const [newUser] = await db.insert(schema.users).values({ guestId: viewerId }).returning()
user = newUser
}
diff --git a/apps/web/src/app/api/players/route.ts b/apps/web/src/app/api/players/route.ts
index 117f6243..524b1188 100644
--- a/apps/web/src/app/api/players/route.ts
+++ b/apps/web/src/app/api/players/route.ts
@@ -26,10 +26,7 @@ export async function GET() {
let players
if (linkedIds.length > 0) {
players = await db.query.players.findMany({
- where: or(
- eq(schema.players.userId, user.id),
- inArray(schema.players.id, linkedIds)
- ),
+ where: or(eq(schema.players.userId, user.id), inArray(schema.players.id, linkedIds)),
orderBy: (players, { desc }) => [desc(players.createdAt)],
})
} else {
diff --git a/apps/web/src/app/practice/PracticeClient.tsx b/apps/web/src/app/practice/PracticeClient.tsx
index 5849b8f2..1a9e7c5e 100644
--- a/apps/web/src/app/practice/PracticeClient.tsx
+++ b/apps/web/src/app/practice/PracticeClient.tsx
@@ -3,10 +3,12 @@
import { useRouter } from 'next/navigation'
import { useCallback, useMemo, useState } from 'react'
import { Z_INDEX } from '@/constants/zIndex'
+import { ClassroomDashboard, CreateClassroomForm } from '@/components/classroom'
import { PageWithNav } from '@/components/PageWithNav'
import { StudentFilterBar } from '@/components/practice/StudentFilterBar'
import { StudentSelector, type StudentWithProgress } from '@/components/practice'
import { useTheme } from '@/contexts/ThemeContext'
+import { useMyClassroom } from '@/hooks/useClassroom'
import { usePlayersWithSkillData, useUpdatePlayer } from '@/hooks/useUserPlayers'
import type { StudentWithSkillData } from '@/utils/studentGrouping'
import { filterStudents, getStudentsNeedingAttention, groupStudents } from '@/utils/studentGrouping'
@@ -29,6 +31,10 @@ export function PracticeClient({ initialPlayers }: PracticeClientProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
+ // Classroom state - check if user is a teacher
+ const { data: classroom, isLoading: isLoadingClassroom } = useMyClassroom()
+ const [showCreateClassroom, setShowCreateClassroom] = useState(false)
+
// Filter state
const [searchQuery, setSearchQuery] = useState('')
const [skillFilters, setSkillFilters] = useState([])
@@ -175,6 +181,61 @@ export function PracticeClient({ initialPlayers }: PracticeClientProps) {
.length
}, [players, searchQuery, skillFilters, showArchived])
+ // Handle classroom creation
+ const handleBecomeTeacher = useCallback(() => {
+ setShowCreateClassroom(true)
+ }, [])
+
+ const handleCloseCreateClassroom = useCallback(() => {
+ setShowCreateClassroom(false)
+ }, [])
+
+ // If user is a teacher, show the classroom dashboard
+ if (classroom) {
+ return (
+
+
+
+
+
+ )
+ }
+
+ // Show create classroom modal if requested
+ if (showCreateClassroom) {
+ return (
+
+
+
+
+
+
+
+ )
+ }
+
+ // Parent view - show student list with filter bar
return (
Build your soroban skills one step at a time
+
+ {/* Become a Teacher option */}
+ {!isLoadingClassroom && !classroom && (
+
+ )}
{/* Needs Attention Section - uses same bucket styling as other sections */}
diff --git a/apps/web/src/components/classroom/ClassroomCodeShare.tsx b/apps/web/src/components/classroom/ClassroomCodeShare.tsx
new file mode 100644
index 00000000..c82cf935
--- /dev/null
+++ b/apps/web/src/components/classroom/ClassroomCodeShare.tsx
@@ -0,0 +1,149 @@
+'use client'
+
+import { useCallback, useState } from 'react'
+import { useTheme } from '@/contexts/ThemeContext'
+import { css } from '../../../styled-system/css'
+
+interface ClassroomCodeShareProps {
+ code: string
+ /** Compact display for inline use */
+ compact?: boolean
+}
+
+/**
+ * Display classroom join code with copy button
+ */
+export function ClassroomCodeShare({ code, compact = false }: ClassroomCodeShareProps) {
+ const { resolvedTheme } = useTheme()
+ const isDark = resolvedTheme === 'dark'
+ const [copied, setCopied] = useState(false)
+
+ const handleCopy = useCallback(async () => {
+ try {
+ await navigator.clipboard.writeText(code)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ } catch {
+ // Fallback for older browsers
+ const textarea = document.createElement('textarea')
+ textarea.value = code
+ document.body.appendChild(textarea)
+ textarea.select()
+ document.execCommand('copy')
+ document.body.removeChild(textarea)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+ }, [code])
+
+ if (compact) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+ Classroom Code
+
+
+ {code}
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/classroom/ClassroomDashboard.tsx b/apps/web/src/components/classroom/ClassroomDashboard.tsx
new file mode 100644
index 00000000..4ceb6b83
--- /dev/null
+++ b/apps/web/src/components/classroom/ClassroomDashboard.tsx
@@ -0,0 +1,227 @@
+'use client'
+
+import { useState } from 'react'
+import type { Classroom, Player } from '@/db/schema'
+import { useTheme } from '@/contexts/ThemeContext'
+import { css } from '../../../styled-system/css'
+import { ClassroomCodeShare } from './ClassroomCodeShare'
+import { ClassroomTab } from './ClassroomTab'
+import { StudentManagerTab } from './StudentManagerTab'
+
+type TabId = 'classroom' | 'students'
+
+interface ClassroomDashboardProps {
+ classroom: Classroom
+ /** Teacher's own children (get special "parent access" treatment) */
+ ownChildren?: Player[]
+}
+
+/**
+ * ClassroomDashboard - Main teacher dashboard
+ *
+ * Two tabs:
+ * - Classroom: Live view of present students
+ * - Student Manager: Enrolled students list with progress
+ *
+ * Teacher's own children appear separately with full parent access.
+ */
+export function ClassroomDashboard({ classroom, ownChildren = [] }: ClassroomDashboardProps) {
+ const { resolvedTheme } = useTheme()
+ const isDark = resolvedTheme === 'dark'
+ const [activeTab, setActiveTab] = useState('classroom')
+
+ const tabs: { id: TabId; label: string; icon: string }[] = [
+ { id: 'classroom', label: 'Classroom', icon: '🏫' },
+ { id: 'students', label: 'Student Manager', icon: '👥' },
+ ]
+
+ return (
+
+ {/* Header */}
+
+
+
+ {classroom.name}
+
+
+ Teacher Dashboard
+
+
+
+
+
+
+ {/* Own children section (if teacher has kids) */}
+ {ownChildren.length > 0 && (
+
+
+ Your Children
+
+
+ {ownChildren.map((child) => (
+
+
+ {child.emoji}
+
+
+ {child.name}
+
+
+ ))}
+
+
+ )}
+
+ {/* Tab navigation */}
+
+
+ {/* Tab content */}
+
+ {activeTab === 'classroom' ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/apps/web/src/components/classroom/ClassroomTab.tsx b/apps/web/src/components/classroom/ClassroomTab.tsx
new file mode 100644
index 00000000..07cbdbcf
--- /dev/null
+++ b/apps/web/src/components/classroom/ClassroomTab.tsx
@@ -0,0 +1,116 @@
+'use client'
+
+import type { Classroom } from '@/db/schema'
+import { useTheme } from '@/contexts/ThemeContext'
+import { css } from '../../../styled-system/css'
+import { ClassroomCodeShare } from './ClassroomCodeShare'
+
+interface ClassroomTabProps {
+ classroom: Classroom
+}
+
+/**
+ * ClassroomTab - Shows live classroom view
+ *
+ * Displays students currently "present" in the classroom.
+ * For Phase 3, this is an empty state.
+ * Phase 6 will add presence functionality.
+ */
+export function ClassroomTab({ classroom }: ClassroomTabProps) {
+ const { resolvedTheme } = useTheme()
+ const isDark = resolvedTheme === 'dark'
+
+ return (
+
+ {/* Empty state */}
+
+
+ 🏫
+
+
+ No Students Present
+
+
+ When students join your classroom for practice, they'll appear here. Share your classroom
+ code to get started.
+
+
+
+
+
+ {/* Instructions */}
+
+
+ How students join
+
+
+ - Share your classroom code with parents
+ - Parents enroll their child using the code
+ - Students appear here when they start practicing
+
+
+
+ )
+}
diff --git a/apps/web/src/components/classroom/CreateClassroomForm.tsx b/apps/web/src/components/classroom/CreateClassroomForm.tsx
new file mode 100644
index 00000000..5779dfec
--- /dev/null
+++ b/apps/web/src/components/classroom/CreateClassroomForm.tsx
@@ -0,0 +1,182 @@
+'use client'
+
+import { useCallback, useState } from 'react'
+import { useTheme } from '@/contexts/ThemeContext'
+import { useCreateClassroom } from '@/hooks/useClassroom'
+import { css } from '../../../styled-system/css'
+
+interface CreateClassroomFormProps {
+ onSuccess?: () => void
+ onCancel?: () => void
+}
+
+/**
+ * Form to create a new classroom (become a teacher)
+ */
+export function CreateClassroomForm({ onSuccess, onCancel }: CreateClassroomFormProps) {
+ const { resolvedTheme } = useTheme()
+ const isDark = resolvedTheme === 'dark'
+
+ const [name, setName] = useState('')
+ const createClassroom = useCreateClassroom()
+
+ const handleSubmit = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!name.trim()) return
+
+ try {
+ await createClassroom.mutateAsync({ name: name.trim() })
+ onSuccess?.()
+ } catch {
+ // Error is handled by mutation state
+ }
+ },
+ [name, createClassroom, onSuccess]
+ )
+
+ return (
+
+
+ Create Your Classroom
+
+
+ As a teacher, you can enroll students and observe their practice sessions.
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/classroom/StudentManagerTab.tsx b/apps/web/src/components/classroom/StudentManagerTab.tsx
new file mode 100644
index 00000000..1ccfdc1c
--- /dev/null
+++ b/apps/web/src/components/classroom/StudentManagerTab.tsx
@@ -0,0 +1,117 @@
+'use client'
+
+import type { Classroom } from '@/db/schema'
+import { useTheme } from '@/contexts/ThemeContext'
+import { css } from '../../../styled-system/css'
+import { ClassroomCodeShare } from './ClassroomCodeShare'
+
+interface StudentManagerTabProps {
+ classroom: Classroom
+}
+
+/**
+ * StudentManagerTab - Manage enrolled students
+ *
+ * Shows all students enrolled in the classroom.
+ * For Phase 3, this is an empty state.
+ * Phase 4 will add enrollment functionality.
+ */
+export function StudentManagerTab({ classroom }: StudentManagerTabProps) {
+ const { resolvedTheme } = useTheme()
+ const isDark = resolvedTheme === 'dark'
+
+ return (
+
+ {/* Empty state */}
+
+
+ 👥
+
+
+ No Students Enrolled
+
+
+ Parents can enroll their children using your classroom code. Once enrolled, you can view
+ their progress and skills here.
+
+
+
+
+
+ {/* What you'll see when students enroll */}
+
+
+ When students enroll, you can:
+
+
+ - View each student's skill mastery and progress
+ - See their practice session history
+ - Observe live practice sessions
+ - Guide them through tutorials remotely
+
+
+
+ )
+}
diff --git a/apps/web/src/components/classroom/index.ts b/apps/web/src/components/classroom/index.ts
new file mode 100644
index 00000000..955b4efc
--- /dev/null
+++ b/apps/web/src/components/classroom/index.ts
@@ -0,0 +1,5 @@
+export { ClassroomCodeShare } from './ClassroomCodeShare'
+export { ClassroomDashboard } from './ClassroomDashboard'
+export { ClassroomTab } from './ClassroomTab'
+export { CreateClassroomForm } from './CreateClassroomForm'
+export { StudentManagerTab } from './StudentManagerTab'
diff --git a/apps/web/src/hooks/useClassroom.ts b/apps/web/src/hooks/useClassroom.ts
index 441c44b4..87cfe9e0 100644
--- a/apps/web/src/hooks/useClassroom.ts
+++ b/apps/web/src/hooks/useClassroom.ts
@@ -1,76 +1,100 @@
'use client'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
-import type { Classroom } from '@/db/schema/classrooms'
+import type { Classroom, User } from '@/db/schema'
import { api } from '@/lib/queryClient'
+import { classroomKeys } from '@/lib/queryKeys'
-// Query keys for classroom data
-export const classroomKeys = {
- all: ['classroom'] as const,
- mine: () => [...classroomKeys.all, 'mine'] as const,
- detail: (id: string) => [...classroomKeys.all, 'detail', id] as const,
- byCode: (code: string) => [...classroomKeys.all, 'code', code] as const,
+// Re-export query keys for consumers
+export { classroomKeys } from '@/lib/queryKeys'
+
+// ============================================================================
+// Types
+// ============================================================================
+
+export interface ClassroomWithTeacher extends Classroom {
+ teacher?: User
}
+// ============================================================================
+// API Functions
+// ============================================================================
+
/**
* Fetch current user's classroom
*/
async function fetchMyClassroom(): Promise {
- const res = await api('classrooms')
- if (!res.ok) {
- // 404 means no classroom
- if (res.status === 404) return null
- throw new Error('Failed to fetch classroom')
- }
+ const res = await api('classrooms/mine')
+ if (res.status === 404) return null
+ if (!res.ok) throw new Error('Failed to fetch classroom')
const data = await res.json()
- return data.classroom ?? null
-}
-
-/**
- * Create a classroom
- */
-async function createClassroom(name: string): Promise {
- const res = await api('classrooms', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name }),
- })
- const data = await res.json()
- if (!res.ok || !data.success) {
- throw new Error(data.error || 'Failed to create classroom')
- }
return data.classroom
}
/**
- * Look up classroom by code
+ * Look up classroom by join code
*/
-async function fetchClassroomByCode(
- code: string
-): Promise<{ classroom: Classroom; teacherName: string } | null> {
- const res = await api(`classrooms/code/${code.toUpperCase()}`)
- if (!res.ok) {
- if (res.status === 404) return null
- throw new Error('Failed to look up classroom')
- }
+async function fetchClassroomByCode(code: string): Promise {
+ if (!code || code.length < 4) return null
+ const res = await api(`classrooms/code/${encodeURIComponent(code)}`)
+ if (res.status === 404) return null
+ if (!res.ok) throw new Error('Failed to fetch classroom')
const data = await res.json()
- return data
+ return data.classroom
}
/**
- * Hook: Get current user's classroom (if they're a teacher)
+ * Create a new classroom
+ */
+async function createClassroom(params: { name: string }): Promise {
+ const res = await api('classrooms', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(params),
+ })
+ if (!res.ok) {
+ const data = await res.json()
+ throw new Error(data.error || 'Failed to create classroom')
+ }
+ const data = await res.json()
+ return data.classroom
+}
+
+// ============================================================================
+// Hooks
+// ============================================================================
+
+/**
+ * Get current user's classroom (if they are a teacher)
*/
export function useMyClassroom() {
return useQuery({
queryKey: classroomKeys.mine(),
queryFn: fetchMyClassroom,
- // Don't refetch too aggressively - classrooms rarely change
- staleTime: 60_000, // 1 minute
+ staleTime: 5 * 60 * 1000, // 5 minutes
})
}
/**
- * Hook: Create a classroom
+ * Look up classroom by join code
+ *
+ * Use this when a parent wants to enroll their child.
+ * The query is disabled until code has at least 4 characters.
+ */
+export function useClassroomByCode(code: string) {
+ return useQuery({
+ queryKey: classroomKeys.byCode(code.toUpperCase()),
+ queryFn: () => fetchClassroomByCode(code),
+ enabled: code.length >= 4,
+ staleTime: 60 * 1000, // 1 minute
+ })
+}
+
+/**
+ * Create a new classroom
+ *
+ * Use this when a user wants to become a teacher.
+ * Each user can have only one classroom.
*/
export function useCreateClassroom() {
const queryClient = useQueryClient()
@@ -78,34 +102,19 @@ export function useCreateClassroom() {
return useMutation({
mutationFn: createClassroom,
onSuccess: (classroom) => {
- // Update the cache with the new classroom
+ // Update the 'mine' query with the new classroom
queryClient.setQueryData(classroomKeys.mine(), classroom)
- // Invalidate to ensure consistency
- queryClient.invalidateQueries({ queryKey: classroomKeys.all })
},
})
}
-/**
- * Hook: Look up classroom by join code
- */
-export function useClassroomByCode(code: string | null) {
- return useQuery({
- queryKey: classroomKeys.byCode(code ?? ''),
- queryFn: () => (code ? fetchClassroomByCode(code) : null),
- enabled: !!code && code.length >= 4, // Only query if code is entered
- staleTime: 30_000, // 30 seconds
- })
-}
-
/**
* Check if current user is a teacher (has a classroom)
*/
export function useIsTeacher() {
const { data: classroom, isLoading } = useMyClassroom()
return {
- isTeacher: classroom !== null && classroom !== undefined,
+ isTeacher: classroom !== null,
isLoading,
- classroom,
}
}
diff --git a/apps/web/src/lib/queryKeys.ts b/apps/web/src/lib/queryKeys.ts
index 5af98b48..1a30eb89 100644
--- a/apps/web/src/lib/queryKeys.ts
+++ b/apps/web/src/lib/queryKeys.ts
@@ -34,3 +34,13 @@ export const sessionHistoryKeys = {
all: ['sessionHistory'] as const,
list: (playerId: string) => [...sessionHistoryKeys.all, playerId] as const,
}
+
+// Classroom query keys
+export const classroomKeys = {
+ all: ['classrooms'] as const,
+ mine: () => [...classroomKeys.all, 'mine'] as const,
+ byCode: (code: string) => [...classroomKeys.all, 'byCode', code] as const,
+ detail: (id: string) => [...classroomKeys.all, 'detail', id] as const,
+ enrollments: (id: string) => [...classroomKeys.all, 'enrollments', id] as const,
+ presence: (id: string) => [...classroomKeys.all, 'presence', id] as const,
+}