import React, { useState, useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { BrainCircuit, Loader2, Play, PenTool, BookOpen, Upload, X, FileText, Image, ScanText, Sparkles, Settings, Palette, Lock } from 'lucide-react'; import { useAuth } from 'react-oidc-context'; import { AuthButton } from './AuthButton'; import { QuizLibrary } from './QuizLibrary'; import { ImportQuizzesModal } from './ImportQuizzesModal'; import { DefaultConfigModal } from './DefaultConfigModal'; import { PreferencesModal } from './PreferencesModal'; import { ApiKeyModal } from './ApiKeyModal'; import { useQuizLibrary } from '../hooks/useQuizLibrary'; import { useUserConfig } from '../hooks/useUserConfig'; import { useUserPreferences } from '../hooks/useUserPreferences'; import type { Quiz, GameConfig } from '../types'; type GenerateMode = 'topic' | 'document'; import type { AIProvider } from '../types'; interface LandingProps { onGenerate: (options: { topic?: string; questionCount?: number; files?: File[]; useOcr?: boolean; aiProvider?: AIProvider; apiKey?: string; geminiModel?: string; openRouterModel?: string; openAIModel?: string; }) => void; onCreateManual: () => void; onLoadQuiz: (quiz: Quiz, quizId?: string) => void; onJoin: (pin: string, name: string) => void; isLoading: boolean; error: string | null; initialPin?: string | null; } export const Landing: React.FC = ({ onGenerate, onCreateManual, onLoadQuiz, onJoin, isLoading, error, initialPin }) => { const auth = useAuth(); const [searchParams, setSearchParams] = useSearchParams(); const getModeFromUrl = (): 'HOST' | 'JOIN' => { if (initialPin) return 'JOIN'; const modeParam = searchParams.get('mode'); return modeParam === 'host' ? 'HOST' : 'JOIN'; }; const [mode, setModeState] = useState<'HOST' | 'JOIN'>(getModeFromUrl); const setMode = (newMode: 'HOST' | 'JOIN') => { setModeState(newMode); const newParams = new URLSearchParams(); const modalParam = searchParams.get('modal'); if (modalParam) { newParams.set('modal', modalParam); } if (newMode === 'HOST') { newParams.set('mode', 'host'); } setSearchParams(newParams, { replace: true }); }; const [generateMode, setGenerateMode] = useState('topic'); const [topic, setTopic] = useState(''); const [pin, setPin] = useState(initialPin || ''); const [name, setName] = useState(''); const modalParam = searchParams.get('modal'); const libraryOpen = modalParam === 'library'; const importOpen = modalParam === 'import'; const preferencesOpen = modalParam === 'preferences'; const defaultConfigOpen = modalParam === 'settings'; const accountSettingsOpen = modalParam === 'account'; const buildCleanParams = (updates: Record) => { const newParams = new URLSearchParams(); const modeParam = searchParams.get('mode'); if (modeParam) newParams.set('mode', modeParam); for (const [key, value] of Object.entries(updates)) { if (value !== null) { newParams.set(key, value); } } return newParams; }; const setLibraryOpen = (open: boolean) => { setSearchParams(buildCleanParams({ modal: open ? 'library' : null })); }; const setImportOpen = (open: boolean) => { setSearchParams(buildCleanParams({ modal: open ? 'import' : null })); }; const setPreferencesOpen = (open: boolean) => { setSearchParams(buildCleanParams({ modal: open ? 'preferences' : null })); }; const setDefaultConfigOpen = (open: boolean) => { setSearchParams(buildCleanParams({ modal: open ? 'settings' : null })); }; const setAccountSettingsOpen = (open: boolean) => { setSearchParams(buildCleanParams({ modal: open ? 'account' : null })); }; const [selectedFiles, setSelectedFiles] = useState([]); const [questionCount, setQuestionCount] = useState(10); const [isDragging, setIsDragging] = useState(false); const [useOcr, setUseOcr] = useState(false); const [editingDefaultConfig, setEditingDefaultConfig] = useState(null); 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 hasValidApiKey = (() => { if (preferences.aiProvider === 'openrouter') return !!preferences.openRouterApiKey; if (preferences.aiProvider === 'openai') return !!preferences.openAIApiKey; return hasAIAccess || !!preferences.geminiApiKey; })(); const canUseAI = auth.isAuthenticated && hasValidApiKey; const { quizzes, loading: libraryLoading, loadingQuizId, deletingQuizId, sharingQuizId, exporting, importing, error: libraryError, fetchQuizzes, loadQuiz, deleteQuiz, shareQuiz, unshareQuiz, exportQuizzes, importQuizzes, parseImportFile, retry: retryLibrary } = useQuizLibrary(); useEffect(() => { if (libraryOpen && auth.isAuthenticated) { fetchQuizzes(); } }, [libraryOpen, auth.isAuthenticated, fetchQuizzes]); useEffect(() => { if (defaultConfigOpen) { setEditingDefaultConfig(defaultConfig); } else { setEditingDefaultConfig(null); } }, [defaultConfigOpen, defaultConfig]); 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); setSelectedFiles(prev => [...prev, ...newFiles]); } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { const acceptedTypes = ['.pdf', '.txt', '.md', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods', '.rtf', '.jpg', '.jpeg', '.png', '.gif', '.webp']; const newFiles = Array.from(e.dataTransfer.files).filter((file: File) => { const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); return acceptedTypes.includes(fileExtension) || file.type.startsWith('image/'); }); if (newFiles.length > 0) { setSelectedFiles(prev => [...prev, ...newFiles]); } } }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); }; const removeFile = (index: number) => { setSelectedFiles(prev => prev.filter((_, i) => i !== index)); if (selectedFiles.length <= 1) { setUseOcr(false); } }; const clearAllFiles = () => { setSelectedFiles([]); setUseOcr(false); }; const canGenerateInput = generateMode === 'topic' ? topic.trim() : selectedFiles.length > 0; const canGenerate = canUseAI && canGenerateInput; const handleHostSubmit = (e: React.FormEvent) => { e.preventDefault(); if (canGenerate && !isLoading) { const aiProvider = preferences.aiProvider || 'gemini'; let apiKey: string | undefined; if (aiProvider === 'openrouter') { apiKey = preferences.openRouterApiKey; } else if (aiProvider === 'openai') { apiKey = preferences.openAIApiKey; } else { apiKey = preferences.geminiApiKey; } onGenerate({ topic: generateMode === 'topic' ? topic.trim() : undefined, questionCount, files: generateMode === 'document' ? selectedFiles : undefined, useOcr: generateMode === 'document' && showOcrOption ? useOcr : undefined, aiProvider, apiKey, geminiModel: aiProvider === 'gemini' ? preferences.geminiModel : undefined, openRouterModel: aiProvider === 'openrouter' ? preferences.openRouterModel : undefined, openAIModel: aiProvider === 'openai' ? preferences.openAIModel : undefined, }); } }; const handleJoinSubmit = (e: React.FormEvent) => { e.preventDefault(); if (pin.trim() && (gameInfo?.randomNamesEnabled || name.trim())) { onJoin(pin, name.trim() || 'Player'); } }; const handleLoadQuiz = async (id: string) => { try { const quiz = await loadQuiz(id); setLibraryOpen(false); onLoadQuiz(quiz, id); } catch (err) { if (err instanceof Error && err.message.includes('redirecting')) { return; } console.error('Failed to load quiz:', err); } }; return (
{auth.isAuthenticated && ( <> )} setAccountSettingsOpen(true)} />

Kaboot

The AI Quiz Party

{mode === 'HOST' ? (
{!auth.isAuthenticated ? (

Sign in to host quizzes

) : ( <> {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

{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
)} {!canUseAI && ( )} )}
) : (
setPin(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6))} 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 uppercase tracking-widest" /> {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" /> )}
)} {error && (

{error}

)}
setLibraryOpen(false)} quizzes={quizzes} loading={libraryLoading} loadingQuizId={loadingQuizId} deletingQuizId={deletingQuizId} sharingQuizId={sharingQuizId} exporting={exporting} error={libraryError} onLoadQuiz={handleLoadQuiz} onDeleteQuiz={deleteQuiz} onShareQuiz={shareQuiz} onUnshareQuiz={unshareQuiz} onExportQuizzes={exportQuizzes} onImportClick={() => setImportOpen(true)} onRetry={retryLibrary} /> setImportOpen(false)} onImport={importQuizzes} parseFile={parseImportFile} importing={importing} /> { setDefaultConfigOpen(false); setEditingDefaultConfig(null); }} config={editingDefaultConfig || defaultConfig} onChange={setEditingDefaultConfig} onSave={async () => { if (editingDefaultConfig) { await saveDefaultConfig(editingDefaultConfig); setDefaultConfigOpen(false); setEditingDefaultConfig(null); } }} saving={savingConfig} /> setPreferencesOpen(false)} preferences={preferences} onSave={savePreferences} onPreview={applyColorScheme} saving={savingPrefs} /> setAccountSettingsOpen(false)} preferences={preferences} onSave={async (prefs) => { await savePreferences({ ...preferences, ...prefs }); }} saving={savingPrefs} hasAIAccess={hasAIAccess} />
); };