diff --git a/apps/web/src/app/practice/AddStudentModal.tsx b/apps/web/src/app/practice/AddStudentModal.tsx index 7a78219d..f0d02739 100644 --- a/apps/web/src/app/practice/AddStudentModal.tsx +++ b/apps/web/src/app/practice/AddStudentModal.tsx @@ -453,56 +453,60 @@ export function AddStudentModal({ - {/* Link existing child option */} -
-

- Have a family code from another parent? -

- -
+

+ Have a family code from another parent? +

+ + + )} - {/* Link Child Form Modal */} - setShowLinkForm(false)} - onSuccess={onClose} - /> + {/* Link Child Form Modal - only used in parent mode */} + {!classroomId && ( + setShowLinkForm(false)} + onSuccess={onClose} + /> + )} ) } diff --git a/apps/web/src/app/practice/PracticeClient.tsx b/apps/web/src/app/practice/PracticeClient.tsx index 50a19e89..feb0a09d 100644 --- a/apps/web/src/app/practice/PracticeClient.tsx +++ b/apps/web/src/app/practice/PracticeClient.tsx @@ -311,12 +311,6 @@ export function PracticeClient({ initialPlayers, viewerId, userId }: PracticeCli setShowUnifiedAddModal(true) }, []) - // Handle create student from unified modal - opens create modal with auto-enroll - const handleCreateStudentFromUnified = useCallback(() => { - setAddToClassroomMode(true) - setShowAddModal(true) - }, []) - const handleCloseAddModal = useCallback(() => { setShowAddModal(false) setAddToClassroomMode(false) @@ -726,7 +720,6 @@ export function PracticeClient({ initialPlayers, viewerId, userId }: PracticeCli classroomId={classroomId} classroomName={classroom.name} classroomCode={classroomCode} - onCreateStudent={handleCreateStudentFromUnified} /> )} diff --git a/apps/web/src/components/classroom/AddStudentToClassroomModal.tsx b/apps/web/src/components/classroom/AddStudentToClassroomModal.tsx index 939ae375..ca8ebbf7 100644 --- a/apps/web/src/components/classroom/AddStudentToClassroomModal.tsx +++ b/apps/web/src/components/classroom/AddStudentToClassroomModal.tsx @@ -1,13 +1,33 @@ 'use client' import * as Dialog from '@radix-ui/react-dialog' -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { AbacusQRCode } from '@/components/common/AbacusQRCode' +import { EmojiPicker } from '@/components/EmojiPicker' import { Z_INDEX } from '@/constants/zIndex' +import { PLAYER_EMOJIS } from '@/constants/playerEmojis' import { useTheme } from '@/contexts/ThemeContext' +import { useDirectEnrollStudent } from '@/hooks/useClassroom' import { useShareCode } from '@/hooks/useShareCode' +import { useCreatePlayer } from '@/hooks/useUserPlayers' import { css } from '../../../styled-system/css' +// Available colors for student avatars +const AVAILABLE_COLORS = [ + '#FFB3BA', // light pink + '#FFDFBA', // light orange + '#FFFFBA', // light yellow + '#BAFFC9', // light green + '#BAE1FF', // light blue + '#DCC6E0', // light purple + '#F0E68C', // khaki + '#98D8C8', // mint + '#F7DC6F', // gold + '#BB8FCE', // orchid + '#85C1E9', // sky blue + '#F8B500', // amber +] + interface PlayerPreview { id: string name: string @@ -15,24 +35,24 @@ interface PlayerPreview { color: string } +type LeftColumnMode = 'choose' | 'create' | 'family-success' + interface AddStudentToClassroomModalProps { isOpen: boolean onClose: () => void classroomId: string classroomName: string classroomCode: string - /** Called when user wants to create a new student (opens the create student modal) */ - onCreateStudent: () => void } /** * Unified modal for adding students to a classroom. * * Two columns: - * - Left: "ADD NOW" - Create student button + Enter family code + * - Left: "ADD NOW" - Create student form OR enter family code * - Right: "INVITE PARENTS" - Share classroom code with QR * - * This consolidates multiple add-student flows into a single discoverable UI. + * The create student form is integrated inline (not a separate modal). */ export function AddStudentToClassroomModal({ isOpen, @@ -40,21 +60,46 @@ export function AddStudentToClassroomModal({ classroomId, classroomName, classroomCode, - onCreateStudent, }: AddStudentToClassroomModalProps) { const { resolvedTheme } = useTheme() const isDark = resolvedTheme === 'dark' + // Left column mode: choose (initial), create (form), or family-success + const [leftMode, setLeftMode] = useState('choose') + + // Create student form state + const [formName, setFormName] = useState('') + const [formEmoji, setFormEmoji] = useState(PLAYER_EMOJIS[0]) + const [formColor, setFormColor] = useState(AVAILABLE_COLORS[0]) + const [showEmojiPicker, setShowEmojiPicker] = useState(false) + // Family code input state const [familyCode, setFamilyCode] = useState('') const [isSubmittingFamilyCode, setIsSubmittingFamilyCode] = useState(false) const [familyCodeError, setFamilyCodeError] = useState(null) - const [familyCodeSuccess, setFamilyCodeSuccess] = useState(false) const [enrolledPlayer, setEnrolledPlayer] = useState(null) + // Mutations + const createPlayer = useCreatePlayer() + const directEnroll = useDirectEnrollStudent() + // Share code hook for the classroom const shareCode = useShareCode({ type: 'classroom', code: classroomCode }) + // Reset state when modal opens + useEffect(() => { + if (isOpen) { + setLeftMode('choose') + setFormName('') + setFormEmoji(PLAYER_EMOJIS[Math.floor(Math.random() * PLAYER_EMOJIS.length)]) + setFormColor(AVAILABLE_COLORS[Math.floor(Math.random() * AVAILABLE_COLORS.length)]) + setShowEmojiPicker(false) + setFamilyCode('') + setFamilyCodeError(null) + setEnrolledPlayer(null) + } + }, [isOpen]) + const handleFamilyCodeSubmit = useCallback(async () => { if (!familyCode.trim()) { setFamilyCodeError('Please enter a family code') @@ -79,7 +124,7 @@ export function AddStudentToClassroomModal({ } setEnrolledPlayer(data.player) - setFamilyCodeSuccess(true) + setLeftMode('family-success') } catch (_err) { setFamilyCodeError('Failed to add student. Please try again.') } finally { @@ -87,27 +132,72 @@ export function AddStudentToClassroomModal({ } }, [familyCode, classroomId]) + const handleCreateStudentSubmit = useCallback(() => { + if (!formName.trim()) return + + createPlayer.mutate( + { + name: formName.trim(), + emoji: formEmoji, + color: formColor, + }, + { + onSuccess: (player) => { + if (player) { + directEnroll.mutate( + { classroomId, playerId: player.id }, + { + onSettled: () => { + onClose() + }, + } + ) + } else { + onClose() + } + }, + } + ) + }, [formName, formEmoji, formColor, createPlayer, classroomId, directEnroll, onClose]) + const handleClose = useCallback(() => { - // Reset state - setFamilyCode('') - setFamilyCodeError(null) - setFamilyCodeSuccess(false) - setEnrolledPlayer(null) onClose() }, [onClose]) - const handleCreateStudent = useCallback(() => { - handleClose() - onCreateStudent() - }, [handleClose, onCreateStudent]) - - const handleAddAnother = useCallback(() => { + const handleBackToChoose = useCallback(() => { + setLeftMode('choose') setFamilyCode('') setFamilyCodeError(null) - setFamilyCodeSuccess(false) setEnrolledPlayer(null) }, []) + const handleStartCreate = useCallback(() => { + // Randomize emoji and color when entering create mode + setFormEmoji(PLAYER_EMOJIS[Math.floor(Math.random() * PLAYER_EMOJIS.length)]) + setFormColor(AVAILABLE_COLORS[Math.floor(Math.random() * AVAILABLE_COLORS.length)]) + setFormName('') + setLeftMode('create') + }, []) + + const isPending = createPlayer.isPending || directEnroll.isPending + + // Show emoji picker as overlay + if (showEmojiPicker) { + return ( + { + setFormEmoji(emoji) + setShowEmojiPicker(false) + }} + onClose={() => setShowEmojiPicker(false)} + title="Choose Avatar" + accentColor="green" + isDark={isDark} + /> + ) + } + return ( !open && handleClose()}> @@ -130,7 +220,7 @@ export function AddStudentToClassroomModal({ backgroundColor: isDark ? 'gray.800' : 'white', borderRadius: '16px', width: 'calc(100% - 2rem)', - maxWidth: { base: '400px', sm: '700px' }, + maxWidth: '700px', maxHeight: '90vh', overflowY: 'auto', boxShadow: '0 20px 50px -12px rgba(0, 0, 0, 0.4)', @@ -191,7 +281,7 @@ export function AddStudentToClassroomModal({ data-element="modal-content" className={css({ display: 'grid', - gridTemplateColumns: { base: '1fr', sm: '1fr 1fr' }, + gridTemplateColumns: { base: '1fr', md: '1fr 1fr' }, gap: '0', })} > @@ -200,8 +290,8 @@ export function AddStudentToClassroomModal({ data-section="add-now" className={css({ padding: '20px', - borderRight: { base: 'none', sm: '1px solid' }, - borderBottom: { base: '1px solid', sm: 'none' }, + borderRight: { base: 'none', md: '1px solid' }, + borderBottom: { base: '1px solid', md: 'none' }, borderColor: isDark ? 'gray.700' : 'gray.200', })} > @@ -218,107 +308,44 @@ export function AddStudentToClassroomModal({ Add Now - {/* Create Student Button */} - -

