Scoreboard ui stuff

This commit is contained in:
Joey Yakimowich-Payne 2026-01-15 08:21:38 -07:00
commit 279dc7f2c3
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
12 changed files with 1558 additions and 77 deletions

View file

@ -126,18 +126,38 @@ export const GameScreen: React.FC<GameScreenProps> = ({
<AnimatePresence>
{isClient && hasAnswered && (
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
className="absolute inset-0 bg-theme-primary/95 flex flex-col items-center justify-center z-50 p-8 text-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 bg-black/20 backdrop-blur-[2px] flex flex-col items-center justify-center z-50 p-4"
>
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ repeat: Infinity, duration: 1.5 }}
className="text-6xl mb-6"
initial={{ scale: 0.9, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-theme-primary/40 backdrop-blur-xl border border-white/20 p-8 md:p-12 rounded-[2.5rem] shadow-[0_20px_50px_rgba(0,0,0,0.5)] text-center max-w-md w-full relative overflow-hidden"
>
🚀
<div className="absolute -inset-full bg-gradient-to-tr from-transparent via-white/10 to-transparent rotate-45 animate-pulse pointer-events-none" />
<div className="absolute top-0 left-0 right-0 h-32 bg-gradient-to-b from-white/10 to-transparent pointer-events-none" />
<motion.div
animate={{
y: [0, -15, 0],
rotate: [0, 5, -5, 0],
scale: [1, 1.1, 1]
}}
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
className="text-7xl mb-6 drop-shadow-2xl relative z-10 inline-block filter"
>
🚀
</motion.div>
<h2 className="text-4xl font-black text-white font-display mb-3 drop-shadow-lg relative z-10 tracking-tight">
Answer Sent!
</h2>
<p className="text-lg font-bold text-white/80 relative z-10">
Cross your fingers...
</p>
</motion.div>
<h2 className="text-4xl md:text-5xl font-black text-white font-display mb-4">Answer Sent!</h2>
<p className="text-xl font-bold opacity-80">Cross your fingers...</p>
</motion.div>
)}
</AnimatePresence>

View file

@ -1,4 +1,5 @@
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';
@ -21,25 +22,87 @@ interface LandingProps {
onJoin: (pin: string, name: string) => void;
isLoading: boolean;
error: string | null;
initialPin?: string | null;
}
export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, onLoadQuiz, onJoin, isLoading, error }) => {
export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, onLoadQuiz, onJoin, isLoading, error, initialPin }) => {
const auth = useAuth();
const [mode, setMode] = useState<'HOST' | 'JOIN'>('JOIN');
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(searchParams);
if (newMode === 'HOST') {
newParams.set('mode', 'host');
} else {
newParams.delete('mode');
}
setSearchParams(newParams, { replace: true });
};
const [generateMode, setGenerateMode] = useState<GenerateMode>('topic');
const [topic, setTopic] = useState('');
const [pin, setPin] = useState('');
const [pin, setPin] = useState(initialPin || '');
const [name, setName] = useState('');
const [libraryOpen, setLibraryOpen] = useState(false);
const modalParam = searchParams.get('modal');
const libraryOpen = modalParam === 'library';
const preferencesOpen = modalParam === 'preferences';
const defaultConfigOpen = modalParam === 'settings';
const accountSettingsOpen = modalParam === 'account';
const setLibraryOpen = (open: boolean) => {
const newParams = new URLSearchParams(searchParams);
if (open) {
newParams.set('modal', 'library');
} else {
newParams.delete('modal');
}
setSearchParams(newParams);
};
const setPreferencesOpen = (open: boolean) => {
const newParams = new URLSearchParams(searchParams);
if (open) {
newParams.set('modal', 'preferences');
} else {
newParams.delete('modal');
}
setSearchParams(newParams);
};
const setDefaultConfigOpen = (open: boolean) => {
const newParams = new URLSearchParams(searchParams);
if (open) {
newParams.set('modal', 'settings');
} else {
newParams.delete('modal');
}
setSearchParams(newParams);
};
const setAccountSettingsOpen = (open: boolean) => {
const newParams = new URLSearchParams(searchParams);
if (open) {
newParams.set('modal', 'account');
} else {
newParams.delete('modal');
}
setSearchParams(newParams);
};
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [questionCount, setQuestionCount] = useState(10);
const [isDragging, setIsDragging] = useState(false);
const [useOcr, setUseOcr] = useState(false);
const [defaultConfigOpen, setDefaultConfigOpen] = useState(false);
const [editingDefaultConfig, setEditingDefaultConfig] = useState<GameConfig | null>(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);
@ -69,6 +132,14 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
}
}, [libraryOpen, auth.isAuthenticated, fetchQuizzes]);
useEffect(() => {
if (defaultConfigOpen) {
setEditingDefaultConfig(defaultConfig);
} else {
setEditingDefaultConfig(null);
}
}, [defaultConfigOpen, defaultConfig]);
useEffect(() => {
const checkGamePin = async () => {
if (pin.trim().length === 6) {
@ -404,7 +475,7 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
<input
type="range"
min="5"
max="30"
max="50"
value={questionCount}
onChange={(e) => setQuestionCount(Number(e.target.value))}
className="w-full accent-theme-primary h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { X, Palette, Loader2, Check } from 'lucide-react';
import { useBodyScrollLock } from '../hooks/useBodyScrollLock';
@ -25,6 +25,12 @@ export const PreferencesModal: React.FC<PreferencesModalProps> = ({
useBodyScrollLock(isOpen);
const [localPrefs, setLocalPrefs] = useState<UserPreferences>(preferences);
useEffect(() => {
if (isOpen) {
setLocalPrefs(preferences);
}
}, [isOpen, preferences]);
if (!isOpen) return null;
const handleColorSelect = (schemeId: string) => {

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useAuth } from 'react-oidc-context';
import { Quiz, Question, AnswerOption } from '../types';
import { v4 as uuidv4 } from 'uuid';
@ -18,7 +18,14 @@ export const QuizCreator: React.FC<QuizCreatorProps> = ({ onFinalize, onCancel }
const [options, setOptions] = useState<string[]>(['', '', '', '']);
const [reasons, setReasons] = useState<string[]>(['', '', '', '']);
const [correctIdx, setCorrectIdx] = useState<number>(0);
const [saveToLibrary, setSaveToLibrary] = useState(false);
const [saveToLibrary, setSaveToLibrary] = useState(auth.isAuthenticated);
const [hasToggledSave, setHasToggledSave] = useState(false);
useEffect(() => {
if (auth.isAuthenticated && !hasToggledSave) {
setSaveToLibrary(true);
}
}, [auth.isAuthenticated, hasToggledSave]);
const handleAddQuestion = () => {
if (!qText.trim() || options.some(o => !o.trim())) {
@ -182,12 +189,15 @@ export const QuizCreator: React.FC<QuizCreatorProps> = ({ onFinalize, onCancel }
<div className="p-6 bg-gray-50 border-t-2 border-gray-100 flex justify-between items-center">
{auth.isAuthenticated ? (
<label className="flex items-center gap-3 cursor-pointer select-none group">
<input
type="checkbox"
checked={saveToLibrary}
onChange={(e) => setSaveToLibrary(e.target.checked)}
className="sr-only peer"
/>
<input
type="checkbox"
checked={saveToLibrary}
onChange={(e) => {
setSaveToLibrary(e.target.checked);
setHasToggledSave(true);
}}
className="sr-only peer"
/>
<div className="w-6 h-6 border-2 border-gray-300 rounded-lg flex items-center justify-center peer-checked:bg-theme-primary peer-checked:border-theme-primary transition-all group-hover:border-gray-400">
{saveToLibrary && <CheckCircle size={16} className="text-white" />}
</div>

View file

@ -111,7 +111,7 @@ const PlayerRow: React.FC<PlayerRowProps> = ({ player, index, maxScore }) => {
style={{ backgroundColor: player.color }}
initial={{ width: 0 }}
animate={{ width: `${Math.max(barWidth, 2)}%` }}
transition={{ duration: 0.6, delay: baseDelay + 0.1 }}
transition={{ duration: 0.6, delay: phase === 0 ? baseDelay + 0.1 : 0 }}
/>
</div>
@ -191,7 +191,7 @@ export const Scoreboard: React.FC<ScoreboardProps> = ({ players, onNext, isHost,
displayName: p.id === currentPlayerId ? `${p.name} (You)` : p.name
}));
const sortedPlayers = [...playersWithDisplayName].sort((a, b) => b.score - a.score);
const maxScore = Math.max(...sortedPlayers.map(p => p.score), 1);
const maxScore = Math.max(...sortedPlayers.map(p => Math.max(p.score, p.previousScore)), 1);
return (
<div className="flex flex-col h-screen p-4 md:p-8 overflow-hidden">