diff --git a/css/style.css b/css/style.css
index e25e30e..dfffcef 100644
--- a/css/style.css
+++ b/css/style.css
@@ -740,9 +740,10 @@ kbd {
border: 1px solid;
white-space: nowrap;
}
-.rank-bronze { color: #cd7f32; background: rgba(205,127,50,0.12); border-color: rgba(205,127,50,0.35); }
-.rank-silver { color: #b8b8b8; background: rgba(192,192,192,0.12); border-color: rgba(192,192,192,0.35); }
-.rank-gold { color: #ffd700; background: rgba(255,215,0,0.12); border-color: rgba(255,215,0,0.4); }
+.rank-bronze { color: #cd7f32; background: rgba(205,127,50,0.12); border-color: rgba(205,127,50,0.35); }
+.rank-silver { color: #b8b8b8; background: rgba(192,192,192,0.12); border-color: rgba(192,192,192,0.35); }
+.rank-silver2 { color: #c0c8ff; background: rgba(192,200,255,0.15); border-color: rgba(192,200,255,0.4); }
+.rank-gold { color: #ffd700; background: rgba(255,215,0,0.12); border-color: rgba(255,215,0,0.4); }
/* ── Rank-up overlay ── */
.rankup-overlay {
@@ -784,6 +785,118 @@ kbd {
100% { opacity: 1; transform: scale(1) rotate(0deg); }
}
+/* ── Reaction bar ── */
+.reaction-bar {
+ display: flex;
+ gap: 0.5rem;
+ justify-content: center;
+}
+.reaction-btn {
+ font-size: 1.5rem;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ width: 3rem;
+ height: 3rem;
+ cursor: pointer;
+ transition: transform 0.15s, border-color 0.15s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.reaction-btn:hover { transform: scale(1.2); border-color: var(--accent); }
+.reaction-btn:active { transform: scale(0.9); }
+
+/* ── Reaction overlay ── */
+.reaction-overlay {
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 400;
+}
+.reaction-float {
+ position: absolute;
+ font-size: 4rem;
+ animation: reactionPop 1.8s ease forwards;
+ pointer-events: none;
+}
+@keyframes reactionPop {
+ 0% { opacity: 0; transform: translateY(0) scale(0.5); }
+ 20% { opacity: 1; transform: translateY(-20px) scale(1.3); }
+ 80% { opacity: 1; transform: translateY(-80px) scale(1); }
+ 100% { opacity: 0; transform: translateY(-120px) scale(0.8); }
+}
+
+/* ── Share + Submit row ── */
+.result-actions-row {
+ display: flex;
+ gap: 0.5rem;
+}
+.result-actions-row .btn-secondary { flex: 1; }
+
+/* ── Profile modal body ── */
+.profile-body { display: flex; flex-direction: column; align-items: center; gap: 0.8rem; }
+.profile-username { font-size: 1.5rem; font-weight: 900; }
+.profile-rank { }
+.profile-stats {
+ display: flex;
+ gap: 0.75rem;
+ width: 100%;
+}
+.profile-stat {
+ flex: 1;
+ background: var(--surface2);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 0.75rem 0.5rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+/* ── Clickable username in leaderboard ── */
+.lb-name-link {
+ cursor: pointer;
+ text-decoration: underline;
+ text-decoration-color: var(--border2);
+ text-underline-offset: 3px;
+ transition: color 0.15s;
+}
+.lb-name-link:hover { color: var(--accent); }
+
+/* ── History Q&A list ── */
+.history-qa {
+ display: none;
+ flex-direction: column;
+ gap: 0.2rem;
+ margin-top: 0.3rem;
+ padding-top: 0.3rem;
+ border-top: 1px solid var(--border);
+}
+.history-entry.expanded .history-qa { display: flex; }
+.history-qa-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 0.68rem;
+ color: var(--muted);
+}
+.history-qa-item.correct-q { color: var(--green); }
+.history-qa-item.wrong-q { color: var(--red); }
+.history-toggle {
+ background: none;
+ border: none;
+ color: var(--muted);
+ font-size: 0.68rem;
+ cursor: pointer;
+ padding: 0.1rem 0.3rem;
+ border-radius: 4px;
+ transition: color 0.15s;
+ align-self: flex-start;
+}
+.history-toggle:hover { color: var(--accent); }
+
/* ── History entries ── */
.history-entry {
background: var(--surface);
diff --git a/index.html b/index.html
index 928f099..30e4f12 100644
--- a/index.html
+++ b/index.html
@@ -3,7 +3,7 @@
- MathBlitz
+ QuantQuiz
@@ -15,6 +15,35 @@
3
+
+
+
+
+
+
+
PROFILE
+
+
—
+
+
+
+ Total XP
+ 0
+
+
+ Streak
+ 0
+
+
+ Best Streak
+ 0
+
+
+
+
+
+
+
@@ -85,7 +114,7 @@
-
MathBlitz
+
QuantQuiz
Sharpen your mind. Beat the clock. Crush the competition.
@@ -261,7 +290,10 @@ Game Over
-
+
+
+
+
@@ -277,9 +309,9 @@
Leaderboard
Mode
+
-
@@ -370,6 +402,12 @@
Finding Opponent
+
+
+
+
+
+
diff --git a/js/daily.js b/js/daily.js
index 904fcba..f9025fa 100644
--- a/js/daily.js
+++ b/js/daily.js
@@ -69,7 +69,7 @@ const Daily = (() => {
// ── Completion tracking (localStorage) ───────────────────────────────────
- function storageKey() { return `mathblitz_daily_${getTodayISO()}`; }
+ function storageKey() { return `quantquiz_daily_${getTodayISO()}`; }
function hasCompletedToday() {
return localStorage.getItem(storageKey()) === 'done';
diff --git a/js/db.js b/js/db.js
index 95c014c..0a8af87 100644
--- a/js/db.js
+++ b/js/db.js
@@ -204,12 +204,33 @@ const DB = (() => {
return data ?? [];
}
+ async function getGlobalLeaderboard(limit = 25) {
+ if (!_client) return [];
+ const { data, error } = await _client
+ .from('profiles')
+ .select('username, total_xp, current_streak, longest_streak')
+ .order('total_xp', { ascending: false })
+ .limit(limit);
+ if (error) throw error;
+ return data ?? [];
+ }
+
+ async function getUserProfileByUsername(username) {
+ if (!_client) return null;
+ const { data } = await _client
+ .from('profiles')
+ .select('username, total_xp, current_streak, longest_streak')
+ .eq('username', username)
+ .single();
+ return data;
+ }
+
return {
isConfigured: SUPABASE_CONFIGURED,
signUp, signIn, signOut, getUser, getProfile, onAuthChange,
saveScore, updateStreak,
saveDailyScore, getDailyLeaderboard, hasUserCompletedDaily,
- getLeaderboard,
+ getLeaderboard, getGlobalLeaderboard, getUserProfileByUsername,
getCommunityQuestions, submitCommunityQuestion,
};
})();
diff --git a/js/duel-client.js b/js/duel-client.js
index cceae4c..026a1c6 100644
--- a/js/duel-client.js
+++ b/js/duel-client.js
@@ -28,7 +28,7 @@ const DuelClient = (() => {
const events = [
'searching', 'matched', 'countdown', 'duel_start',
'answer_result', 'opponent_update', 'timer_tick',
- 'duel_end', 'match_cancelled',
+ 'duel_end', 'match_cancelled', 'reaction',
];
events.forEach(e => socket.on(e, data => _dispatch(e, data)));
@@ -47,9 +47,10 @@ const DuelClient = (() => {
socket.emit('find_match', { username });
}
- function cancelMatch() { socket?.emit('cancel_match'); }
- function submitAnswer(answer) { socket?.emit('submit_answer', { answer }); }
- function forfeit() { socket?.emit('forfeit'); disconnect(); }
+ function cancelMatch() { socket?.emit('cancel_match'); }
+ function submitAnswer(answer) { socket?.emit('submit_answer', { answer }); }
+ function forfeit() { socket?.emit('forfeit'); disconnect(); }
+ function sendReaction(emoji) { socket?.emit('send_reaction', { emoji }); }
- return { on, connect, disconnect, findMatch, cancelMatch, submitAnswer, forfeit };
+ return { on, connect, disconnect, findMatch, cancelMatch, submitAnswer, forfeit, sendReaction };
})();
diff --git a/js/game.js b/js/game.js
index d0a2266..a0f1360 100644
--- a/js/game.js
+++ b/js/game.js
@@ -37,9 +37,10 @@ const Game = (() => {
questionIdx: 0,
totalQuestions: predefined ? predefined.length : (settings.mode === 'sprint' ? SPRINT_QUESTIONS : null),
predefined,
- currentQ: null,
- startTime: Date.now(),
- active: false,
+ currentQ: null,
+ answeredQuestions: [],
+ startTime: Date.now(),
+ active: false,
};
}
@@ -116,6 +117,15 @@ const Game = (() => {
state.wrong++;
}
+ if (state.answeredQuestions.length < 30) {
+ state.answeredQuestions.push({
+ display: state.currentQ.display,
+ answer: state.currentQ.answer,
+ given: userAnswer,
+ correct: isCorrect,
+ });
+ }
+
state.questionIdx++;
if (isGameOver()) {
diff --git a/js/main.js b/js/main.js
index e600669..a54081b 100644
--- a/js/main.js
+++ b/js/main.js
@@ -5,9 +5,10 @@
// ── Rank system (global so ui.js leaderboard rendering can access) ─────────
const RANKS = [
- { name: 'Bronze', icon: '🥉', min: 0, cls: 'rank-bronze' },
- { name: 'Silver', icon: '🥈', min: 1000, cls: 'rank-silver' },
- { name: 'Gold', icon: '🥇', min: 5000, cls: 'rank-gold' },
+ { name: 'Bronze', icon: '🥉', min: 0, cls: 'rank-bronze' },
+ { name: 'Silver I', icon: '🥈', min: 1000, cls: 'rank-silver' },
+ { name: 'Silver II', icon: '🥈', min: 2500, cls: 'rank-silver2' },
+ { name: 'Gold', icon: '🥇', min: 5000, cls: 'rank-gold' },
];
function getRankForXP(xp) {
let r = RANKS[0];
@@ -35,9 +36,9 @@ const App = (() => {
// ── XP & Rank helpers ─────────────────────────────────────────────────────
- function getTotalXP() { return parseInt(localStorage.getItem('mathblitz_total_xp') || '0', 10); }
- function addXP(pts) { const n = getTotalXP() + pts; localStorage.setItem('mathblitz_total_xp', n); return n; }
- function canSubmit(xp) { return xp >= RANKS[1].min; } // Silver+
+ function getTotalXP() { return parseInt(localStorage.getItem('quantquiz_total_xp') || '0', 10); }
+ function addXP(pts) { const n = getTotalXP() + pts; localStorage.setItem('quantquiz_total_xp', n); return n; }
+ function canSubmit(xp) { return xp >= RANKS[2].min; } // Silver II+
function updateRankBadge(xp) {
const badge = document.getElementById('rank-badge');
@@ -50,17 +51,18 @@ const App = (() => {
// ── History helpers ───────────────────────────────────────────────────────
function saveGameHistory(state, elapsed) {
- const key = 'mathblitz_history';
+ const key = 'quantquiz_history';
const hist = JSON.parse(localStorage.getItem(key) || '[]');
hist.unshift({
- ts: Date.now(),
- mode: state.mode,
- diff: state.difficulty,
- score: state.score,
- correct: state.correct,
- wrong: state.wrong,
- streak: state.bestStreak,
- elapsed: parseFloat(elapsed),
+ ts: Date.now(),
+ mode: state.mode,
+ diff: state.difficulty,
+ score: state.score,
+ correct: state.correct,
+ wrong: state.wrong,
+ streak: state.bestStreak,
+ elapsed: parseFloat(elapsed),
+ questions: state.answeredQuestions ?? [],
});
if (hist.length > 20) hist.pop();
localStorage.setItem(key, JSON.stringify(hist));
@@ -70,8 +72,8 @@ const App = (() => {
function storageKey(mode, diff, time) {
return mode === 'classic'
- ? `mathblitz_best_${mode}_${diff}_${time}s`
- : `mathblitz_best_${mode}_${diff}`;
+ ? `quantquiz_best_${mode}_${diff}_${time}s`
+ : `quantquiz_best_${mode}_${diff}`;
}
function getBest(mode, diff, time) {
const v = localStorage.getItem(storageKey(mode, diff, time));
@@ -270,12 +272,12 @@ const App = (() => {
function initThemes() {
ThemeBG.init();
- const saved = localStorage.getItem('mathblitz_theme') || 'void';
+ const saved = localStorage.getItem('quantquiz_theme') || 'void';
applyTheme(saved);
document.querySelectorAll('.theme-dot').forEach(btn => {
btn.addEventListener('click', () => {
applyTheme(btn.dataset.theme);
- localStorage.setItem('mathblitz_theme', btn.dataset.theme);
+ localStorage.setItem('quantquiz_theme', btn.dataset.theme);
});
});
}
@@ -301,7 +303,7 @@ const App = (() => {
btn.textContent = '🔔✓';
btn.style.color = 'var(--green)';
// Show a test notification
- new Notification('MathBlitz', {
+ new Notification('QuantQuiz', {
body: "Notifications enabled! We'll remind you about the daily challenge.",
icon: '/icon.png',
});
@@ -365,6 +367,7 @@ const App = (() => {
gameSettings.predefinedQuestions = qs.length ? qs : Daily.generateQuestions();
}
+ Sound.gameStart();
const state = Game.start(gameSettings);
UI.updateHUD(state);
UI.showQuestion(Game.getCurrentQuestion());
@@ -536,7 +539,8 @@ const App = (() => {
alert('Fill in js/config.js with your Supabase credentials first.');
return;
}
- lb.mode = settings.mode === 'daily' ? 'daily' : settings.mode;
+ const validLbModes = ['global', 'classic', 'sprint', 'daily'];
+ lb.mode = validLbModes.includes(settings.mode) ? settings.mode : 'classic';
lb.difficulty = settings.difficulty;
lb.timeLimit = settings.timeLimit;
syncLbFilterUI();
@@ -552,7 +556,7 @@ const App = (() => {
document.querySelectorAll('[data-lb-time]').forEach(b =>
b.classList.toggle('active', parseInt(b.dataset.lbTime) === lb.timeLimit));
const showTime = lb.mode === 'classic';
- const showDiff = lb.mode !== 'daily';
+ const showDiff = lb.mode !== 'daily' && lb.mode !== 'global';
document.getElementById('lb-time-section').classList.toggle('hidden', !showTime);
document.querySelectorAll('[data-lb-diff]').forEach(b =>
b.closest('.section')?.classList.toggle('hidden', !showDiff));
@@ -595,9 +599,14 @@ const App = (() => {
list.innerHTML = 'Loading...
';
try {
- const rows = lb.mode === 'daily'
- ? await DB.getDailyLeaderboard(Daily.getTodayISO())
- : await DB.getLeaderboard(lb.mode, lb.difficulty, lb.timeLimit);
+ let rows;
+ if (lb.mode === 'global') {
+ rows = await DB.getGlobalLeaderboard();
+ } else if (lb.mode === 'daily') {
+ rows = await DB.getDailyLeaderboard(Daily.getTodayISO());
+ } else {
+ rows = await DB.getLeaderboard(lb.mode, lb.difficulty, lb.timeLimit);
+ }
if (rows.length === 0) {
list.innerHTML = 'No scores yet. Be the first!
';
@@ -605,13 +614,20 @@ const App = (() => {
}
list.innerHTML = rows.map((row, i) => {
- const username = row.profiles?.username ?? 'anonymous';
- const rank = i + 1;
- const isMe = currentUsername && username === currentUsername;
- const medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : `#${rank}`;
-
- let primary, secondary;
- if (lb.mode === 'sprint') {
+ const username = lb.mode === 'global'
+ ? (row.username ?? 'anonymous')
+ : (row.profiles?.username ?? 'anonymous');
+ const pos = i + 1;
+ const isMe = currentUsername && username === currentUsername;
+ const medal = pos === 1 ? '🥇' : pos === 2 ? '🥈' : pos === 3 ? '🥉' : `#${pos}`;
+
+ let primary, secondary, rankBadge = '';
+ if (lb.mode === 'global') {
+ const r = getRankForXP(row.total_xp ?? 0);
+ rankBadge = `${r.icon} ${r.name}`;
+ primary = (row.total_xp ?? 0) + ' XP';
+ secondary = `streak ${row.current_streak ?? 0} days`;
+ } else if (lb.mode === 'sprint') {
primary = row.elapsed_seconds.toFixed(2) + 's';
secondary = `${row.correct}/10 correct`;
} else if (lb.mode === 'daily') {
@@ -625,13 +641,19 @@ const App = (() => {
return `
${medal}
-
${username}${isMe ? ' (you)' : ''}
+
+ ${username}${isMe ? ' (you)' : ''}${rankBadge}
+
${primary}
${secondary}
`;
}).join('');
+
+ list.querySelectorAll('.lb-name-link').forEach(el => {
+ el.addEventListener('click', () => showProfile(el.dataset.username));
+ });
} catch (e) {
list.innerHTML = `Failed to load: ${e.message}
`;
}
@@ -796,6 +818,27 @@ const App = (() => {
});
DuelClient.on('match_cancelled', () => UI.showScreen('menu'));
+
+ DuelClient.on('reaction', ({ emoji }) => {
+ spawnReaction(emoji);
+ });
+
+ document.querySelectorAll('.reaction-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ DuelClient.sendReaction(btn.dataset.emoji);
+ });
+ });
+ }
+
+ function spawnReaction(emoji) {
+ const overlay = document.getElementById('reaction-overlay');
+ const el = document.createElement('div');
+ el.className = 'reaction-float';
+ el.textContent = emoji;
+ el.style.left = `${20 + Math.random() * 60}%`;
+ el.style.top = `${30 + Math.random() * 30}%`;
+ overlay.appendChild(el);
+ el.addEventListener('animationend', () => el.remove());
}
function startDuelSearch() {
@@ -811,7 +854,7 @@ const App = (() => {
// ── History ───────────────────────────────────────────────────────────────
function showHistory() {
- const hist = JSON.parse(localStorage.getItem('mathblitz_history') || '[]');
+ const hist = JSON.parse(localStorage.getItem('quantquiz_history') || '[]');
UI.renderHistory(hist);
UI.showScreen('history');
}
@@ -820,6 +863,55 @@ const App = (() => {
document.getElementById('history-back').addEventListener('click', () => UI.showScreen('menu'));
}
+ // ── Share ─────────────────────────────────────────────────────────────────
+
+ function initShare() {
+ document.getElementById('share-btn').addEventListener('click', () => {
+ const score = document.getElementById('res-score').textContent;
+ const correct = document.getElementById('res-correct').textContent;
+ const acc = document.getElementById('res-accuracy').textContent;
+ const mode = settings.mode.charAt(0).toUpperCase() + settings.mode.slice(1);
+ const text = `QuantQuiz ${mode} — ${score} pts | ${correct} correct | ${acc} accuracy\nPlay at https://mathblitz-jade.vercel.app`;
+
+ if (navigator.share) {
+ navigator.share({ title: 'QuantQuiz', text }).catch(() => {});
+ } else {
+ navigator.clipboard.writeText(text).then(() => {
+ const btn = document.getElementById('share-btn');
+ btn.textContent = 'Copied!';
+ setTimeout(() => { btn.textContent = '↗ Share'; }, 2000);
+ });
+ }
+ });
+ }
+
+ // ── Profile modal ─────────────────────────────────────────────────────────
+
+ function initProfileModal() {
+ document.getElementById('profile-close-btn').addEventListener('click', () => {
+ document.getElementById('profile-modal').classList.remove('active');
+ });
+ }
+
+ async function showProfile(username) {
+ document.getElementById('profile-username').textContent = username;
+ document.getElementById('profile-xp').textContent = '...';
+ document.getElementById('profile-streak').textContent = '...';
+ document.getElementById('profile-longest').textContent = '...';
+ document.getElementById('profile-rank').innerHTML = '';
+ document.getElementById('profile-modal').classList.add('active');
+
+ const data = await DB.getUserProfileByUsername(username);
+ if (!data) return;
+
+ const rank = getRankForXP(data.total_xp ?? 0);
+ document.getElementById('profile-xp').textContent = data.total_xp ?? 0;
+ document.getElementById('profile-streak').textContent = data.current_streak ?? 0;
+ document.getElementById('profile-longest').textContent = data.longest_streak ?? 0;
+ document.getElementById('profile-rank').innerHTML =
+ `${rank.icon} ${rank.name}`;
+ }
+
// ── Submit Problem ────────────────────────────────────────────────────────
function initSubmitProblem() {
@@ -907,6 +999,8 @@ const App = (() => {
initServiceWorker();
initHistory();
initSubmitProblem();
+ initShare();
+ initProfileModal();
updateTimeSectionVisibility();
updateRankBadge(getTotalXP());
refreshBest();
diff --git a/js/sound.js b/js/sound.js
index 987f989..ee0a6f2 100644
--- a/js/sound.js
+++ b/js/sound.js
@@ -67,10 +67,15 @@ const Sound = (() => {
}
}
+ function gameStart() {
+ if (!enabled) return;
+ vibrate([80, 40, 80, 40, 160]);
+ }
+
function toggle() {
enabled = !enabled;
return enabled;
}
- return { correct, wrong, streakMilestone, countdown, toggle };
+ return { correct, wrong, streakMilestone, countdown, gameStart, toggle };
})();
diff --git a/js/ui.js b/js/ui.js
index 083859f..c71dadb 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -166,22 +166,48 @@ const UI = (() => {
list.innerHTML = 'No games yet.
';
return;
}
- list.innerHTML = entries.map(e => {
+ list.innerHTML = entries.map((e, i) => {
const date = new Date(e.ts).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
const mode = MODE_LABEL[e.mode] ?? e.mode;
const diff = DIFF_LABEL[e.diff] ?? '';
const elapsed = e.mode === 'sprint' ? ` · ${e.elapsed}s` : '';
const total = e.correct + e.wrong;
const acc = total > 0 ? Math.round((e.correct / total) * 100) : 0;
+
+ const qaHtml = e.questions && e.questions.length
+ ? `` +
+ e.questions.map(q =>
+ `
+ ${q.display}
+ ${q.correct ? '✓ ' + q.answer : '✗ ' + q.given + ' → ' + q.answer}
+
`
+ ).join('') +
+ `
`
+ : '';
+
+ const toggleBtn = e.questions && e.questions.length
+ ? ``
+ : '';
+
return `
-
+
${e.correct}✓ ${e.wrong}✗ · ${acc}% · streak ×${e.streak}${elapsed} · ${date}
+ ${toggleBtn}
+ ${qaHtml}
`;
}).join('');
+
+ list.querySelectorAll('.history-toggle').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const entry = btn.closest('.history-entry');
+ const open = entry.classList.toggle('expanded');
+ btn.textContent = open ? '▲ Questions' : '▼ Questions';
+ });
+ });
}
return {
diff --git a/server/package.json b/server/package.json
index 3c33bdf..0c9c150 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,7 +1,7 @@
{
- "name": "mathblitz-server",
+ "name": "quantquiz-server",
"version": "1.0.0",
- "description": "MathBlitz real-time duel server",
+ "description": "QuantQuiz real-time duel server",
"main": "server.js",
"scripts": {
"start": "node server.js",
diff --git a/server/server.js b/server/server.js
index 60421ce..eb71b83 100644
--- a/server/server.js
+++ b/server/server.js
@@ -1,5 +1,5 @@
/**
- * server.js — MathBlitz duel server
+ * server.js — QuantQuiz duel server
* Express + Socket.io. Handles matchmaking and real-time duels.
*
* Usage: node server.js
@@ -82,6 +82,12 @@ io.on('connection', socket => {
});
});
+ socket.on('send_reaction', ({ emoji }) => {
+ const { roomId } = socket.data;
+ if (!roomId) return;
+ socket.to(roomId).emit('reaction', { emoji });
+ });
+
socket.on('forfeit', () => {
const { roomId, playerIdx } = socket.data;
const room = rooms[roomId];
@@ -203,6 +209,6 @@ function endRoom(roomId, forcedWinner, reason) {
// ── Start ─────────────────────────────────────────────────────────────────────
server.listen(PORT, () => {
- console.log(`\nMathBlitz duel server → http://localhost:${PORT}`);
+ console.log(`\nQuantQuiz duel server → http://localhost:${PORT}`);
console.log('Waiting for players...\n');
});
diff --git a/sw.js b/sw.js
index d886fce..1ca7fb8 100644
--- a/sw.js
+++ b/sw.js
@@ -1,9 +1,9 @@
/**
- * sw.js — MathBlitz Service Worker
+ * sw.js — QuantQuiz Service Worker
* Handles push notifications. Requires HTTPS to activate.
*/
-const CACHE = 'mathblitz-v3';
+const CACHE = 'quantquiz-v3';
// Cache core files on install
self.addEventListener('install', event => {
@@ -38,7 +38,7 @@ self.addEventListener('fetch', event => {
self.addEventListener('push', event => {
const data = event.data?.json() ?? {};
event.waitUntil(
- self.registration.showNotification(data.title || 'MathBlitz', {
+ self.registration.showNotification(data.title || 'QuantQuiz', {
body: data.body || "Your daily challenge is ready! Can you top the leaderboard today? 🧠",
icon: '/icon.png',
badge: '/icon.png',