- Quick setup - student doesn't need an existing account -

- - {/* Divider */} -
-
- - or - -
-
- - {/* Family Code Section */} - {familyCodeSuccess && enrolledPlayer ? ( - - ) : ( - { + {leftMode === 'choose' && ( + { setFamilyCode(val) setFamilyCodeError(null) }} - onSubmit={handleFamilyCodeSubmit} - isSubmitting={isSubmittingFamilyCode} - error={familyCodeError} + onFamilyCodeSubmit={handleFamilyCodeSubmit} + isSubmittingFamilyCode={isSubmittingFamilyCode} + familyCodeError={familyCodeError} isDark={isDark} /> )} + + {leftMode === 'create' && ( + setShowEmojiPicker(true)} + color={formColor} + onColorChange={setFormColor} + onSubmit={handleCreateStudentSubmit} + onCancel={handleBackToChoose} + isPending={isPending} + isDark={isDark} + /> + )} + + {leftMode === 'family-success' && enrolledPlayer && ( + + )}
{/* Right column: INVITE PARENTS */} @@ -516,113 +543,414 @@ export function AddStudentToClassroomModal({ // --- Sub-components --- -interface FamilyCodeInputProps { - value: string - onChange: (value: string) => void - onSubmit: () => void - isSubmitting: boolean - error: string | null +interface ChooseSectionProps { + onCreateStudent: () => void + familyCode: string + onFamilyCodeChange: (value: string) => void + onFamilyCodeSubmit: () => void + isSubmittingFamilyCode: boolean + familyCodeError: string | null isDark: boolean } -function FamilyCodeInput({ - value, - onChange, - onSubmit, - isSubmitting, - error, +function ChooseSection({ + onCreateStudent, + familyCode, + onFamilyCodeChange, + onFamilyCodeSubmit, + isSubmittingFamilyCode, + familyCodeError, isDark, -}: FamilyCodeInputProps) { +}: ChooseSectionProps) { return ( -
-