Files
homeschool/public/js/game.js
T
nmemmert 8d37038177 feat: Complete tablet UI overhaul with popup menu system and help bubble
Major Changes:
- 🪟 Popup Menu System: Converted slide-down navigation to draggable, minimizable popup window
- ❓ Help Bubble: Added contextual help system for admin dashboard (accessible via ? bubble)
- 🎨 Universal Theme Engine: All elements now properly match selected theme across all panels
- 📱 Enhanced Tablet Support: Comprehensive touch gestures, handwriting recognition, and Apple Pencil optimization
- 🖊️ Unified Write Mode: Combined handwriting and keyboard input with large canvas interface

Technical Implementation:
- Draggable popup with minimize/restore functionality and boundary constraints
- Dynamic theme switching using CSS variables throughout all components
- Help system with modal overlay and keyboard shortcuts (ESC to close)
- Responsive design optimizations for mobile, tablet, and desktop
- Comprehensive CSS overrides to eliminate hardcoded colors
- JavaScript fixes for canvas stroke colors and input styling

UI/UX Improvements:
- Clean admin dashboard without instruction clutter
- Professional popup interface with smooth animations
- Better contrast and readability across all themes (default, playground, chalkboard, galaxy, nature)
- Improved accessibility with proper focus management
- Enhanced mobile responsiveness with touch-optimized controls

Files Modified:
- Menu system: tablet-menu.js, tablet-menu.css
- Theme engine: styles.css, themes integration
- Admin interface: index.html, ui.js
- Input systems: game.js, unified-write.js, handwriting.js
- Authentication: auth.js with tablet menu cleanup
2025-10-02 14:26:04 -04:00

531 lines
19 KiB
JavaScript

