+
{
const hostSecretRef = useRef(null);
const gameStateRef = useRef("LANDING");
const firstCorrectPlayerIdRef = useRef(null);
- const currentCorrectShapeRef = useRef(null);
useEffect(() => { timeLeftRef.current = timeLeft; }, [timeLeft]);
useEffect(() => { playersRef.current = players; }, [players]);
@@ -85,19 +83,9 @@ export const useGame = () => {
useEffect(() => { hostSecretRef.current = hostSecret; }, [hostSecret]);
useEffect(() => { gameStateRef.current = gameState; }, [gameState]);
useEffect(() => { firstCorrectPlayerIdRef.current = firstCorrectPlayerId; }, [firstCorrectPlayerId]);
- useEffect(() => { currentCorrectShapeRef.current = currentCorrectShape; }, [currentCorrectShape]);
const generateGamePin = () => Math.floor(Math.random() * 900000) + 100000 + "";
- const generateRandomName = (): string => {
- return uniqueNamesGenerator({
- dictionaries: [adjectives, animals],
- separator: ' ',
- style: 'capital',
- length: 2,
- });
- };
-
const syncGameState = useCallback(async () => {
if (!gamePinRef.current || !hostSecretRef.current) return;
if (gameStateRef.current === 'LANDING' || gameStateRef.current === 'EDITING' || gameStateRef.current === 'CREATING' || gameStateRef.current === 'GENERATING') return;
@@ -522,23 +510,13 @@ export const useGame = () => {
allPlayers: playersRef.current.map(p => ({ id: p.id, name: p.name, lastAnswerCorrect: p.lastAnswerCorrect }))
});
- let assignedName = payload.name;
- if (!reconnectedPlayer && gameConfigRef.current.randomNamesEnabled) {
- assignedName = generateRandomName();
- }
-
- let updatedPlayers = playersRef.current;
- let newPlayer: Player | null = null;
-
if (reconnectedPlayer) {
- updatedPlayers = playersRef.current.map(p => p.id === reconnectedPlayer.id ? { ...p, id: conn.peer } : p);
- setPlayers(updatedPlayers);
- assignedName = reconnectedPlayer.name;
+ setPlayers(prev => prev.map(p => p.id === reconnectedPlayer.id ? { ...p, id: conn.peer } : p));
} else if (!playersRef.current.find(p => p.id === conn.peer)) {
const colorIndex = playersRef.current.length % PLAYER_COLORS.length;
- newPlayer = {
+ const newPlayer: Player = {
id: conn.peer,
- name: assignedName,
+ name: payload.name,
score: 0,
previousScore: 0,
streak: 0,
@@ -549,8 +527,7 @@ export const useGame = () => {
avatarSeed: Math.random(),
color: PLAYER_COLORS[colorIndex]
};
- updatedPlayers = [...playersRef.current, newPlayer];
- setPlayers(updatedPlayers);
+ setPlayers(prev => [...prev, newPlayer]);
}
const currentState = gameStateRef.current;
@@ -561,7 +538,7 @@ export const useGame = () => {
const welcomePayload: any = {
playerId: conn.peer,
quizTitle: currentQuiz?.title || 'Kaboot',
- players: updatedPlayers,
+ players: playersRef.current,
gameState: currentState,
currentQuestionIndex: currentIndex,
totalQuestions: currentQuiz?.questions.length || 0,
@@ -571,7 +548,6 @@ export const useGame = () => {
hasAnswered: false,
lastAnswerCorrect: null,
selectedShape: null,
- assignedName,
};
if (currentQuestion) {
@@ -750,48 +726,6 @@ export const useGame = () => {
const endQuestion = () => {
if (timerRef.current) clearInterval(timerRef.current);
-
- const unansweredPlayers = playersRef.current.filter(p => p.lastAnswerCorrect === null);
- if (unansweredPlayers.length > 0) {
- const updatedPlayers = playersRef.current.map(p => {
- if (p.lastAnswerCorrect !== null) return p;
-
- const playerRank = getPlayerRank(p.id, playersRef.current);
- const breakdown = calculatePointsWithBreakdown({
- isCorrect: false,
- timeLeftMs: 0,
- questionTimeMs: QUESTION_TIME_MS,
- streak: 0,
- playerRank,
- isFirstCorrect: false,
- config: gameConfigRef.current,
- });
- const newScore = Math.max(0, p.score + breakdown.total);
-
- if (p.id === 'host') {
- setLastPointsEarned(breakdown.total);
- setLastAnswerCorrect(false);
- setCurrentPlayerScore(newScore);
- setCurrentStreak(0);
- } else {
- const conn = connectionsRef.current.get(p.id);
- if (conn?.open) {
- conn.send({ type: 'RESULT', payload: { isCorrect: false, scoreAdded: breakdown.total, newScore, breakdown } });
- }
- }
-
- return {
- ...p,
- score: newScore,
- previousScore: p.score,
- streak: 0,
- lastAnswerCorrect: false,
- pointsBreakdown: breakdown,
- };
- });
- setPlayers(updatedPlayers);
- }
-
setGameState('REVEAL');
broadcast({ type: 'TIME_UP', payload: {} });
};
@@ -981,13 +915,6 @@ export const useGame = () => {
if (payload.players) {
setPlayers(payload.players);
}
- if (payload.assignedName) {
- setCurrentPlayerName(payload.assignedName);
- const session = getStoredSession();
- if (session) {
- storeSession({ ...session, playerName: payload.assignedName });
- }
- }
if (payload.selectedShape && payload.options) {
const matchedOption = payload.options.find(opt => opt.shape === payload.selectedShape);
if (matchedOption) {
@@ -1160,9 +1087,8 @@ export const useGame = () => {
} else {
const option = arg as AnswerOption;
setSelectedOption(option);
- // Use ref to avoid stale closure - currentCorrectShape state may not be updated yet
- const isCorrect = option.shape === currentCorrectShapeRef.current;
- console.log('[CLIENT] Answering:', { selectedShape: option.shape, currentCorrectShape: currentCorrectShapeRef.current, isCorrect });
+ const isCorrect = option.shape === currentCorrectShape;
+ console.log('[CLIENT] Answering:', { selectedShape: option.shape, currentCorrectShape, isCorrect });
hostConnectionRef.current?.send({ type: 'ANSWER', payload: { playerId: peerRef.current?.id, isCorrect, selectedShape: option.shape } });
}
};
diff --git a/hooks/useUserPreferences.ts b/hooks/useUserPreferences.ts
deleted file mode 100644
index af188b6..0000000
--- a/hooks/useUserPreferences.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { useState, useCallback, useEffect } from 'react';
-import toast from 'react-hot-toast';
-import { useAuthenticatedFetch } from './useAuthenticatedFetch';
-import type { UserPreferences } from '../types';
-import { COLOR_SCHEMES } from '../types';
-
-const DEFAULT_PREFERENCES: UserPreferences = {
- colorScheme: 'blue',
-};
-
-export const applyColorScheme = (schemeId: string) => {
- const scheme = COLOR_SCHEMES.find(s => s.id === schemeId) || COLOR_SCHEMES[0];
- document.documentElement.style.setProperty('--theme-primary', scheme.primary);
- document.documentElement.style.setProperty('--theme-primary-dark', scheme.primaryDark);
- document.documentElement.style.setProperty('--theme-primary-darker', scheme.primaryDarker);
-};
-
-interface UseUserPreferencesReturn {
- preferences: UserPreferences;
- hasAIAccess: boolean;
- loading: boolean;
- saving: boolean;
- fetchPreferences: () => Promise;
- savePreferences: (prefs: UserPreferences) => Promise;
- applyColorScheme: (schemeId: string) => void;
-}
-
-export const useUserPreferences = (): UseUserPreferencesReturn => {
- const { authFetch, isAuthenticated } = useAuthenticatedFetch();
- const [preferences, setPreferences] = useState(DEFAULT_PREFERENCES);
- const [hasAIAccess, setHasAIAccess] = useState(false);
- const [loading, setLoading] = useState(false);
- const [saving, setSaving] = useState(false);
-
- const fetchPreferences = useCallback(async () => {
- if (!isAuthenticated) return;
-
- setLoading(true);
- try {
- const response = await authFetch('/api/users/me/preferences');
- if (response.ok) {
- const data = await response.json();
- const prefs: UserPreferences = {
- colorScheme: data.colorScheme || 'blue',
- geminiApiKey: data.geminiApiKey || undefined,
- };
- setPreferences(prefs);
- setHasAIAccess(data.hasAIAccess || false);
- applyColorScheme(prefs.colorScheme);
- }
- } catch {
- } finally {
- setLoading(false);
- }
- }, [authFetch, isAuthenticated]);
-
- const savePreferences = useCallback(async (prefs: UserPreferences) => {
- setSaving(true);
- try {
- const response = await authFetch('/api/users/me/preferences', {
- method: 'PUT',
- body: JSON.stringify(prefs),
- });
-
- if (!response.ok) {
- throw new Error('Failed to save preferences');
- }
-
- setPreferences(prefs);
- applyColorScheme(prefs.colorScheme);
- toast.success('Preferences saved!');
- } catch (err) {
- const message = err instanceof Error ? err.message : 'Failed to save preferences';
- toast.error(message);
- throw err;
- } finally {
- setSaving(false);
- }
- }, [authFetch]);
-
- useEffect(() => {
- fetchPreferences();
- }, [fetchPreferences]);
-
- return {
- preferences,
- hasAIAccess,
- loading,
- saving,
- fetchPreferences,
- savePreferences,
- applyColorScheme,
- };
-};
diff --git a/index.css b/index.css
index 1d21d0e..e8b64b7 100644
--- a/index.css
+++ b/index.css
@@ -1,11 +1,16 @@
+html, body, #root {
+ height: 100%;
+ min-height: 100dvh;
+}
+
.h-screen {
- height: 100dvh !important;
+ height: 100dvh;
}
.min-h-screen {
- min-height: 100dvh !important;
+ min-height: 100dvh;
}
.max-h-screen {
- max-height: 100dvh !important;
+ max-height: 100dvh;
}
diff --git a/index.html b/index.html
index 925b482..46b0b5c 100644
--- a/index.html
+++ b/index.html
@@ -33,19 +33,8 @@
font-family: 'Montserrat', sans-serif;
background: linear-gradient(180deg, var(--theme-primary) 0%, var(--theme-primary-darker) 100%);
color: white;
- overflow: hidden;
- height: 100dvh;
- margin: 0;
- padding: 0;
- }
-
- html {
- height: 100dvh;
- overflow: hidden;
- }
-
- #root {
- height: 100%;
+ overflow-x: hidden;
+ min-height: 100dvh;
}
h1, h2, h3, h4, h5, h6, .font-display {
diff --git a/package-lock.json b/package-lock.json
index 6c215ed..7323a05 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,7 +22,6 @@
"react-hot-toast": "^2.6.0",
"react-oidc-context": "^3.2.0",
"recharts": "^3.6.0",
- "unique-names-generator": "^4.7.1",
"uuid": "^13.0.0"
},
"devDependencies": {
@@ -4161,15 +4160,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/unique-names-generator": {
- "version": "4.7.1",
- "resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz",
- "integrity": "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
diff --git a/package.json b/package.json
index 1077f01..72eb488 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,6 @@
"react-hot-toast": "^2.6.0",
"react-oidc-context": "^3.2.0",
"recharts": "^3.6.0",
- "unique-names-generator": "^4.7.1",
"uuid": "^13.0.0"
},
"devDependencies": {
diff --git a/server/src/db/connection.ts b/server/src/db/connection.ts
index ce70dfd..8f654e3 100644
--- a/server/src/db/connection.ts
+++ b/server/src/db/connection.ts
@@ -60,18 +60,6 @@ const runMigrations = () => {
db.exec("ALTER TABLE game_sessions ADD COLUMN first_correct_player_id TEXT");
console.log("Migration: Added first_correct_player_id to game_sessions");
}
-
- const userTableInfo2 = db.prepare("PRAGMA table_info(users)").all() as { name: string }[];
- const hasColorScheme = userTableInfo2.some(col => col.name === "color_scheme");
- if (!hasColorScheme) {
- db.exec("ALTER TABLE users ADD COLUMN color_scheme TEXT DEFAULT 'blue'");
- console.log("Migration: Added color_scheme to users");
- }
- const hasGeminiKey = userTableInfo2.some(col => col.name === "gemini_api_key");
- if (!hasGeminiKey) {
- db.exec("ALTER TABLE users ADD COLUMN gemini_api_key TEXT");
- console.log("Migration: Added gemini_api_key to users");
- }
};
runMigrations();
diff --git a/server/src/db/schema.sql b/server/src/db/schema.sql
index f1e2cd6..86b3d49 100644
--- a/server/src/db/schema.sql
+++ b/server/src/db/schema.sql
@@ -5,9 +5,7 @@ CREATE TABLE IF NOT EXISTS users (
display_name TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME,
- default_game_config TEXT,
- color_scheme TEXT DEFAULT 'blue',
- gemini_api_key TEXT
+ default_game_config TEXT
);
CREATE TABLE IF NOT EXISTS quizzes (
diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts
index bbf6c14..a5aa677 100644
--- a/server/src/middleware/auth.ts
+++ b/server/src/middleware/auth.ts
@@ -34,7 +34,6 @@ export interface AuthenticatedUser {
preferred_username: string;
email?: string;
name?: string;
- groups?: string[];
}
export interface AuthenticatedRequest extends Request {
@@ -75,7 +74,6 @@ export function requireAuth(
preferred_username: payload.preferred_username || payload.sub!,
email: payload.email,
name: payload.name,
- groups: payload.groups || [],
};
next();
diff --git a/server/src/routes/games.ts b/server/src/routes/games.ts
index 0d6aa29..c083f1e 100644
--- a/server/src/routes/games.ts
+++ b/server/src/routes/games.ts
@@ -74,7 +74,6 @@ router.get('/:pin', (req: Request, res: Response) => {
return;
}
- const gameConfig = JSON.parse(session.game_config);
res.json({
pin: session.pin,
hostPeerId: session.host_peer_id,
@@ -82,7 +81,6 @@ router.get('/:pin', (req: Request, res: Response) => {
currentQuestionIndex: session.current_question_index,
quizTitle: JSON.parse(session.quiz_data).title,
playerCount: JSON.parse(session.players_data).length,
- randomNamesEnabled: gameConfig.randomNamesEnabled || false,
});
} catch (err) {
console.error('Error getting game session:', err);
diff --git a/server/src/routes/users.ts b/server/src/routes/users.ts
index 0860ea7..24925b5 100644
--- a/server/src/routes/users.ts
+++ b/server/src/routes/users.ts
@@ -8,16 +8,11 @@ router.use(requireAuth);
router.get('/me', (req: AuthenticatedRequest, res: Response) => {
const user = db.prepare(`
- SELECT id, username, email, display_name as displayName, default_game_config as defaultGameConfig,
- color_scheme as colorScheme, gemini_api_key as geminiApiKey,
- created_at as createdAt, last_login as lastLogin
+ SELECT id, username, email, display_name as displayName, default_game_config as defaultGameConfig, created_at as createdAt, last_login as lastLogin
FROM users
WHERE id = ?
`).get(req.user!.sub) as Record | undefined;
- const groups = req.user!.groups || [];
- const hasAIAccess = groups.includes('kaboot-ai-access');
-
if (!user) {
res.json({
id: req.user!.sub,
@@ -25,9 +20,6 @@ router.get('/me', (req: AuthenticatedRequest, res: Response) => {
email: req.user!.email,
displayName: req.user!.name,
defaultGameConfig: null,
- colorScheme: 'blue',
- geminiApiKey: null,
- hasAIAccess,
createdAt: null,
lastLogin: null,
isNew: true,
@@ -44,12 +36,7 @@ router.get('/me', (req: AuthenticatedRequest, res: Response) => {
}
}
- res.json({
- ...user,
- defaultGameConfig: parsedConfig,
- hasAIAccess,
- isNew: false
- });
+ res.json({ ...user, defaultGameConfig: parsedConfig, isNew: false });
});
router.put('/me/default-config', (req: AuthenticatedRequest, res: Response) => {
@@ -77,47 +64,4 @@ router.put('/me/default-config', (req: AuthenticatedRequest, res: Response) => {
res.json({ success: true });
});
-router.get('/me/preferences', (req: AuthenticatedRequest, res: Response) => {
- const user = db.prepare(`
- SELECT color_scheme as colorScheme, gemini_api_key as geminiApiKey
- FROM users
- WHERE id = ?
- `).get(req.user!.sub) as { colorScheme: string | null; geminiApiKey: string | null } | undefined;
-
- const groups = req.user!.groups || [];
- const hasAIAccess = groups.includes('kaboot-ai-access');
-
- res.json({
- colorScheme: user?.colorScheme || 'blue',
- geminiApiKey: user?.geminiApiKey || null,
- hasAIAccess,
- });
-});
-
-router.put('/me/preferences', (req: AuthenticatedRequest, res: Response) => {
- const { colorScheme, geminiApiKey } = req.body;
-
- const upsertUser = db.prepare(`
- INSERT INTO users (id, username, email, display_name, color_scheme, gemini_api_key, last_login)
- VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
- ON CONFLICT(id) DO UPDATE SET
- color_scheme = ?,
- gemini_api_key = ?,
- last_login = CURRENT_TIMESTAMP
- `);
-
- upsertUser.run(
- req.user!.sub,
- req.user!.preferred_username,
- req.user!.email || null,
- req.user!.name || null,
- colorScheme || 'blue',
- geminiApiKey || null,
- colorScheme || 'blue',
- geminiApiKey || null
- );
-
- res.json({ success: true });
-});
-
export default router;
diff --git a/types.ts b/types.ts
index 6b85014..26378a7 100644
--- a/types.ts
+++ b/types.ts
@@ -13,34 +13,6 @@ export type GameState =
| 'WAITING_TO_REJOIN'
| 'HOST_RECONNECTED';
-export interface ColorScheme {
- id: string;
- name: string;
- primary: string;
- primaryDark: string;
- primaryDarker: string;
-}
-
-export const COLOR_SCHEMES: ColorScheme[] = [
- { id: 'blue', name: 'Ocean Blue', primary: '#2563eb', primaryDark: '#1e40af', primaryDarker: '#1e3a5f' },
- { id: 'purple', name: 'Royal Purple', primary: '#7c3aed', primaryDark: '#5b21b6', primaryDarker: '#3b1a5f' },
- { id: 'pink', name: 'Hot Pink', primary: '#db2777', primaryDark: '#9d174d', primaryDarker: '#5f1a3b' },
- { id: 'red', name: 'Ruby Red', primary: '#dc2626', primaryDark: '#991b1b', primaryDarker: '#5f1a1a' },
- { id: 'orange', name: 'Sunset Orange', primary: '#ea580c', primaryDark: '#c2410c', primaryDarker: '#5f2a1a' },
- { id: 'yellow', name: 'Golden Yellow', primary: '#ca8a04', primaryDark: '#a16207', primaryDarker: '#5f4a1a' },
- { id: 'green', name: 'Emerald Green', primary: '#16a34a', primaryDark: '#15803d', primaryDarker: '#1a5f3b' },
- { id: 'teal', name: 'Teal', primary: '#0d9488', primaryDark: '#0f766e', primaryDarker: '#1a5f5a' },
- { id: 'cyan', name: 'Cyan', primary: '#0891b2', primaryDark: '#0e7490', primaryDarker: '#1a4a5f' },
- { id: 'indigo', name: 'Indigo', primary: '#4f46e5', primaryDark: '#4338ca', primaryDarker: '#2a2a5f' },
- { id: 'slate', name: 'Slate', primary: '#475569', primaryDark: '#334155', primaryDarker: '#1e293b' },
- { id: 'rose', name: 'Rose', primary: '#e11d48', primaryDark: '#be123c', primaryDarker: '#5f1a2a' },
-];
-
-export interface UserPreferences {
- colorScheme: string;
- geminiApiKey?: string;
-}
-
export type GameRole = 'HOST' | 'CLIENT';
export interface AnswerOption {
@@ -62,7 +34,6 @@ export interface GameConfig {
shuffleQuestions: boolean;
shuffleAnswers: boolean;
hostParticipates: boolean;
- randomNamesEnabled: boolean;
streakBonusEnabled: boolean;
streakThreshold: number;
streakMultiplier: number;
@@ -78,7 +49,6 @@ export const DEFAULT_GAME_CONFIG: GameConfig = {
shuffleQuestions: false,
shuffleAnswers: false,
hostParticipates: true,
- randomNamesEnabled: false,
streakBonusEnabled: false,
streakThreshold: 3,
streakMultiplier: 1.1,
@@ -172,7 +142,6 @@ export type NetworkMessage =
options?: AnswerOption[];
correctShape?: string;
timeLeft?: number;
- assignedName?: string;
} }
| { type: 'PLAYER_JOINED'; payload: { player: Player } }
| { type: 'GAME_START'; payload: {} }