diff --git a/App.tsx b/App.tsx index 1bcfac8..8f0e380 100644 --- a/App.tsx +++ b/App.tsx @@ -137,10 +137,10 @@ function App() { }); return ( -
+
-
+
{gameState === 'LANDING' || gameState === 'GENERATING' ? ( {auth.isAuthenticated && pendingQuizToSave && ( void; - apiKey: string | undefined; - onSave: (key: string | undefined) => Promise; - saving: boolean; - hasAIAccess: boolean; -} - -export const ApiKeyModal: React.FC = ({ - isOpen, - onClose, - apiKey, - onSave, - saving, - hasAIAccess, -}) => { - useBodyScrollLock(isOpen); - const [localApiKey, setLocalApiKey] = useState(apiKey || ''); - const [showApiKey, setShowApiKey] = useState(false); - - useEffect(() => { - if (isOpen) { - setLocalApiKey(apiKey || ''); - } - }, [isOpen, apiKey]); - - if (!isOpen) return null; - - const handleSave = async () => { - await onSave(localApiKey || undefined); - onClose(); - }; - - return ( - - e.stopPropagation()} - > -
-
-
- -
-
-

Account Settings

-

Manage your API access

-
-
- -
- -
- {hasAIAccess ? ( -
-

- - Custom Gemini API Key -

-

- Use your own API key for quiz generation. Leave empty to use the system key. -

-
- setLocalApiKey(e.target.value)} - placeholder="AIza..." - className="w-full px-4 py-3 pr-12 rounded-xl border-2 border-gray-200 focus:border-theme-primary focus:outline-none text-gray-800" - /> - -
-
- ) : ( -
- -

API access not available

-

Contact an administrator for access

-
- )} -
- -
- - {hasAIAccess && ( - - )} -
-
-
- ); -}; diff --git a/components/AuthButton.tsx b/components/AuthButton.tsx index 39e1473..6af93a8 100644 --- a/components/AuthButton.tsx +++ b/components/AuthButton.tsx @@ -1,31 +1,9 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React from 'react'; import { useAuth } from 'react-oidc-context'; -import { LogIn, LogOut, User, Loader2, Key, ChevronDown } from 'lucide-react'; +import { LogIn, LogOut, User, Loader2 } from 'lucide-react'; -interface AuthButtonProps { - onAccountSettingsClick?: () => void; -} - -export const AuthButton: React.FC = ({ onAccountSettingsClick }) => { +export const AuthButton: React.FC = () => { const auth = useAuth(); - const [dropdownOpen, setDropdownOpen] = useState(false); - const dropdownRef = useRef(null); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setDropdownOpen(false); - } - }; - - if (dropdownOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [dropdownOpen]); if (auth.isLoading) { return ( @@ -46,44 +24,20 @@ export const AuthButton: React.FC = ({ onAccountSettingsClick } if (auth.isAuthenticated) { return ( -
- - - {dropdownOpen && ( -
- {onAccountSettingsClick && ( - - )} - -
- )}
); } diff --git a/components/DisconnectedScreen.tsx b/components/DisconnectedScreen.tsx index 5077fd5..0596d5a 100644 --- a/components/DisconnectedScreen.tsx +++ b/components/DisconnectedScreen.tsx @@ -18,7 +18,7 @@ export const DisconnectedScreen: React.FC = ({ onGoHome, }) => { return ( -
+
= ({ onChange={(v) => update({ hostParticipates: v })} /> - } - iconActive={config.randomNamesEnabled} - label="Random Names" - description="Assign fun two-word names to players" - checked={config.randomNamesEnabled} - onChange={(v) => update({ randomNamesEnabled: v })} - tooltip="Players will be assigned random names like 'Brave Falcon' or 'Swift Tiger' instead of choosing their own nicknames." - /> - } iconActive={config.streakBonusEnabled} diff --git a/components/GameScreen.tsx b/components/GameScreen.tsx index d68279d..060de98 100644 --- a/components/GameScreen.tsx +++ b/components/GameScreen.tsx @@ -40,34 +40,35 @@ export const GameScreen: React.FC = ({ return (
{/* Header */} -
-
+
+
{currentQuestionIndex + 1} / {totalQuestions}
+ {/* Whimsical Timer */}
-
-
- {timeLeftSeconds} -
+
+
+ {timeLeftSeconds} +
-
+
{isClient ? 'Player' : isSpectator ? 'Spectator' : 'Host'}
{/* Question Area */} -
+
{question && ( -

+

{question.text}

@@ -75,7 +76,7 @@ export const GameScreen: React.FC = ({
{/* Answer Grid */} -
+
{displayOptions.map((option, idx) => { const ShapeIcon = SHAPES[option.shape]; const colorClass = COLORS[option.color]; diff --git a/components/HostReconnected.tsx b/components/HostReconnected.tsx index 4783fef..3069ac0 100644 --- a/components/HostReconnected.tsx +++ b/components/HostReconnected.tsx @@ -20,7 +20,7 @@ export const HostReconnected: React.FC = ({ onEndGame, }) => { return ( -
+
= ({ onGenerate, onCreateManual, onLoadQuiz, onJoin, isLoading, error }) => { const auth = useAuth(); - const [mode, setMode] = useState<'HOST' | 'JOIN'>('JOIN'); + const [mode, setMode] = useState<'HOST' | 'JOIN'>('HOST'); const [generateMode, setGenerateMode] = useState('topic'); const [topic, setTopic] = useState(''); const [pin, setPin] = useState(''); @@ -38,18 +35,12 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on const [useOcr, setUseOcr] = useState(false); const [defaultConfigOpen, setDefaultConfigOpen] = useState(false); const [editingDefaultConfig, setEditingDefaultConfig] = useState(null); - const [preferencesOpen, setPreferencesOpen] = useState(false); - const [accountSettingsOpen, setAccountSettingsOpen] = useState(false); - const [gameInfo, setGameInfo] = useState<{ randomNamesEnabled: boolean; quizTitle: string } | null>(null); - const [checkingPin, setCheckingPin] = useState(false); const hasImageFile = selectedFiles.some(f => f.type.startsWith('image/')); const hasDocumentFile = selectedFiles.some(f => !f.type.startsWith('image/') && !['application/pdf', 'text/plain', 'text/markdown', 'text/csv', 'text/html'].includes(f.type)); const showOcrOption = hasImageFile || hasDocumentFile; const { defaultConfig, saving: savingConfig, saveDefaultConfig } = useUserConfig(); - const { preferences, hasAIAccess, saving: savingPrefs, savePreferences, applyColorScheme } = useUserPreferences(); - const canUseAI = auth.isAuthenticated && (hasAIAccess || preferences.geminiApiKey); const { quizzes, @@ -69,30 +60,6 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on } }, [libraryOpen, auth.isAuthenticated, fetchQuizzes]); - useEffect(() => { - const checkGamePin = async () => { - if (pin.trim().length === 6) { - setCheckingPin(true); - try { - const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3001'; - const response = await fetch(`${backendUrl}/api/games/${pin.trim()}`); - if (response.ok) { - const data = await response.json(); - setGameInfo({ randomNamesEnabled: data.randomNamesEnabled, quizTitle: data.quizTitle }); - } else { - setGameInfo(null); - } - } catch { - setGameInfo(null); - } - setCheckingPin(false); - } else { - setGameInfo(null); - } - }; - checkGamePin(); - }, [pin]); - const handleFileSelect = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { const newFiles = Array.from(e.target.files); @@ -137,8 +104,7 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on setUseOcr(false); }; - const canGenerateInput = generateMode === 'topic' ? topic.trim() : selectedFiles.length > 0; - const canGenerate = canUseAI && canGenerateInput; + const canGenerate = generateMode === 'topic' ? topic.trim() : selectedFiles.length > 0; const handleHostSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -154,9 +120,7 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on const handleJoinSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (pin.trim() && (gameInfo?.randomNamesEnabled || name.trim())) { - onJoin(pin, name.trim() || 'Player'); - } + if (pin.trim() && name.trim()) onJoin(pin, name); }; const handleLoadQuiz = async (id: string) => { @@ -173,46 +137,37 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on }; return ( -
+
{auth.isAuthenticated && ( - <> - - - + )} - setAccountSettingsOpen(true)} /> +
-
-
- +
+
+
-

Kaboot

-

The AI Quiz Party

+

Kaboot

+

The AI Quiz Party

-
+
+
- ) : ( - <> - {canUseAI && ( - <> - -
- - + + + {generateMode === 'topic' ? ( + + setTopic(e.target.value)} + className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none transition-all placeholder:font-medium text-center" + disabled={isLoading} + /> + + ) : ( + +
document.getElementById('file-upload')?.click()} + className={`border-2 border-dashed rounded-2xl p-4 text-center cursor-pointer transition-all ${ + isDragging + ? 'border-theme-primary bg-theme-primary/5 scale-[1.02]' + : selectedFiles.length > 0 + ? 'border-theme-primary/50 bg-theme-primary/5' + : 'border-gray-300 hover:border-gray-400 hover:bg-gray-50' + }`} + > + + +
+ +

+ {isDragging ? 'Drop files here' : 'Drop files or click to browse'} +

+

PDF, DOCX, PPTX, XLSX, TXT, Images

- - - {generateMode === 'topic' ? ( - - setTopic(e.target.value)} - className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none transition-all placeholder:font-medium text-center" - disabled={isLoading} - /> - - ) : ( - -
document.getElementById('file-upload')?.click()} - className={`border-2 border-dashed rounded-2xl p-4 text-center cursor-pointer transition-all ${ - isDragging - ? 'border-theme-primary bg-theme-primary/5 scale-[1.02]' - : selectedFiles.length > 0 - ? 'border-theme-primary/50 bg-theme-primary/5' - : 'border-gray-300 hover:border-gray-400 hover:bg-gray-50' - }`} - > - - -
- -

- {isDragging ? 'Drop files here' : 'Drop files or click to browse'} -

-

PDF, DOCX, PPTX, XLSX, TXT, Images

-
-
- - {selectedFiles.length > 0 && ( -
-
- {selectedFiles.length} file{selectedFiles.length > 1 ? 's' : ''} selected - -
-
- {selectedFiles.map((file, index) => ( -
-
-
- {file.type.startsWith('image/') ? ( - - ) : ( - - )} -
- {file.name} -
- -
- ))} -
-
- )} - - {showOcrOption && ( - - )} -
- )} -
- -
-
- Questions - {questionCount} -
- setQuestionCount(Number(e.target.value))} - className="w-full accent-theme-primary h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" - /> -
- - - - -
-
- OR -
+ + {selectedFiles.length > 0 && ( +
+
+ {selectedFiles.length} file{selectedFiles.length > 1 ? 's' : ''} selected + +
+
+ {selectedFiles.map((file, index) => ( +
+
+
+ {file.type.startsWith('image/') ? ( + + ) : ( + + )} +
+ {file.name} +
+ +
+ ))} +
+
+ )} + + {showOcrOption && ( + + )} + + )} + + +
+
+ Questions + {questionCount} +
+ setQuestionCount(Number(e.target.value))} + className="w-full accent-theme-primary h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" + /> +
+ + + + +
+
+ OR +
+
- + - - + {auth.isAuthenticated && ( + )}
) : ( @@ -460,27 +398,19 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on onChange={(e) => setPin(e.target.value)} className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none text-center" /> - {gameInfo?.randomNamesEnabled ? ( -
-

- You'll get a random name! -

-
- ) : ( - setName(e.target.value)} - className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none text-center" - /> - )} + setName(e.target.value)} + className="w-full p-4 text-xl font-bold border-2 border-gray-200 rounded-2xl focus:border-theme-primary focus:ring-4 focus:ring-theme-primary/20 outline-none text-center" + /> )} @@ -522,26 +452,6 @@ export const Landing: React.FC = ({ onGenerate, onCreateManual, on }} saving={savingConfig} /> - - setPreferencesOpen(false)} - preferences={preferences} - onSave={savePreferences} - onPreview={applyColorScheme} - saving={savingPrefs} - /> - - setAccountSettingsOpen(false)} - apiKey={preferences.geminiApiKey} - onSave={async (key) => { - await savePreferences({ ...preferences, geminiApiKey: key }); - }} - saving={savingPrefs} - hasAIAccess={hasAIAccess} - />
); }; diff --git a/components/Lobby.tsx b/components/Lobby.tsx index e3c518f..972f783 100644 --- a/components/Lobby.tsx +++ b/components/Lobby.tsx @@ -11,25 +11,23 @@ interface LobbyProps { role: 'HOST' | 'CLIENT'; onStart: () => void; onEndGame?: () => void; - currentPlayerId?: string | null; } -export const Lobby: React.FC = ({ quizTitle, players, gamePin, role, onStart, onEndGame, currentPlayerId }) => { +export const Lobby: React.FC = ({ quizTitle, players, gamePin, role, onStart, onEndGame }) => { const isHost = role === 'HOST'; const realPlayers = players.filter(p => p.id !== 'host'); - const currentPlayer = currentPlayerId ? players.find(p => p.id === currentPlayerId) : null; return ( -
-
+
+
- Game PIN -
+ Game PIN +
{gamePin}
-
+
{quizTitle}
@@ -37,23 +35,23 @@ export const Lobby: React.FC = ({ quizTitle, players, gamePin, role,
-
+
{realPlayers.length} - Players + Players
-
+
{isHost ? ( <> -
+
{realPlayers.length === 0 && ( -
-
- +
+
+
-
Waiting for players to join...
+
Waiting for players to join...
)} {realPlayers.map((player) => ( @@ -62,7 +60,7 @@ export const Lobby: React.FC = ({ quizTitle, players, gamePin, role, initial={{ scale: 0, rotate: -10 }} animate={{ scale: 1, rotate: 0 }} exit={{ scale: 0, opacity: 0 }} - className="bg-white text-black px-4 md:px-6 py-2 md:py-3 rounded-full font-black text-base md:text-xl shadow-[0_4px_0_rgba(0,0,0,0.2)] flex items-center gap-2 md:gap-3 border-b-4 border-gray-200" + className="bg-white text-black px-6 py-3 rounded-full font-black text-xl shadow-[0_4px_0_rgba(0,0,0,0.2)] flex items-center gap-3 border-b-4 border-gray-200" > {player.name} @@ -74,45 +72,38 @@ export const Lobby: React.FC = ({ quizTitle, players, gamePin, role, {onEndGame && ( )} ) : ( -
+
- {currentPlayer ? ( - - ) : ( - - )} + -

- {currentPlayer?.name || "You're in!"} -

-

Waiting for the host to start...

+

You're in!

+

See your name on the big screen?

)}
diff --git a/components/Podium.tsx b/components/Podium.tsx index 0030697..31526be 100644 --- a/components/Podium.tsx +++ b/components/Podium.tsx @@ -34,10 +34,10 @@ export const Podium: React.FC = ({ players, onRestart }) => { }, []); return ( -
-

Podium

+
+

Podium

-
+
{/* Second Place */} {second && ( void; - preferences: UserPreferences; - onSave: (prefs: UserPreferences) => Promise; - onPreview: (schemeId: string) => void; - saving: boolean; -} - -export const PreferencesModal: React.FC = ({ - isOpen, - onClose, - preferences, - onSave, - onPreview, - saving, -}) => { - useBodyScrollLock(isOpen); - const [localPrefs, setLocalPrefs] = useState(preferences); - - if (!isOpen) return null; - - const handleColorSelect = (schemeId: string) => { - setLocalPrefs(prev => ({ ...prev, colorScheme: schemeId })); - onPreview(schemeId); - }; - - const handleSave = async () => { - await onSave(localPrefs); - onClose(); - }; - - const handleClose = () => { - onPreview(preferences.colorScheme); - onClose(); - }; - - return ( - - e.stopPropagation()} - > -
-
-
- -
-
-

Color Scheme

-

Customize your theme

-
-
- -
- -
-
- {COLOR_SCHEMES.map((scheme) => ( - - ))} -
-

- {COLOR_SCHEMES.find(s => s.id === localPrefs.colorScheme)?.name || 'Select a color'} -

-
- -
- - -
-
-
- ); -}; diff --git a/components/QuizCreator.tsx b/components/QuizCreator.tsx index acaebee..7260cd1 100644 --- a/components/QuizCreator.tsx +++ b/components/QuizCreator.tsx @@ -60,8 +60,8 @@ export const QuizCreator: React.FC = ({ onFinalize, onCancel } }; return ( -
-
+
+

Create Quiz

@@ -79,7 +79,7 @@ export const QuizCreator: React.FC = ({ onFinalize, onCancel }
-
+
= ({ }; return ( -
-
+
+
-
+

Questions