// 🌍 Game state
let words = [];
let currentWord = '';
let currentIndex = 0;
let results = [];
let typingWords = [];
let isSpacedRepetitionMode = false;
// 📝 Check user's answer and move to next word regardless of correctness
window.submitAnswer = function(providedAnswer = null) {
// Get input from unified write mode or legacy inputs
let input = '';
// First check if answer was provided directly (from unified write mode)
if (providedAnswer) {
input = providedAnswer.trim();
} else if (window.unifiedWriteMode) {
// Get from unified write mode
input = window.unifiedWriteMode.getText();
if (!input) {
alert('Please enter an answer first!');
return;
}
} else {
// Legacy fallback
const typingInput = document.getElementById('userInput');
const recognizedText = document.getElementById('recognizedText');
const handwritingContainer = document.getElementById('handwritingContainer');
// Check which input mode is active
if (handwritingContainer && handwritingContainer.style.display !== 'none') {
// Handwriting mode - get recognized text
input = recognizedText ? recognizedText.textContent.trim() : '';
if (input === 'Draw to see recognized text here' || input === '') {
alert('Please write something or switch to typing mode');
return;
}
} else {
// Typing mode - get typed input
input = typingInput.value.trim();
if (input === '') {
alert('Please type something or switch to handwriting mode');
return;
}
}
}
// Record the attempt
results.push({
word: currentWord,
attempt: input,
inputMode: handwritingContainer && handwritingContainer.style.display !== 'none' ? 'handwriting' : 'typing'
});
// Show feedback and move to next word regardless of correctness
if (input.toLowerCase() === currentWord.toLowerCase()) {
// Correct answer - add visual feedback
const inputEl = document.getElementById('userInput');
inputEl.style.borderColor = 'var(--success)';
inputEl.style.backgroundColor = 'var(--bg-secondary)';
// Add feedback text if element exists
const feedbackEl = document.getElementById('wordFeedback');
if (feedbackEl) {
feedbackEl.textContent = '✓ Correct!';
feedbackEl.className = 'feedback-correct';
feedbackEl.style.color = 'var(--success)';
}
} else {
// Incorrect answer - show correct spelling but continue
const inputEl = document.getElementById('userInput');
inputEl.style.borderColor = 'var(--error)';
inputEl.style.backgroundColor = 'var(--bg-secondary)';
// Add feedback text if element exists
const feedbackEl = document.getElementById('wordFeedback');
if (feedbackEl) {
feedbackEl.textContent = `✗ Correct spelling: ${currentWord}`;
feedbackEl.className = 'feedback-incorrect';
feedbackEl.style.color = 'var(--error)';
}
}
// Always advance to next word
currentIndex++;
document.getElementById('inputSection').classList.add('hidden');
// Small delay before showing next word
setTimeout(() => showWord(), 1500);
}
// Add Enter key support for game input
document.addEventListener('DOMContentLoaded', () => {
const gameInput = document.getElementById('userInput');
if (gameInput) {
gameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
submitAnswer();
}
});
}
// Add keyboard support for unified write mode
if (window.unifiedWriteMode) {
document.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && document.activeElement === window.unifiedWriteMode.canvas) {
submitAnswer();
}
});
}
});
let currentSentence = '';
function showStudent() {
console.log("🔊 Showing student panel");
// Hide admin panel
document.getElementById('adminPanel')?.classList.add('hidden');
// Show student panel and its components
const studentPanel = document.getElementById('studentPanel');
studentPanel?.classList.remove('hidden');
// Get the buttons and explicitly set their properties
const gameBtn = document.getElementById('startGameBtn');
const typingBtn = document.getElementById('startTypingBtn');
if (gameBtn && typingBtn) {
console.log("Found buttons, making visible");
// Remove all possible hiding classes/styles
gameBtn.classList.remove('hidden');
typingBtn.classList.remove('hidden');
// Explicitly set display style
gameBtn.style.cssText = 'display: inline-block !important; visibility: visible !important;';
typingBtn.style.cssText = 'display: inline-block !important; visibility: visible !important;';
} else {
console.error("Could not find game buttons");
}
}
// 🚀 Begin game session
window.startGame = async function(customWords = null, isSpacedRepetition = false) {
console.log('🎮 Starting spelling game');
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) {
alert('Please log in first');
return;
}
try {
// Hide student dashboard and other sections
document.getElementById('studentPanel').style.display = 'none';
document.getElementById('typingSection').style.display = 'none';
document.getElementById('bibleSection').style.display = 'none';
// Show game section
document.getElementById('gameSection').classList.remove('hidden');
document.getElementById('gameSection').style.display = 'block';
document.getElementById('wordBox').textContent = 'Loading...';
document.getElementById('summary')?.classList.add('hidden');
document.getElementById('badgeDisplay')?.classList.add('hidden');
let wordList;
// Use custom words if provided (for spaced repetition)
if (customWords && Array.isArray(customWords) && customWords.length > 0) {
wordList = customWords;
isSpacedRepetitionMode = isSpacedRepetition;
console.log('Using custom word list for spaced repetition:', wordList);
} else {
// Fetch words from user's word list
const response = await fetch(`/getWordList?username=${user.username}`);
const data = await response.json();
console.log('Received words:', data);
let weekWords = [];
// Safely handle various response formats
try {
if (data && data.words && Array.isArray(data.words)) {
// Server returns {words: [array]}
weekWords = data.words;
} else if (data && data.words && typeof data.words === 'object' && data.words.weeks && data.words.activeWeek) {
// Server returns {words: {weeks: [], activeWeek: ""}}
const active = data.words.activeWeek;
const found = data.words.weeks.find(w => w.date === active);
if (found && Array.isArray(found.words)) {
weekWords = found.words;
}
} else if (Array.isArray(data)) {
// Server returns direct array
weekWords = data;
} else if (typeof data === 'object' && Array.isArray(Object.values(data)[0])) {
// Handle unexpected format as best we can
weekWords = Object.values(data)[0];
}
} catch (err) {
console.error("Error processing word list data:", err);
}
wordList = weekWords.length > 0 ? weekWords : ["default", "practice", "words"];
}
if (!wordList.length) {
alert('No words found for practice');
return;
}
// Start game
words = [...wordList];
typingWords = [...wordList]; // Initialize typing words too
currentIndex = 0;
results = []; // Reset results array
showWord();
} catch (error) {
console.error('Error:', error);
alert('Failed to load words');
}
}
function showWord() {
if (currentIndex >= words.length) {
showSummary();
return;
}
currentWord = words[currentIndex];
console.log('Current word:', currentWord);
// Show word briefly then hide
const wordBox = document.getElementById('wordBox');
const inputSection = document.getElementById('inputSection');
// Reset any feedback text
const feedbackEl = document.getElementById('wordFeedback');
if (feedbackEl) {
feedbackEl.textContent = '';
feedbackEl.className = '';
}
wordBox.textContent = currentWord;
inputSection.classList.add('hidden');
setTimeout(() => {
wordBox.textContent = '';
// Make sure input section is visible
inputSection.classList.remove('hidden');
inputSection.style.display = 'block';
// Reset input (unified write mode or legacy)
if (window.unifiedWriteMode) {
window.unifiedWriteMode.clearText();
} else {
// Legacy input reset
const userInput = document.getElementById('userInput');
if (userInput) {
userInput.value = '';
userInput.classList.add('visible-input');
}
// Reset handwriting input
if (window.clearHandwriting) {
window.clearHandwriting();
}
// Reset recognition status and text
const recognitionStatus = document.getElementById('recognitionStatus');
const recognizedText = document.getElementById('recognizedText');
if (recognitionStatus) {
recognitionStatus.className = 'recognition-status empty';
recognitionStatus.textContent = '';
}
if (recognizedText) {
recognizedText.textContent = 'Draw to see recognized text here';
recognizedText.className = 'recognized-text';
}
}
// Focus appropriate input
setTimeout(() => {
if (window.unifiedWriteMode) {
// Focus unified write mode
window.unifiedWriteMode.focusCanvas();
console.log('🎯 Unified write mode focused, ready for input');
} else {
// Legacy focus logic
const userInput = document.getElementById('userInput');
const handwritingContainer = document.getElementById('handwritingContainer');
if (handwritingContainer && handwritingContainer.style.display !== 'none') {
// Handwriting mode - focus canvas
const canvas = document.getElementById('handwritingCanvas');
if (canvas) {
canvas.focus();
console.log('🎨 Canvas focused, ready for handwriting');
}
} else if (userInput) {
// Typing mode - focus input
userInput.focus();
try {
userInput.setSelectionRange(0, 0);
} catch (e) {
console.log('Could not set selection range:', e);
}
console.log('🔍 Input focused, ready for typing');
}
}
}, 50);
}, 2000);
}
function showSummary() {
console.log('Game completed, showing summary');
document.getElementById('inputSection').classList.add('hidden');
// Calculate stats
let correctCount = 0;
const incorrectWords = [];
results.forEach(result => {
if (result.attempt.toLowerCase() === result.word.toLowerCase()) {
correctCount++;
} else {
incorrectWords.push(result);
}
});
const accuracy = Math.round((correctCount / results.length) * 100);
// Show summary
document.getElementById('summary').classList.remove('hidden');
document.getElementById('totalWords').textContent = results.length;
document.getElementById('correctWords').textContent = correctCount;
document.getElementById('accuracy').textContent = `${accuracy}%`;
// Show incorrect words
const incorrectList = document.getElementById('incorrectList');
incorrectList.innerHTML = '';
if (incorrectWords.length === 0) {
incorrectList.innerHTML = '<li>Perfect! No mistakes.</li>';
} else {
incorrectWords.forEach(item => {
incorrectList.innerHTML += `
<li>
<span class="word">${item.word}</span>
<span class="attempt">${item.attempt}</span>
</li>
`;
});
}
// Save results if not in spaced repetition mode
if (!isSpacedRepetitionMode) {
saveResults(results, accuracy);
} else {
console.log('In spaced repetition mode, not saving normal results');
// Update spaced repetition data
updateSpacedRepetitionResults(results);
}
}
async function saveResults(resultData, accuracy) {
try {
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) return;
// Transform the result data to match server's expected format
const answers = resultData.map(item => ({
word: item.word,
attempt: item.attempt,
correct: item.attempt.toLowerCase() === item.word.toLowerCase()
}));
const payload = {
username: user.username,
result: {
score: accuracy,
completed: true,
answers: answers,
timestamp: new Date().toISOString()
}
};
const response = await fetch('/saveResults', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
console.log('Results saved successfully');
checkForBadges(accuracy, resultData.length);
}
} catch (error) {
console.error('Error saving results:', error);
}
}
async function checkForBadges(accuracy, wordCount) {
try {
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) return;
// Check for earned badges
const response = await fetch(`/checkBadges?username=${user.username}&accuracy=${accuracy}&wordCount=${wordCount}`);
const data = await response.json();
if (data && data.newBadges && data.newBadges.length > 0) {
// Show badge notification
showBadgeNotification(data.newBadges);
}
} catch (error) {
console.error('Error checking badges:', error);
}
}
function showBadgeNotification(badges) {
const badgeDisplay = document.getElementById('badgeDisplay');
if (!badgeDisplay) {
console.error('Badge display element not found!');
return;
}
const badgeList = document.getElementById('badgeList');
if (!badgeList) {
console.error('Badge list element not found!');
return;
}
badgeList.innerHTML = '';
badges.forEach(badge => {
// Ensure badge has all required properties
const badgeIcon = badge.icon || '🏅';
const badgeName = badge.name || 'Achievement Unlocked!';
const description = badge.description || 'Great job on your spelling practice!';
// Log the badge data to ensure it's correct
console.log('Badge data:', { icon: badgeIcon, name: badgeName, description: description });
badgeList.innerHTML += `
<div class="badge-item">
<div class="badge-icon">${badgeIcon}</div>
<div class="badge-info">
<h4>${badgeName}</h4>
<p>${description}</p>
</div>
</div>
`;
});
console.log('Showing badge notification for', badges.length, 'badges');
badgeDisplay.classList.remove('hidden');
}
// Function to check if user has completed all words for today
async function checkDailyCompletion() {
try {
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) return;
const response = await fetch(`/checkDailyCompletion?username=${user.username}`);
const data = await response.json();
return data.completed;
} catch (error) {
console.error('Error checking daily completion:', error);
return false;
}
}
// Return to student panel
window.returnToStudentPanel = function() {
// Hide game and typing sections
document.getElementById('gameSection').style.display = 'none';
document.getElementById('typingSection').style.display = 'none';
document.getElementById('bibleSection').style.display = 'none';
// Show student panel
document.getElementById('studentPanel').style.display = 'block';
// Force panel to be visible
showStudent();
}
// Alias for returnToStudentPanel for backward compatibility
window.backToStudentDashboard = function() {
window.returnToStudentPanel();
}
// Handle spaced repetition results
async function updateSpacedRepetitionResults(resultData) {
try {
const user = JSON.parse(localStorage.getItem('loggedInUser'));
if (!user) return;
// Process results for spaced repetition algorithm
const processedResults = resultData.map(result => ({
word: result.word,
isCorrect: result.attempt.toLowerCase() === result.word.toLowerCase(),
timestamp: new Date().toISOString()
}));
// Send to server
const response = await fetch('/updateSpacedRepetition', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: user.username,
results: processedResults
})
});
if (response.ok) {
console.log('Spaced repetition data updated successfully');
} else {
console.error('Failed to update spaced repetition data');
}
} catch (error) {
console.error('Error updating spaced repetition data:', error);
}
}