Scoreboard ui stuff
This commit is contained in:
parent
f0d177feeb
commit
279dc7f2c3
12 changed files with 1558 additions and 77 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue