✨ Major code cleanup and optimization
- Removed unnecessary dependencies (fs, path - built-in modules) - Organized debug files into /debug folder - Reduced excessive console logging for cleaner output - Fixed README formatting issues - Added comprehensive .gitignore - Removed redundant backup files (game.js.new) - Fixed duplicate code patterns in analytics - Created CLEANUP_SUMMARY.md documentation - Regenerated clean package-lock.json No breaking changes - all functionality preserved
This commit is contained in:
1 parent
8d37038177
commit
d860df7de6
59 files changed
+1372
-5916
No files matched your search
@@ -3,9 +3,9 @@
|
||||
/* Main grid layout for analytics dashboard */
|
||||
.analytics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(250px, 100vw), 1fr));
|
||||
gap: clamp(0.75rem, 2vw, 1.5rem);
|
||||
margin-top: clamp(0.75rem, 2vw, 1.5rem);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -3,8 +3,9 @@
|
||||
/* Badge Celebration - User Game View */
|
||||
.badge-celebration {
|
||||
text-align: center;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
background-color: var(--bg-primary);
|
||||
background-image: linear-gradient(135deg, rgba(255,255,255,0.05) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.05) 50%, rgba(255,255,255,0.05) 75%, transparent 75%);
|
||||
|
||||
@@ -249,9 +249,9 @@
|
||||
/* User profile stats */
|
||||
.user-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(200px, 100vw), 1fr));
|
||||
gap: clamp(0.5rem, 2vw, 1.5rem);
|
||||
margin-bottom: clamp(1rem, 2vw, 1.5rem);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
|
||||
+19
-3
@@ -55,6 +55,7 @@
|
||||
<script src="js/unified-write.js"></script>
|
||||
<script src="js/tablet-menu.js"></script>
|
||||
<script src="js/tablet-utils.js"></script>
|
||||
<script src="js/draggable-cards.js"></script>
|
||||
<script src="js/main.js"></script>
|
||||
<!-- Initialization script - loads last -->
|
||||
<script src="js/chart-safety.js"></script>
|
||||
@@ -203,8 +204,15 @@
|
||||
<!-- Admin Dashboard Overview -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>👨💼 Admin Dashboard</h2>
|
||||
<p style="color: var(--text-secondary);">System overview and quick access to admin functions</p>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div>
|
||||
<h2>👨💼 Admin Dashboard</h2>
|
||||
<p style="color: var(--text-secondary); margin: 0;">System overview and quick access to admin functions</p>
|
||||
</div>
|
||||
<button id="refreshDashboardBtn" class="btn btn-secondary" onclick="loadAdminDashboardStats()" title="Refresh statistics">
|
||||
🔄 Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats Overview -->
|
||||
@@ -239,6 +247,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last Updated Timestamp -->
|
||||
<div style="text-align: center; margin-top: 1rem; opacity: 0.7;">
|
||||
<small id="lastUpdated">Last updated: Loading...</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help Bubble for Admin Functions -->
|
||||
@@ -361,7 +374,10 @@
|
||||
<h4>Current Word List</h4>
|
||||
<ul id="wordListDisplay" style="max-height: 200px; overflow-y: auto;"></ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- View by Student - Separate section -->
|
||||
<div class="admin-grid" style="margin-top: 2rem;">
|
||||
<div class="card">
|
||||
<h4>View by Student</h4>
|
||||
<select class="word-user-dropdown" id="viewUserSelect" onchange="displayWordListForSelectedUser()" style="width: 100%;">
|
||||
|
||||
+5
-28
@@ -29,7 +29,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// Main function to refresh analytics
|
||||
async function analyticsRefresh() {
|
||||
// This is the main implementation
|
||||
console.log('📊 Refreshing analytics data...');
|
||||
|
||||
try {
|
||||
// Update UI to show loading state
|
||||
updateLoadingState(true);
|
||||
@@ -51,7 +51,7 @@ async function analyticsRefresh() {
|
||||
// Update UI to show data is loaded
|
||||
updateLoadingState(false);
|
||||
|
||||
console.log('📊 Analytics data loaded successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error refreshing analytics:', error);
|
||||
updateLoadingState(false);
|
||||
@@ -92,19 +92,15 @@ function updateLoadingState(isLoading) {
|
||||
|
||||
// Populate user dropdown with available users
|
||||
async function populateUserDropdown() {
|
||||
console.log('🔄 Populating analytics user dropdown...');
|
||||
const userSelect = document.getElementById('analyticsUserSelect');
|
||||
if (!userSelect) {
|
||||
console.error('⚠️ Analytics user select dropdown not found in the DOM!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we can use the shared admin users data
|
||||
if (typeof window.adminUsers !== 'undefined' && Array.isArray(window.adminUsers) && window.adminUsers.length > 0) {
|
||||
console.log('✅ Using shared admin users data:', window.adminUsers.map(u => u.username));
|
||||
|
||||
// Clear existing options except the first one (All Users)
|
||||
console.log('Clearing existing options...');
|
||||
while (userSelect.options.length > 1) {
|
||||
userSelect.remove(1);
|
||||
}
|
||||
@@ -117,21 +113,16 @@ async function populateUserDropdown() {
|
||||
userSelect.appendChild(option);
|
||||
});
|
||||
|
||||
console.log(`✅ Added ${window.adminUsers.length} users to analytics dropdown`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we can't use shared data, fetch from server
|
||||
try {
|
||||
// Clear existing options except the first one (All Users)
|
||||
console.log('Clearing existing options...');
|
||||
while (userSelect.options.length > 1) {
|
||||
userSelect.remove(1);
|
||||
}
|
||||
|
||||
// Always fetch fresh users from server
|
||||
console.log("📥 Fetching users directly from server...");
|
||||
|
||||
// Add a loading option
|
||||
const loadingOption = document.createElement('option');
|
||||
loadingOption.value = "";
|
||||
@@ -140,7 +131,6 @@ async function populateUserDropdown() {
|
||||
userSelect.appendChild(loadingOption);
|
||||
|
||||
const response = await fetch('/getUsers');
|
||||
console.log("GET /getUsers response:", response.status, response.statusText);
|
||||
|
||||
// Remove loading option
|
||||
if (userSelect.contains(loadingOption)) {
|
||||
@@ -152,20 +142,15 @@ async function populateUserDropdown() {
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
console.log("Raw server response:", responseText.substring(0, 100) + (responseText.length > 100 ? '...' : ''));
|
||||
|
||||
let users = [];
|
||||
try {
|
||||
const allUsers = JSON.parse(responseText);
|
||||
console.log("Parsed users data:", allUsers);
|
||||
|
||||
if (Array.isArray(allUsers)) {
|
||||
users = allUsers.map(user => user.username);
|
||||
console.log(`✅ Loaded ${users.length} users from server:`, users);
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn("⚠️ Server returned empty users array!");
|
||||
|
||||
// Add a warning option
|
||||
const warningOption = document.createElement('option');
|
||||
warningOption.value = "";
|
||||
@@ -174,32 +159,27 @@ async function populateUserDropdown() {
|
||||
userSelect.appendChild(warningOption);
|
||||
}
|
||||
} else {
|
||||
console.error("❌ Server returned non-array data:", allUsers);
|
||||
throw new Error("Invalid user data format");
|
||||
}
|
||||
} catch (jsonError) {
|
||||
console.error("❌ Error parsing JSON response:", jsonError);
|
||||
throw jsonError;
|
||||
}
|
||||
|
||||
// Add users to dropdown
|
||||
console.log(`Adding ${users.length} users to dropdown`);
|
||||
users.forEach(username => {
|
||||
const option = document.createElement('option');
|
||||
option.value = username;
|
||||
option.textContent = username;
|
||||
userSelect.appendChild(option);
|
||||
});
|
||||
console.log('✅ User dropdown populated successfully');
|
||||
} catch (error) {
|
||||
console.error("❌ Error populating user dropdown:", error);
|
||||
console.error("Error populating user dropdown:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Render the entire analytics dashboard
|
||||
function renderAnalyticsDashboard() {
|
||||
if (!analyticsData) {
|
||||
console.warn('No analytics data available to render');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -443,7 +423,6 @@ function renderProgressChart(data) {
|
||||
|
||||
// Skip invalid coordinates
|
||||
if (isNaN(x) || isNaN(y)) {
|
||||
console.warn('Invalid coordinate for progress chart:', { x, y, point, i });
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -473,10 +452,8 @@ function renderProgressChart(data) {
|
||||
chartGroup.appendChild(areaPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating area path:', error);
|
||||
// Silently handle area path creation errors
|
||||
}
|
||||
} else {
|
||||
console.warn('Unable to create valid path data for progress chart');
|
||||
}
|
||||
|
||||
// Add data points
|
||||
@@ -763,7 +740,7 @@ function renderProblemWordsList(data) {
|
||||
|
||||
const word = document.createElement('div');
|
||||
word.className = 'problem-word';
|
||||
word.textContent = wordItem.word || wordItem.word;
|
||||
word.textContent = wordItem.word;
|
||||
|
||||
const count = document.createElement('div');
|
||||
count.className = 'problem-count';
|
||||
|
||||
@@ -0,0 +1,678 @@
|
||||
/**
|
||||
* Draggable & Resizable Cards System
|
||||
* Allows admin cards to be dragged, dropped, and resized within the admin panel
|
||||
*/
|
||||
|
||||
class DraggableCardManager {
|
||||
constructor() {
|
||||
this.cards = [];
|
||||
this.draggedCard = null;
|
||||
this.startPos = { x: 0, y: 0 };
|
||||
this.startSize = { width: 0, height: 0 };
|
||||
this.isResizing = false;
|
||||
this.isDragging = false;
|
||||
this.gridContainer = null;
|
||||
this.savedLayouts = this.loadSavedLayouts();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => this.setupDraggableCards());
|
||||
} else {
|
||||
this.setupDraggableCards();
|
||||
}
|
||||
|
||||
console.log('🎯 Draggable Cards Manager initialized');
|
||||
}
|
||||
|
||||
setupDraggableCards() {
|
||||
// Find all potential containers with cards
|
||||
const containers = [
|
||||
document.querySelector('.admin-grid'),
|
||||
document.querySelector('.container'),
|
||||
document.body
|
||||
];
|
||||
|
||||
// Use the first container found, or body as fallback
|
||||
this.gridContainer = containers.find(container => container && container.querySelector('.card')) || document.body;
|
||||
|
||||
if (!this.gridContainer) {
|
||||
console.log('No container with cards found, retrying in 1 second...');
|
||||
setTimeout(() => this.setupDraggableCards(), 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make the container suitable for absolute positioning
|
||||
if (this.gridContainer.style.position !== 'relative' && this.gridContainer.style.position !== 'absolute') {
|
||||
this.gridContainer.style.position = 'relative';
|
||||
}
|
||||
this.gridContainer.style.minHeight = '400px';
|
||||
|
||||
// Setup all cards found anywhere on the page
|
||||
this.setupAllCards();
|
||||
|
||||
// Load saved layout
|
||||
this.applySavedLayout();
|
||||
|
||||
// Add control buttons
|
||||
this.addControlButtons();
|
||||
|
||||
// Watch for new cards being added dynamically
|
||||
this.setupCardObserver();
|
||||
|
||||
console.log('📋 Draggable cards setup complete for all pages');
|
||||
}
|
||||
|
||||
setupAllCards() {
|
||||
// Find all cards on the entire page, not just in one container
|
||||
const allCards = document.querySelectorAll('.card');
|
||||
|
||||
allCards.forEach((card, index) => {
|
||||
// Skip if already setup
|
||||
if (card.dataset.draggableSetup === 'true') return;
|
||||
|
||||
this.makeCardDraggable(card, index);
|
||||
this.makeCardResizable(card, index);
|
||||
this.addCardHeader(card, index);
|
||||
|
||||
card.dataset.draggableSetup = 'true';
|
||||
});
|
||||
|
||||
console.log(`🎯 Setup ${allCards.length} draggable cards`);
|
||||
}
|
||||
|
||||
setupCardObserver() {
|
||||
// Watch for new cards being added to the page
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Check if the added node is a card or contains cards
|
||||
const cards = node.classList && node.classList.contains('card')
|
||||
? [node]
|
||||
: node.querySelectorAll ? node.querySelectorAll('.card') : [];
|
||||
|
||||
cards.forEach((card, index) => {
|
||||
if (card.dataset.draggableSetup !== 'true') {
|
||||
const allCards = document.querySelectorAll('.card');
|
||||
const cardIndex = Array.from(allCards).indexOf(card);
|
||||
this.makeCardDraggable(card, cardIndex);
|
||||
this.makeCardResizable(card, cardIndex);
|
||||
this.addCardHeader(card, cardIndex);
|
||||
card.dataset.draggableSetup = 'true';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
this.cardObserver = observer;
|
||||
}
|
||||
|
||||
autoArrangeCards() {
|
||||
const cards = this.freeformContainer.querySelectorAll('.card');
|
||||
const containerWidth = this.freeformContainer.clientWidth - 40; // Account for padding
|
||||
const cardWidth = 350; // Standard width for side-by-side
|
||||
const cardHeight = 300; // Standard height
|
||||
const gap = 20; // Gap between cards
|
||||
|
||||
const cardsPerRow = Math.floor(containerWidth / (cardWidth + gap));
|
||||
|
||||
cards.forEach((card, index) => {
|
||||
const row = Math.floor(index / cardsPerRow);
|
||||
const col = index % cardsPerRow;
|
||||
|
||||
const x = 20 + (col * (cardWidth + gap));
|
||||
const y = 20 + (row * (cardHeight + gap));
|
||||
|
||||
card.style.left = x + 'px';
|
||||
card.style.top = y + 'px';
|
||||
card.style.width = cardWidth + 'px';
|
||||
});
|
||||
|
||||
console.log(`📐 Auto-arranged ${cards.length} cards in ${cardsPerRow} columns`);
|
||||
}
|
||||
|
||||
setupCards() {
|
||||
const cards = this.gridContainer.querySelectorAll('.card');
|
||||
|
||||
cards.forEach((card, index) => {
|
||||
this.makeCardDraggable(card, index);
|
||||
this.makeCardResizable(card, index);
|
||||
this.addCardHeader(card, index);
|
||||
});
|
||||
}
|
||||
|
||||
makeCardDraggable(card, index) {
|
||||
// Add draggable handle
|
||||
const header = card.querySelector('.card-header') || this.createCardHeader(card);
|
||||
|
||||
header.style.cursor = 'move';
|
||||
header.addEventListener('mousedown', (e) => this.startDrag(e, card, index));
|
||||
header.addEventListener('touchstart', (e) => this.startDrag(e, card, index));
|
||||
|
||||
// Prevent text selection while dragging
|
||||
header.style.userSelect = 'none';
|
||||
header.style.webkitUserSelect = 'none';
|
||||
}
|
||||
|
||||
makeCardResizable(card, index) {
|
||||
// Add resize handles
|
||||
const resizeHandle = document.createElement('div');
|
||||
resizeHandle.className = 'resize-handle';
|
||||
resizeHandle.innerHTML = '⟲';
|
||||
resizeHandle.style.cssText = `
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: se-resize;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
border-radius: 4px 0 8px 0;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
`;
|
||||
|
||||
resizeHandle.addEventListener('mousedown', (e) => this.startResize(e, card, index));
|
||||
resizeHandle.addEventListener('touchstart', (e) => this.startResize(e, card, index));
|
||||
|
||||
card.style.position = 'relative';
|
||||
card.appendChild(resizeHandle);
|
||||
|
||||
// Show/hide resize handle on hover
|
||||
card.addEventListener('mouseenter', () => resizeHandle.style.opacity = '1');
|
||||
card.addEventListener('mouseleave', () => resizeHandle.style.opacity = '0.7');
|
||||
}
|
||||
|
||||
createCardHeader(card) {
|
||||
const header = document.createElement('div');
|
||||
header.className = 'card-header';
|
||||
header.style.cssText = `
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: -2rem -2rem 1rem -2rem;
|
||||
border-radius: 8px 8px 0 0;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.3s ease;
|
||||
`;
|
||||
|
||||
const title = card.querySelector('h4');
|
||||
const titleText = title ? title.textContent : `Card ${card.dataset.cardIndex || ''}`;
|
||||
header.innerHTML = `
|
||||
<span>${titleText}</span>
|
||||
<span class="drag-indicator" style="opacity: 0.7;">⋮⋮</span>
|
||||
`;
|
||||
|
||||
card.insertBefore(header, card.firstChild);
|
||||
if (title) title.style.display = 'none'; // Hide original title
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
addCardHeader(card, index) {
|
||||
card.dataset.cardIndex = index;
|
||||
|
||||
if (!card.querySelector('.card-header')) {
|
||||
this.createCardHeader(card);
|
||||
}
|
||||
}
|
||||
|
||||
startDrag(e, card, index) {
|
||||
// Only allow dragging if global drag mode is enabled
|
||||
if (!document.body.classList.contains('global-drag-mode')) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
this.isDragging = true;
|
||||
this.draggedCard = card;
|
||||
|
||||
const clientX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = e.type === 'touchstart' ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
const rect = card.getBoundingClientRect();
|
||||
this.startPos = {
|
||||
x: clientX - rect.left,
|
||||
y: clientY - rect.top
|
||||
};
|
||||
|
||||
// Switch to absolute positioning
|
||||
const parentRect = card.offsetParent ? card.offsetParent.getBoundingClientRect() : { left: 0, top: 0 };
|
||||
card.style.position = 'absolute';
|
||||
card.style.left = rect.left - parentRect.left + 'px';
|
||||
card.style.top = rect.top - parentRect.top + 'px';
|
||||
card.style.width = rect.width + 'px';
|
||||
card.style.zIndex = '1000';
|
||||
card.style.transform = 'rotate(2deg)';
|
||||
card.style.boxShadow = '0 8px 32px rgba(0,0,0,0.3)';
|
||||
|
||||
// Add event listeners
|
||||
document.addEventListener('mousemove', this.handleDrag);
|
||||
document.addEventListener('touchmove', this.handleDrag);
|
||||
document.addEventListener('mouseup', this.stopDrag);
|
||||
document.addEventListener('touchend', this.stopDrag);
|
||||
|
||||
card.classList.add('dragging');
|
||||
}
|
||||
|
||||
handleDrag = (e) => {
|
||||
if (!this.isDragging || !this.draggedCard) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
const containerRect = this.freeformContainer ? this.freeformContainer.getBoundingClientRect() :
|
||||
(this.draggedCard.offsetParent ? this.draggedCard.offsetParent.getBoundingClientRect() : { left: 0, top: 0 });
|
||||
const newX = clientX - containerRect.left - this.startPos.x;
|
||||
const newY = clientY - containerRect.top - this.startPos.y;
|
||||
|
||||
// Keep within bounds of the container with some padding
|
||||
const container = this.freeformContainer || this.draggedCard.offsetParent || document.body;
|
||||
const maxX = container.clientWidth - this.draggedCard.offsetWidth - 10;
|
||||
const maxY = container.clientHeight - this.draggedCard.offsetHeight - 10;
|
||||
|
||||
this.draggedCard.style.left = Math.max(0, Math.min(maxX, newX)) + 'px';
|
||||
this.draggedCard.style.top = Math.max(0, Math.min(maxY, newY)) + 'px';
|
||||
}
|
||||
|
||||
stopDrag = (e) => {
|
||||
if (!this.isDragging || !this.draggedCard) return;
|
||||
|
||||
this.isDragging = false;
|
||||
|
||||
// Reset visual effects
|
||||
this.draggedCard.style.transform = '';
|
||||
this.draggedCard.style.zIndex = '';
|
||||
this.draggedCard.style.boxShadow = '';
|
||||
this.draggedCard.classList.remove('dragging');
|
||||
|
||||
// Save position
|
||||
this.saveCardLayout();
|
||||
|
||||
// Clean up event listeners
|
||||
document.removeEventListener('mousemove', this.handleDrag);
|
||||
document.removeEventListener('touchmove', this.handleDrag);
|
||||
document.removeEventListener('mouseup', this.stopDrag);
|
||||
document.removeEventListener('touchend', this.stopDrag);
|
||||
|
||||
this.draggedCard = null;
|
||||
}
|
||||
|
||||
startResize(e, card, index) {
|
||||
// Only allow resizing if global drag mode is enabled
|
||||
if (!document.body.classList.contains('global-drag-mode')) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.isResizing = true;
|
||||
this.draggedCard = card;
|
||||
|
||||
const clientX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = e.type === 'touchstart' ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
this.startPos = { x: clientX, y: clientY };
|
||||
this.startSize = {
|
||||
width: card.offsetWidth,
|
||||
height: card.offsetHeight
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', this.handleResize);
|
||||
document.addEventListener('touchmove', this.handleResize);
|
||||
document.addEventListener('mouseup', this.stopResize);
|
||||
document.addEventListener('touchend', this.stopResize);
|
||||
|
||||
card.classList.add('resizing');
|
||||
}
|
||||
|
||||
handleResize = (e) => {
|
||||
if (!this.isResizing || !this.draggedCard) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
const deltaX = clientX - this.startPos.x;
|
||||
const deltaY = clientY - this.startPos.y;
|
||||
|
||||
const newWidth = Math.max(200, this.startSize.width + deltaX);
|
||||
const newHeight = Math.max(150, this.startSize.height + deltaY);
|
||||
|
||||
this.draggedCard.style.width = newWidth + 'px';
|
||||
this.draggedCard.style.height = newHeight + 'px';
|
||||
}
|
||||
|
||||
stopResize = (e) => {
|
||||
if (!this.isResizing) return;
|
||||
|
||||
this.isResizing = false;
|
||||
this.draggedCard.classList.remove('resizing');
|
||||
|
||||
// Save size
|
||||
this.saveCardLayout();
|
||||
|
||||
// Clean up event listeners
|
||||
document.removeEventListener('mousemove', this.handleResize);
|
||||
document.removeEventListener('touchmove', this.handleResize);
|
||||
document.removeEventListener('mouseup', this.stopResize);
|
||||
document.removeEventListener('touchend', this.stopResize);
|
||||
|
||||
this.draggedCard = null;
|
||||
}
|
||||
|
||||
saveCardLayout() {
|
||||
const layout = {};
|
||||
const cards = this.gridContainer.querySelectorAll('.card');
|
||||
|
||||
cards.forEach((card, index) => {
|
||||
layout[index] = {
|
||||
left: card.style.left,
|
||||
top: card.style.top,
|
||||
width: card.style.width,
|
||||
height: card.style.height,
|
||||
position: card.style.position
|
||||
};
|
||||
});
|
||||
|
||||
localStorage.setItem('adminCardLayout', JSON.stringify(layout));
|
||||
console.log('💾 Card layout saved');
|
||||
}
|
||||
|
||||
loadSavedLayouts() {
|
||||
try {
|
||||
const saved = localStorage.getItem('adminCardLayout');
|
||||
return saved ? JSON.parse(saved) : {};
|
||||
} catch (e) {
|
||||
console.warn('Could not load saved layouts:', e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
applySavedLayout() {
|
||||
const cards = this.gridContainer.querySelectorAll('.card');
|
||||
|
||||
cards.forEach((card, index) => {
|
||||
const savedLayout = this.savedLayouts[index];
|
||||
if (savedLayout) {
|
||||
Object.assign(card.style, savedLayout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addControlButtons() {
|
||||
// Check if controls already exist
|
||||
if (document.querySelector('.global-card-controls')) return;
|
||||
|
||||
// Check if hamburger menu exists and adjust position accordingly
|
||||
const menuToggle = document.querySelector('.menu-toggle');
|
||||
const topPosition = menuToggle ? '80px' : '20px';
|
||||
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.className = 'global-card-controls card-controls';
|
||||
controlsDiv.style.cssText = `
|
||||
position: fixed;
|
||||
top: ${topPosition};
|
||||
right: 20px;
|
||||
z-index: 900;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
background: rgba(255,255,255,0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
|
||||
border: 1px solid var(--border);
|
||||
max-width: 300px;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const toggleBtn = document.createElement('button');
|
||||
toggleBtn.className = 'btn btn-primary';
|
||||
toggleBtn.innerHTML = '🎯 Enable Drag Mode';
|
||||
toggleBtn.style.fontSize = '0.9rem';
|
||||
toggleBtn.onclick = () => this.toggleDragMode(toggleBtn);
|
||||
|
||||
const resetBtn = document.createElement('button');
|
||||
resetBtn.className = 'btn btn-secondary';
|
||||
resetBtn.innerHTML = '🔄 Reset';
|
||||
resetBtn.style.fontSize = '0.9rem';
|
||||
resetBtn.onclick = () => this.resetLayout();
|
||||
|
||||
const saveBtn = document.createElement('button');
|
||||
saveBtn.className = 'btn btn-success';
|
||||
saveBtn.innerHTML = '💾 Save';
|
||||
saveBtn.style.fontSize = '0.9rem';
|
||||
saveBtn.onclick = () => this.saveCardLayout();
|
||||
|
||||
const arrangeBtn = document.createElement('button');
|
||||
arrangeBtn.className = 'btn btn-info';
|
||||
arrangeBtn.innerHTML = '📐 Auto-Arrange';
|
||||
arrangeBtn.style.fontSize = '0.9rem';
|
||||
arrangeBtn.onclick = () => {
|
||||
if (document.body.classList.contains('global-drag-mode') && this.freeformContainer) {
|
||||
this.autoArrangeCards();
|
||||
}
|
||||
};
|
||||
|
||||
const minimizeBtn = document.createElement('button');
|
||||
minimizeBtn.className = 'btn btn-secondary';
|
||||
minimizeBtn.innerHTML = '−';
|
||||
minimizeBtn.style.fontSize = '1.2rem';
|
||||
minimizeBtn.style.width = '32px';
|
||||
minimizeBtn.style.padding = '0.25rem';
|
||||
minimizeBtn.title = 'Minimize controls';
|
||||
minimizeBtn.onclick = () => this.toggleControls(controlsDiv, minimizeBtn);
|
||||
|
||||
controlsDiv.appendChild(toggleBtn);
|
||||
controlsDiv.appendChild(resetBtn);
|
||||
controlsDiv.appendChild(saveBtn);
|
||||
controlsDiv.appendChild(arrangeBtn);
|
||||
controlsDiv.appendChild(minimizeBtn);
|
||||
|
||||
document.body.appendChild(controlsDiv);
|
||||
|
||||
this.controlsDiv = controlsDiv;
|
||||
this.toggleBtn = toggleBtn;
|
||||
|
||||
console.log('🎛️ Global card controls added');
|
||||
}
|
||||
|
||||
toggleControls(controlsDiv, minimizeBtn) {
|
||||
const buttons = controlsDiv.querySelectorAll('.btn:not(:last-child)');
|
||||
const isMinimized = controlsDiv.dataset.minimized === 'true';
|
||||
|
||||
if (isMinimized) {
|
||||
// Expand
|
||||
buttons.forEach(btn => btn.style.display = 'inline-flex');
|
||||
minimizeBtn.innerHTML = '−';
|
||||
minimizeBtn.title = 'Minimize controls';
|
||||
controlsDiv.dataset.minimized = 'false';
|
||||
} else {
|
||||
// Minimize
|
||||
buttons.forEach(btn => btn.style.display = 'none');
|
||||
minimizeBtn.innerHTML = '⚙️';
|
||||
minimizeBtn.title = 'Expand controls';
|
||||
controlsDiv.dataset.minimized = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
toggleDragMode(button) {
|
||||
const isCurrentlyEnabled = document.body.classList.contains('global-drag-mode');
|
||||
|
||||
if (isCurrentlyEnabled) {
|
||||
// Disable drag mode
|
||||
document.body.classList.remove('global-drag-mode');
|
||||
button.innerHTML = '🎯 Enable Drag Mode';
|
||||
button.className = 'btn btn-primary';
|
||||
|
||||
// Return cards to their original containers if possible
|
||||
const cards = document.querySelectorAll('.card');
|
||||
cards.forEach(card => {
|
||||
// Reset positioning
|
||||
card.style.position = '';
|
||||
card.style.left = '';
|
||||
card.style.top = '';
|
||||
card.style.zIndex = '';
|
||||
card.style.width = '';
|
||||
card.style.minWidth = '';
|
||||
|
||||
// Move back to original container if it was in freeform container
|
||||
if (card.parentNode === this.freeformContainer) {
|
||||
const originalContainer = document.querySelector('.admin-grid') ||
|
||||
document.querySelector('.container') ||
|
||||
document.body;
|
||||
originalContainer.appendChild(card);
|
||||
}
|
||||
});
|
||||
|
||||
// Hide freeform container
|
||||
if (this.freeformContainer) {
|
||||
this.freeformContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// Remove drag mode indicator
|
||||
const indicator = document.querySelector('.global-drag-indicator');
|
||||
if (indicator) indicator.remove();
|
||||
|
||||
console.log('🔒 Drag mode disabled - cards returned to normal layout');
|
||||
} else {
|
||||
// Enable drag mode
|
||||
document.body.classList.add('global-drag-mode');
|
||||
button.innerHTML = '🔒 Disable Drag Mode';
|
||||
button.className = 'btn btn-warning';
|
||||
|
||||
// Add drag mode indicator
|
||||
this.addDragModeIndicator();
|
||||
|
||||
// Create and show freeform container
|
||||
this.ensureFreeformContainer();
|
||||
this.freeformContainer.style.display = 'block';
|
||||
|
||||
// Convert all cards to absolute positioning with flexible layout
|
||||
const cards = document.querySelectorAll('.card');
|
||||
|
||||
cards.forEach((card, index) => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
|
||||
// Move card to freeform container if not already there
|
||||
if (card.parentNode !== this.freeformContainer) {
|
||||
this.freeformContainer.appendChild(card);
|
||||
}
|
||||
|
||||
const containerRect = this.freeformContainer.getBoundingClientRect();
|
||||
|
||||
card.style.position = 'absolute';
|
||||
card.style.left = Math.max(10, (rect.left - containerRect.left)) + 'px';
|
||||
card.style.top = Math.max(10, (rect.top - containerRect.top)) + 'px';
|
||||
card.style.width = Math.min(400, Math.max(250, rect.width)) + 'px'; // Flexible width
|
||||
card.style.zIndex = '10';
|
||||
card.style.minWidth = '250px';
|
||||
card.style.maxWidth = '500px'; // Allow for side-by-side placement
|
||||
});
|
||||
|
||||
// Auto-arrange cards in a grid-like pattern initially
|
||||
this.autoArrangeCards();
|
||||
|
||||
console.log('🎯 Free-form drag mode enabled - cards can be placed side-by-side');
|
||||
}
|
||||
}
|
||||
|
||||
ensureFreeformContainer() {
|
||||
// Create or get the freeform positioning container
|
||||
if (!this.freeformContainer || !document.body.contains(this.freeformContainer)) {
|
||||
this.freeformContainer = document.createElement('div');
|
||||
this.freeformContainer.className = 'freeform-card-container';
|
||||
this.freeformContainer.style.cssText = `
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
// Find the main container to replace or append to
|
||||
const mainContainer = document.querySelector('.container') || document.body;
|
||||
if (mainContainer === document.body) {
|
||||
document.body.appendChild(this.freeformContainer);
|
||||
} else {
|
||||
mainContainer.parentNode.insertBefore(this.freeformContainer, mainContainer.nextSibling);
|
||||
}
|
||||
}
|
||||
return this.freeformContainer;
|
||||
}
|
||||
|
||||
addDragModeIndicator() {
|
||||
// Remove existing indicator
|
||||
const existing = document.querySelector('.global-drag-indicator');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'global-drag-indicator';
|
||||
indicator.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
z-index: 899;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
`;
|
||||
indicator.innerHTML = '🎯 Free-Form Layout Active - Drag cards anywhere, place them side-by-side, resize as needed';
|
||||
|
||||
document.body.appendChild(indicator);
|
||||
}
|
||||
|
||||
resetLayout() {
|
||||
const cards = this.gridContainer.querySelectorAll('.card');
|
||||
|
||||
cards.forEach((card) => {
|
||||
card.style.position = '';
|
||||
card.style.left = '';
|
||||
card.style.top = '';
|
||||
card.style.width = '';
|
||||
card.style.height = '';
|
||||
});
|
||||
|
||||
localStorage.removeItem('adminCardLayout');
|
||||
console.log('🔄 Layout reset');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.draggableCardManager = new DraggableCardManager();
|
||||
});
|
||||
} else {
|
||||
window.draggableCardManager = new DraggableCardManager();
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
// 🌍 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() {
|
||||
const input = document.getElementById('userInput').value.trim();
|
||||
|
||||
// Record the attempt
|
||||
results.push({
|
||||
word: currentWord,
|
||||
attempt: input
|
||||
});
|
||||
|
||||
// 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 = '#f0fdf4';
|
||||
|
||||
// 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 = '#fef2f2';
|
||||
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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 = '';
|
||||
inputSection.classList.remove('hidden');
|
||||
document.getElementById('userInput').value = '';
|
||||
document.getElementById('userInput').focus();
|
||||
}, 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;
|
||||
|
||||
const payload = {
|
||||
username: user.username,
|
||||
results: resultData,
|
||||
accuracy,
|
||||
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) return;
|
||||
|
||||
const badgeList = document.getElementById('badgeList');
|
||||
badgeList.innerHTML = '';
|
||||
|
||||
badges.forEach(badge => {
|
||||
badgeList.innerHTML += `
|
||||
<div class="badge-item">
|
||||
<div class="badge-icon">${badge.icon}</div>
|
||||
<div class="badge-info">
|
||||
<h4>${badge.name}</h4>
|
||||
<p>${badge.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -24,12 +24,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
function fixInputVisibility() {
|
||||
const userInput = document.getElementById('userInput');
|
||||
if (!userInput) {
|
||||
console.log('❌ User input element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔧 Fixing input visibility');
|
||||
|
||||
// Create a style element for stronger CSS
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.textContent = `
|
||||
|
||||
@@ -885,21 +885,111 @@ function showAdminDashboard() {
|
||||
}
|
||||
|
||||
function loadAdminDashboardStats() {
|
||||
// Update stats with real data
|
||||
// Update stats with real data from server
|
||||
try {
|
||||
// Total users
|
||||
if (window.users && Array.isArray(window.users)) {
|
||||
document.getElementById('totalUsers').textContent = window.users.length;
|
||||
// Fetch and display total users
|
||||
fetchDashboardStat('/getUsers', 'totalUsers', (data) => {
|
||||
return Array.isArray(data) ? data.length : Object.keys(data).length;
|
||||
});
|
||||
|
||||
// Fetch and display total word lists
|
||||
fetchDashboardStat('/getWordlistsRaw', 'totalWordLists', (data) => {
|
||||
return Object.keys(data).length;
|
||||
});
|
||||
|
||||
// Fetch and display total badges awarded
|
||||
fetchDashboardStat('/getBadges', 'totalBadges', (data) => {
|
||||
let totalAwarded = 0;
|
||||
if (data && typeof data === 'object') {
|
||||
Object.values(data).forEach(userBadges => {
|
||||
if (Array.isArray(userBadges)) {
|
||||
totalAwarded += userBadges.length;
|
||||
}
|
||||
});
|
||||
}
|
||||
return totalAwarded;
|
||||
});
|
||||
|
||||
// Calculate active sessions (users who have results in last 24 hours)
|
||||
fetchDashboardStat('/getResults', 'totalSessions', (data) => {
|
||||
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
|
||||
let activeSessions = 0;
|
||||
if (data && Array.isArray(data)) {
|
||||
const recentUsers = new Set();
|
||||
data.forEach(result => {
|
||||
if (result.timestamp && new Date(result.timestamp).getTime() > oneDayAgo) {
|
||||
recentUsers.add(result.username);
|
||||
}
|
||||
});
|
||||
activeSessions = recentUsers.size;
|
||||
}
|
||||
return activeSessions;
|
||||
});
|
||||
|
||||
// Update last updated timestamp
|
||||
updateLastUpdatedTime();
|
||||
|
||||
// Set up auto-refresh every 30 seconds
|
||||
if (!window.dashboardStatsInterval) {
|
||||
window.dashboardStatsInterval = setInterval(() => {
|
||||
loadAdminDashboardStats();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
// You can add more stats here as needed
|
||||
document.getElementById('totalWordLists').textContent = '5+';
|
||||
document.getElementById('totalSessions').textContent = '0';
|
||||
document.getElementById('totalBadges').textContent = '8';
|
||||
} catch (error) {
|
||||
console.warn('Could not load dashboard stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the last updated timestamp
|
||||
function updateLastUpdatedTime() {
|
||||
const element = document.getElementById('lastUpdated');
|
||||
if (element) {
|
||||
const now = new Date();
|
||||
element.textContent = `Last updated: ${now.toLocaleTimeString()}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to fetch and update dashboard statistics
|
||||
function fetchDashboardStat(endpoint, elementId, processor) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (!element) return;
|
||||
|
||||
// Show loading state
|
||||
const originalText = element.textContent;
|
||||
element.style.opacity = '0.6';
|
||||
element.style.transform = 'scale(0.95)';
|
||||
|
||||
fetch(endpoint)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const value = processor(data);
|
||||
setTimeout(() => {
|
||||
element.textContent = value;
|
||||
element.style.opacity = '1';
|
||||
element.style.transform = 'scale(1.05)';
|
||||
|
||||
// Add success pulse animation
|
||||
setTimeout(() => {
|
||||
element.style.transform = 'scale(1)';
|
||||
element.style.color = 'var(--success)';
|
||||
setTimeout(() => {
|
||||
element.style.color = 'var(--text-primary)';
|
||||
}, 500);
|
||||
}, 150);
|
||||
}, 200);
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn(`Failed to fetch ${endpoint}:`, error);
|
||||
element.textContent = originalText === '-' ? '!' : originalText;
|
||||
element.style.opacity = '0.7';
|
||||
element.style.transform = 'scale(1)';
|
||||
element.style.color = 'var(--error)';
|
||||
setTimeout(() => {
|
||||
element.style.color = 'var(--text-primary)';
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
window.TabletMenuManager = TabletMenuManager;
|
||||
+98
-29
@@ -34,7 +34,8 @@ body {
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
padding: 2rem 1rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
touch-action: manipulation;
|
||||
@@ -42,17 +43,50 @@ body {
|
||||
|
||||
/* Container and layout */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
padding: clamp(0.5rem, 1vw, 1rem);
|
||||
}
|
||||
|
||||
/* Responsive layout utilities */
|
||||
@media (max-width: 768px) {
|
||||
.overview-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 1rem !important;
|
||||
padding: 1.5rem !important;
|
||||
aspect-ratio: auto !important;
|
||||
min-height: 120px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 1rem 0 !important;
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.overview-grid {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1 {
|
||||
font-size: clamp(2.5rem, 6vw, 4rem);
|
||||
font-size: clamp(2rem, 6vw, 4rem);
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
margin-bottom: clamp(2rem, 4vw, 3rem);
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
@@ -60,33 +94,33 @@ h1 {
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.875rem;
|
||||
font-size: clamp(1.25rem, 4vw, 2rem);
|
||||
font-weight: 600;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: clamp(1.5rem, 3vw, 2.5rem);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
font-size: clamp(1rem, 3vw, 1.5rem);
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: clamp(1rem, 2vw, 1.5rem);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
input, textarea, select {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0.75rem;
|
||||
max-width: min(400px, 90vw);
|
||||
padding: clamp(0.75rem, 2vw, 1.25rem) clamp(0.875rem, 2.5vw, 1.5rem);
|
||||
margin: clamp(0.5rem, 1.5vw, 1rem);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 1.125rem;
|
||||
border-radius: clamp(6px, 1.5vw, 12px);
|
||||
font-size: clamp(1rem, 2.5vw, 1.25rem);
|
||||
font-family: inherit;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: var(--transition);
|
||||
min-height: 48px; /* Tablet-friendly touch target */
|
||||
min-height: clamp(44px, 8vw, 60px); /* Responsive touch target */
|
||||
touch-action: manipulation; /* Prevent zoom on focus */
|
||||
}
|
||||
|
||||
@@ -126,14 +160,14 @@ button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 2rem;
|
||||
margin: 0.5rem;
|
||||
gap: clamp(0.25rem, 1vw, 0.75rem);
|
||||
padding: clamp(0.75rem, 2vw, 1.25rem) clamp(1rem, 3vw, 2.5rem);
|
||||
margin: clamp(0.75rem, 2vw, 1.25rem);
|
||||
background: var(--primary);
|
||||
color: #ffffff !important;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 1.125rem;
|
||||
border-radius: clamp(8px, 2vw, 16px);
|
||||
font-size: clamp(0.875rem, 2.5vw, 1.25rem);
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
@@ -141,8 +175,8 @@ button {
|
||||
box-shadow: var(--shadow);
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
min-height: 50px;
|
||||
min-width: 120px;
|
||||
min-height: clamp(44px, 8vw, 60px);
|
||||
min-width: clamp(100px, 20vw, 150px);
|
||||
touch-action: manipulation;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -386,11 +420,23 @@ button:disabled {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
/* Admin panel - Full width */
|
||||
#adminPanel {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
#adminPanel .card {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
#loginPanel form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
gap: 2rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Banner */
|
||||
@@ -415,8 +461,8 @@ button:disabled {
|
||||
/* Navigation */
|
||||
#nav {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: clamp(1.5rem, 3vw, 2.5rem);
|
||||
padding: clamp(1rem, 2vw, 1.5rem);
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
@@ -426,9 +472,9 @@ button:disabled {
|
||||
#adminTabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 0.75rem;
|
||||
gap: clamp(0.75rem, 2vw, 1rem);
|
||||
margin-bottom: clamp(1.5rem, 3vw, 2.5rem);
|
||||
padding: clamp(1rem, 2vw, 1.5rem);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
@@ -460,7 +506,19 @@ button:disabled {
|
||||
.adminTab {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem;
|
||||
padding: 2.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Cards - Full width layout */
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 2.5rem 0;
|
||||
padding: 3rem;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
@@ -474,6 +532,17 @@ button:disabled {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Global section spacing for better breathing room */
|
||||
div[id*="Section"],
|
||||
div[id*="Panel"],
|
||||
div[id*="Tab"],
|
||||
#gameArea,
|
||||
#spellingGame,
|
||||
#results {
|
||||
margin: 2.5rem 0;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Game elements */
|
||||
#wordBox, .word-box, #typingPrompt {
|
||||
font-size: clamp(2.5rem, 8vw, 4rem);
|
||||
|
||||
+300
-1267
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -20,8 +20,8 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
padding: 0 var(--tablet-padding);
|
||||
max-width: 98vw;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
/* Enhanced typography for tablet viewing */
|
||||
|
||||
Reference in new issue
Block a user