Merge pull request 'typing' (#4) from typing into main
Reviewed-on: nmemmert/Spelling#4
This commit is contained in:
commit
e0badaf366
19 files changed
+2663
-488
No files matched your search
@@ -1,109 +1,359 @@
|
|||||||
# 📝 Spelling App
|
# 📝 Modern Spelling & Typing Practice App
|
||||||
|
|
||||||
A lightweight, multi-user spelling practice app with admin controls, customizable themes, and per-user word list management. Built with **Node.js**, **Express**, and **Docker**.
|
A comprehensive educational application featuring spelling practice, typing exercises, and Bible verse typing with mode## ⚙️ Features Overview
|
||||||
---
|
|
||||||
## [Watch the video](https://www.youtube.com/watch?v=K29zCFZPEjE)
|
### 🎓 St### 👨💼 Admin Panel Featuresde---
|
||||||
[](https://www.youtube.com/watch?v=K29zCFZPEjE)
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
## 🛡️ Security Features## Environment VariablestivitiesUI design. Built with **Node.js**, **Express**, and optimized for Docker deployment.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Quick Start (Docker Compose)
|
## [Watch the Demo Video](https://www.youtube.com/watch?v=K29zCFZPEjE)
|
||||||
|
[](https://www.youtube.com/watch?v=K29zCFZPEjE)
|
||||||
|
|
||||||
Clone and run from anywhere using Docker Compose:
|
---
|
||||||
|
|
||||||
|
## 🚀 Installation Options
|
||||||
|
|
||||||
|
### Option 1: Docker (Recommended)
|
||||||
|
|
||||||
|
#### Quick Start with Docker Compose
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.necloud.us/nmemmert/Spelling.git
|
git clone https://git.necloud.us/nmemmert/Spelling.git
|
||||||
cd Spelling
|
cd Spelling
|
||||||
docker-compose up --build
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Then visit: 👉 http://localhost:3000
|
#### Using Pre-built Container
|
||||||
|
```bash
|
||||||
|
# Pull and run the latest container
|
||||||
|
docker run -d \
|
||||||
|
--name spelling-app \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-v ./data:/app/data \
|
||||||
|
--restart unless-stopped \
|
||||||
|
git.necloud.us/nmemmert/spelling:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom Docker Compose
|
||||||
|
```yaml
|
||||||
|
version: "3.9"
|
||||||
|
services:
|
||||||
|
spelling-app:
|
||||||
|
image: git.necloud.us/nmemmert/spelling:latest
|
||||||
|
container_name: spelling-app
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
```
|
||||||
|
|
||||||
## 👥 Default Login
|
### Option 2: CasaOS Installation
|
||||||
|
|
||||||
| Username | Password | Role |
|
#### Method 1: One-Click Install (Recommended)
|
||||||
|-----------|----------|--------|
|
1. **Open CasaOS Dashboard** in your web browser
|
||||||
| admin1 | password | Admin |
|
2. Navigate to **App Store** → **Custom Install**
|
||||||
| student1 | 123456 | Student|
|
3. **Paste this complete Docker Compose configuration:**
|
||||||
|
|
||||||
> You can add new users from the Admin Panel.
|
```yaml
|
||||||
|
name: spelling-practice-app
|
||||||
|
services:
|
||||||
|
spelling-app:
|
||||||
|
image: git.necloud.us/nmemmert/spelling:latest
|
||||||
|
container_name: spelling-practice
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- /DATA/AppData/spelling:/app/data
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
labels:
|
||||||
|
- "casaos.name=Spelling Practice"
|
||||||
|
- "casaos.icon=📝"
|
||||||
|
- "casaos.category=Education"
|
||||||
|
- "casaos.description=Modern spelling and typing practice application"
|
||||||
|
|
||||||
## ⚙️ Features
|
x-casaos:
|
||||||
|
author: NMemmert
|
||||||
|
category: Education
|
||||||
|
description:
|
||||||
|
en_us: "A comprehensive educational application featuring spelling practice, typing exercises, and Bible verse typing with modern UI design."
|
||||||
|
developer: "NMemmert"
|
||||||
|
icon: "📝"
|
||||||
|
screenshot_link: ""
|
||||||
|
tagline:
|
||||||
|
en_us: "Modern Spelling & Typing Practice"
|
||||||
|
thumbnail: ""
|
||||||
|
title:
|
||||||
|
en_us: "Spelling Practice App"
|
||||||
|
tips: {}
|
||||||
|
port_map: "3000"
|
||||||
|
```
|
||||||
|
|
||||||
# 🎮 Student-Facing Features
|
4. **Click Install** and wait for deployment to complete
|
||||||
|
5. **Access the app** via CasaOS Dashboard or directly at `http://your-casaos-ip:3000`
|
||||||
|
|
||||||
- **Word Game Session**
|
> **💡 Tip**: The app will automatically appear in your CasaOS dashboard with the 📝 icon after installation
|
||||||
Timed word presentation with answer input and summary feedback
|
|
||||||
|
|
||||||
- **Retry Missed Words**
|
#### Method 2: Manual CasaOS Installation
|
||||||
Quick review round for any words answered incorrectly
|
1. **Access CasaOS Terminal**:
|
||||||
|
- Open CasaOS Dashboard
|
||||||
|
- Go to **Terminal** or **SSH** into your CasaOS system
|
||||||
|
|
||||||
- **Result Summary Panel**
|
2. **Create application directory:**
|
||||||
Shows score, accuracy percentage, and answers with color-coded feedback
|
```bash
|
||||||
|
mkdir -p /DATA/AppData/spelling
|
||||||
|
cd /DATA/AppData/spelling
|
||||||
|
```
|
||||||
|
|
||||||
- **🎖 Badge Display Section**
|
3. **Create docker-compose.yml file:**
|
||||||
Earned badges appear after each session based on milestone triggers
|
```bash
|
||||||
|
nano docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
- **Responsive Design**
|
4. **Add this configuration and save:**
|
||||||
Touch-friendly interface optimized across devices
|
```yaml
|
||||||
|
version: "3.9"
|
||||||
|
services:
|
||||||
|
spelling-app:
|
||||||
|
image: git.necloud.us/nmemmert/spelling:latest
|
||||||
|
container_name: spelling-practice
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- /DATA/AppData/spelling:/app/data
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Deploy the application:**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Verify deployment:**
|
||||||
|
```bash
|
||||||
|
docker ps | grep spelling-practice
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Add to CasaOS Dashboard** (Optional):
|
||||||
|
- Go to CasaOS **Settings** → **Custom Apps**
|
||||||
|
- Click **Add Custom App**
|
||||||
|
- **Name**: "Spelling Practice"
|
||||||
|
- **URL**: `http://localhost:3000`
|
||||||
|
- **Icon**: 📝 (or upload custom icon)
|
||||||
|
- **Category**: Education
|
||||||
|
|
||||||
|
#### CasaOS-Specific Notes:
|
||||||
|
- **Data Location**: All user data is stored in `/DATA/AppData/spelling/`
|
||||||
|
- **Automatic Restart**: Container will restart automatically with CasaOS
|
||||||
|
- **Port Access**: App will be accessible on port 3000
|
||||||
|
- **Updates**: Use CasaOS container management to update the image
|
||||||
|
- **Backups**: Data directory can be backed up through CasaOS file manager
|
||||||
|
|
||||||
|
#### Troubleshooting CasaOS Installation:
|
||||||
|
```bash
|
||||||
|
# Check if container is running
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
# View container logs
|
||||||
|
docker logs spelling-practice
|
||||||
|
|
||||||
|
# Restart container if needed
|
||||||
|
docker restart spelling-practice
|
||||||
|
|
||||||
|
# Check port availability
|
||||||
|
netstat -tlnp | grep :3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Manual Installation
|
||||||
|
```bash
|
||||||
|
# Clone repository
|
||||||
|
git clone https://git.necloud.us/nmemmert/Spelling.git
|
||||||
|
cd Spelling
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start application
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 📋 Admin Panel Features
|
## 🌐 Access & Default Credentials
|
||||||
|
|
||||||
- **User Management**
|
After installation, access the application at:
|
||||||
Add and remove student accounts from the system
|
- **Local**: http://localhost:3000
|
||||||
|
- **CasaOS**: http://your-casaos-ip:3000
|
||||||
|
|
||||||
- **Word List Editor**
|
### Default Login Credentials
|
||||||
Customize spelling lists per student
|
|
||||||
|
|
||||||
- **Game Preview & Play Trigger**
|
| Username | Password | Role | Description |
|
||||||
Admins can test gameplay directly
|
|-----------|----------|---------|-------------|
|
||||||
|
| admin1 | password | Admin | Full access to admin panel |
|
||||||
|
| student1 | 123456 | Student | Access to practice activities |
|
||||||
|
|
||||||
- **Analytics Tab**
|
> 🔐 **Important**: Change default passwords immediately after first login via Admin Panel → Password Management
|
||||||
Aggregate stats like average scores, most missed words, and per-student breakdown
|
|
||||||
|
|
||||||
- **Badge Viewer Tab**
|
|
||||||
Shows every user's earned badges and lets admins inspect progress visually
|
|
||||||
|
|
||||||
- **Reports Tab**
|
|
||||||
Print-friendly HTML summaries per student with words, scores, and badges
|
|
||||||
|
|
||||||
- **Clipboard Copy Button**
|
|
||||||
Quick export for emailing or recordkeeping
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 🔄 Server-Side Logic
|
## ⚙️ Features Overview
|
||||||
|
|
||||||
- **Structured Result Saving**
|
### � Student Activities
|
||||||
Stores user sessions with score, completion status, and answer accuracy
|
|
||||||
|
|
||||||
- **Badge Awarding & Storage**
|
#### 📝 **Spelling Practice**
|
||||||
Server tracks earned achievements in `badges.json`
|
- Interactive word-by-word spelling challenges
|
||||||
|
- Real-time feedback with visual indicators
|
||||||
|
- Customizable word lists per student
|
||||||
|
- Progress tracking and accuracy scoring
|
||||||
|
- Retry system for missed words
|
||||||
|
|
||||||
- **Safe Data Handling**
|
#### ⌨️ **Typing Practice**
|
||||||
Validates all inputs and formats before writing JSON
|
- Speed and accuracy typing exercises
|
||||||
|
- WPM (Words Per Minute) calculation
|
||||||
|
- Error highlighting and correction
|
||||||
|
- Progressive difficulty levels
|
||||||
|
- Performance analytics
|
||||||
|
|
||||||
|
#### 📖 **Bible Typing (ESV)**
|
||||||
|
- 15 popular Bible verses for typing practice
|
||||||
|
- Verse-by-verse comparison and accuracy analysis
|
||||||
|
- Word-level error detection and highlighting
|
||||||
|
- Spiritual growth through Scripture memorization
|
||||||
|
- Progress tracking for Bible study
|
||||||
|
|
||||||
|
### 🛠️ **Student Features**
|
||||||
|
- **Theme Selection**: Choose from multiple UI themes
|
||||||
|
- **Progress Dashboard**: View personal statistics and achievements
|
||||||
|
- **Badge System**: Earn badges for milestones and consistent practice
|
||||||
|
- **Responsive Design**: Optimized for desktop, tablet, and mobile
|
||||||
|
- **Clean Interface**: Modern, distraction-free learning environment
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 🚀 Upcoming Features (in progress or proposed)
|
### 👨� Admin Panel Features
|
||||||
|
|
||||||
- **Multi-Session History Tracking**
|
#### 📊 **User Management**
|
||||||
Store multiple results per user for trends and performance graphs
|
- Add, edit, and remove student accounts
|
||||||
|
- Role-based access control (Admin/Student)
|
||||||
|
- Password management and reset capabilities
|
||||||
|
- User activity monitoring
|
||||||
|
|
||||||
- **Performance Trends Viewer**
|
#### 📝 **Content Management**
|
||||||
Line/bar charts to show score progression over time
|
- **Word List Editor**: Create and customize spelling lists per student
|
||||||
|
- **Bulk Import**: Upload word lists from text/CSV files
|
||||||
|
- **Category Organization**: Organize words by topics or difficulty
|
||||||
|
- **Preview Mode**: Test activities before assigning to students
|
||||||
|
|
||||||
- **Practice Streak Badges**
|
#### 📈 **Analytics & Reporting**
|
||||||
Awarded for consistent use across days
|
- **Performance Analytics**: Track student progress and trends
|
||||||
|
- **Detailed Reports**: Generate comprehensive progress reports
|
||||||
|
- **Export Options**: Download data for external analysis
|
||||||
|
- **Visual Charts**: Graphical representation of student performance
|
||||||
|
|
||||||
- **Thematic Word Lists**
|
#### 🏆 **Badge Management**
|
||||||
Organize challenges by topic (e.g. animals, tech, holidays)
|
- **Achievement System**: Configure milestone badges
|
||||||
|
- **Progress Tracking**: Monitor student achievements
|
||||||
|
- **Custom Badges**: Create custom rewards for specific goals
|
||||||
|
- **Badge Viewer**: Visual display of all earned achievements
|
||||||
|
|
||||||
- **Email Export Button**
|
#### ⚙️ **System Administration**
|
||||||
Opens a pre-filled `mailto:` with progress summary text
|
- **Password Management**: Change admin and student passwords
|
||||||
|
- **Security Controls**: Manage access permissions
|
||||||
|
- **Data Backup**: Export/import user data and settings
|
||||||
|
- **System Monitoring**: View application health and usage
|
||||||
|
|
||||||
- **PDF Export Option**
|
---
|
||||||
Convert report data into downloadable documents
|
|
||||||
|
## � Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
```bash
|
||||||
|
# Optional environment variables
|
||||||
|
NODE_ENV=production # Set to production for deployment
|
||||||
|
PORT=3000 # Application port (default: 3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Persistence
|
||||||
|
The application stores data in JSON files:
|
||||||
|
- `data/users.json` - User accounts and authentication
|
||||||
|
- `data/wordlists.json` - Custom word lists per student
|
||||||
|
- `data/results.json` - Student progress and results
|
||||||
|
- `data/badges.json` - Achievement and badge data
|
||||||
|
|
||||||
|
### Volume Mapping
|
||||||
|
For Docker installations, map the data directory:
|
||||||
|
```bash
|
||||||
|
-v /host/data/path:/app/data
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## �️ Security Features
|
||||||
|
|
||||||
|
- **Password Hashing**: SHA-256 encryption for all passwords
|
||||||
|
- **Role-Based Access**: Separate admin and student interfaces
|
||||||
|
- **Session Management**: Secure login/logout functionality
|
||||||
|
- **Input Validation**: Protection against common security vulnerabilities
|
||||||
|
- **Data Isolation**: Per-user data segregation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Updates & Maintenance
|
||||||
|
|
||||||
|
### Updating the Application
|
||||||
|
```bash
|
||||||
|
# Pull latest image
|
||||||
|
docker pull git.necloud.us/nmemmert/spelling:latest
|
||||||
|
|
||||||
|
# Restart container
|
||||||
|
docker-compose down && docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup Data
|
||||||
|
```bash
|
||||||
|
# Backup user data
|
||||||
|
docker cp spelling-app:/app/data ./backup-$(date +%Y%m%d)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs & Troubleshooting
|
||||||
|
```bash
|
||||||
|
# View application logs
|
||||||
|
docker logs spelling-app
|
||||||
|
|
||||||
|
# Follow live logs
|
||||||
|
docker logs -f spelling-app
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Support & Contributing
|
||||||
|
|
||||||
|
- **Issues**: Report bugs and feature requests
|
||||||
|
- **Documentation**: Comprehensive setup guides
|
||||||
|
- **Community**: Educational technology discussions
|
||||||
|
|
||||||
|
### Technical Specifications
|
||||||
|
- **Backend**: Node.js 18+ with Express.js
|
||||||
|
- **Frontend**: Vanilla JavaScript with modern CSS
|
||||||
|
- **Storage**: JSON-based file system
|
||||||
|
- **Container**: Multi-stage Docker build (293MB)
|
||||||
|
- **Security**: SHA-256 password hashing, role-based access
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is designed for educational use and community deployment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Built with ❤️ for educators and students everywhere*
|
||||||
@@ -39,6 +39,33 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"timestamp": "2025-07-07T12:14:21.058Z"
|
"timestamp": "2025-07-07T12:14:21.058Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"score": 5,
|
||||||
|
"completed": true,
|
||||||
|
"answers": [
|
||||||
|
{
|
||||||
|
"word": "computer",
|
||||||
|
"correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "keyboard",
|
||||||
|
"correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "mouse",
|
||||||
|
"correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "screen",
|
||||||
|
"correct": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "printer",
|
||||||
|
"correct": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timestamp": "2025-07-15T14:36:13.273Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+12
-4
@@ -1,13 +1,21 @@
|
|||||||
{
|
{
|
||||||
"student1": [],
|
"student1": [
|
||||||
|
"apple",
|
||||||
|
"banana",
|
||||||
|
"cherry",
|
||||||
|
"date",
|
||||||
|
"elderberry"
|
||||||
|
],
|
||||||
"admin1": [
|
"admin1": [
|
||||||
"boy",
|
"boy",
|
||||||
"bot",
|
"bot",
|
||||||
"snot"
|
"snot"
|
||||||
],
|
],
|
||||||
"nate": [
|
"nate": [
|
||||||
"not",
|
"computer",
|
||||||
"bot",
|
"keyboard",
|
||||||
"snot"
|
"mouse",
|
||||||
|
"screen",
|
||||||
|
"printer"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Debug script to check password hashes
|
||||||
|
const { createHash } = require('crypto');
|
||||||
|
|
||||||
|
function hashPassword(password) {
|
||||||
|
return createHash('sha256').update(password).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Testing password hashes:');
|
||||||
|
console.log('password -> ', hashPassword('password'));
|
||||||
|
console.log('123456 -> ', hashPassword('123456'));
|
||||||
|
console.log('test -> ', hashPassword('test'));
|
||||||
|
|
||||||
|
// Known hashes from users.json:
|
||||||
|
console.log('\nExpected hashes from users.json:');
|
||||||
|
console.log('admin1 (password): 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8');
|
||||||
|
console.log('student1 (123456): 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92');
|
||||||
|
console.log('nate: 5a2a558c78d3717db731600c4f354fa1d9c84b556f108091a891f444f1bdec40');
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const resultsPath = path.join(__dirname, 'data', 'results.json');
|
|
||||||
const existingData = JSON.parse(fs.readFileSync(resultsPath));
|
|
||||||
|
|
||||||
for (const username in existingData) {
|
|
||||||
const singleResult = existingData[username];
|
|
||||||
existingData[username] = [{
|
|
||||||
...singleResult,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(resultsPath, JSON.stringify(existingData, null, 2));
|
|
||||||
console.log("✅ Migration complete. Results are now session arrays.");
|
|
||||||
+15
-7
@@ -1,19 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "spelling",
|
"name": "modern-spelling-practice",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
|
"description": "A modern, multi-user spelling practice application with admin controls, customizable themes, and analytics",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"start": "node server.js",
|
||||||
"start": "node server.js"
|
"dev": "node server.js",
|
||||||
|
"test": "echo \"No tests specified\" && exit 0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.necloud.us/nmemmert/Spelling.git"
|
"url": "https://git.necloud.us/nmemmert/Spelling.git"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [
|
||||||
"author": "",
|
"spelling",
|
||||||
|
"education",
|
||||||
|
"practice",
|
||||||
|
"nodejs",
|
||||||
|
"express",
|
||||||
|
"learning"
|
||||||
|
],
|
||||||
|
"author": "NateEmmert",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.1.0"
|
"express": "^5.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+379
-130
@@ -2,200 +2,449 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<title>Spelling App with Roles</title>
|
<title>✨ Modern Spelling Practice</title>
|
||||||
<link rel="stylesheet" href="styles.css" />
|
<link href="styles.css" rel="stylesheet"/>
|
||||||
|
<!-- Scripts -->
|
||||||
<!-- 🔧 Script Load Order -->
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.4.2/sha.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.4.2/sha.js" defer></script>
|
<script src="js/admin.js"></script>
|
||||||
<script src="js/auth.js" defer></script>
|
<script src="js/auth.js"></script>
|
||||||
<script src="js/ui.js" defer></script>
|
<script src="js/ui.js"></script>
|
||||||
<script src="js/themes.js" defer></script>
|
<script src="js/themes.js"></script>
|
||||||
<script src="js/admin.js" defer></script>
|
<script src="js/game.js"></script>
|
||||||
<script src="js/game.js" defer></script>
|
<script src="js/typing.js"></script>
|
||||||
<script src="js/main.js" defer></script>
|
<script src="js/bible.js"></script>
|
||||||
|
<script src="js/main.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>Spelling Practice</h1>
|
<div class="container">
|
||||||
|
<h1>✨ Spelling Practice</h1>
|
||||||
|
|
||||||
<!-- 🏳️ Role Banner -->
|
<!-- Role Banner -->
|
||||||
<div id="roleBanner" class="banner hidden"></div>
|
<div class="banner hidden" id="roleBanner"></div>
|
||||||
|
|
||||||
<!-- 👤 Login Panel -->
|
<!-- Login Panel -->
|
||||||
<div id="loginPanel">
|
<div id="loginPanel" class="card">
|
||||||
<h2>User Login</h2>
|
<div class="card-header">
|
||||||
|
<h2 class="text-center">Welcome Back</h2>
|
||||||
|
<p class="text-center" style="color: var(--text-secondary);">Sign in to continue your spelling practice</p>
|
||||||
|
</div>
|
||||||
<form onsubmit="event.preventDefault(); verifyLogin();">
|
<form onsubmit="event.preventDefault(); verifyLogin();">
|
||||||
<input type="text" id="username" placeholder="Username" required />
|
<input id="username" placeholder="Username" required type="text"/>
|
||||||
<input type="password" id="password" placeholder="Password" required />
|
<input id="password" placeholder="Password" required type="password"/>
|
||||||
<button type="submit">Login</button>
|
<button type="submit">Sign In</button>
|
||||||
</form>
|
</form>
|
||||||
<p id="loginMessage"></p>
|
<p id="loginMessage" style="margin-top: 1rem; text-align: center;"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 🔀 Role-Based Navigation -->
|
<!-- Navigation -->
|
||||||
<div id="nav" class="hidden">
|
<div id="nav" class="hidden">
|
||||||
<p><strong id="userInfo"></strong></p>
|
<p><strong id="userInfo"></strong></p>
|
||||||
<button onclick="showAdmin()" id="adminBtn" class="hidden">Admin Panel</button>
|
<div class="flex gap-4 justify-center" style="margin-top: 1rem;">
|
||||||
<button onclick="showStudent()" id="studentBtn" class="hidden">Student Panel</button>
|
<button class="hidden" id="adminBtn" onclick="showAdmin()">👨💼 Admin Panel</button>
|
||||||
<button onclick="logoutUser()" id="logoutBtn">Log Out</button>
|
<button class="hidden" id="studentBtn" onclick="showStudent()">🎓 Student Panel</button>
|
||||||
|
<button id="logoutBtn" onclick="logoutUser()" class="btn-secondary">Sign Out</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 👨🏫 Admin Panel -->
|
<!-- Admin Panel -->
|
||||||
<div id="adminPanel" class="hidden">
|
<div class="hidden" id="adminPanel">
|
||||||
<h2>👨🏫 Admin Panel</h2>
|
<div class="card-header">
|
||||||
|
<h2>👨💼 Admin Dashboard</h2>
|
||||||
|
<p style="color: var(--text-secondary);">Manage users, word lists, and view analytics</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 🔘 Tabs -->
|
<!-- Admin Tabs -->
|
||||||
<div id="adminTabs">
|
<div id="adminTabs">
|
||||||
<button onclick="switchTab('tabUsers')" class="active">👤 Manage Users</button>
|
<button class="active" onclick="switchTab('tabUsers')">👤 Users</button>
|
||||||
<button onclick="switchTab('tabWords')">📝 Word Lists</button>
|
<button onclick="switchTab('tabWords')">📝 Word Lists</button>
|
||||||
<button onclick="switchTab('tabAnalytics')">📊 Analytics</button>
|
<button onclick="switchTab('tabAnalytics')">📊 Analytics</button>
|
||||||
<button onclick="switchTab('tabResults')">📊 Results</button>
|
<button onclick="switchTab('tabResults')">📋 Results</button>
|
||||||
<button onclick="switchTab('tabReports')">📘 Reports</button>
|
<button onclick="switchTab('tabReports')">📘 Reports</button>
|
||||||
<button onclick="switchTab('tabSessions')">📆 Sessions</button>
|
<button onclick="switchTab('tabSessions')">📅 Sessions</button>
|
||||||
<button onclick="switchTab('tabBadges')">🎖 Badges</button>
|
<button onclick="switchTab('tabBadges')">🏅 Badges</button>
|
||||||
<button onclick="switchTab('tabTheme')">🎨 Theme</button>
|
<button onclick="switchTab('tabPasswords')">🔐 Passwords</button>
|
||||||
|
<button onclick="switchTab('tabTheme')">🎨 Themes</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 👥 Users Tab -->
|
<!-- Users Tab -->
|
||||||
<div id="tabUsers" class="adminTab">
|
<div id="tabUsers" class="adminTab">
|
||||||
<h3>Add New User</h3>
|
<h3>👤 User Management</h3>
|
||||||
<form onsubmit="event.preventDefault(); addNewUser();">
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1.5rem;">
|
||||||
<input type="text" id="newUsername" placeholder="Username" required />
|
<div class="card">
|
||||||
<input type="password" id="newPassword" placeholder="Password" required />
|
<h4>Add New User</h4>
|
||||||
|
<form onsubmit="event.preventDefault(); addNewUser();" style="display: flex; flex-direction: column; gap: 1rem;">
|
||||||
|
<input id="newUsername" placeholder="Username" required type="text"/>
|
||||||
|
<input id="newPassword" placeholder="Password" required type="password"/>
|
||||||
<select id="newRole">
|
<select id="newRole">
|
||||||
<option value="student">Student</option>
|
<option value="student">🎓 Student</option>
|
||||||
<option value="admin">Admin</option>
|
<option value="admin">👨💼 Admin</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit">Add User</button>
|
<button type="submit" class="btn-success">➕ Add User</button>
|
||||||
</form>
|
</form>
|
||||||
|
<p id="userAddMessage" style="margin-top: 1rem;"></p>
|
||||||
|
<small id="hashPreview" style="color: var(--text-secondary);"></small>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h4>Remove User</h4>
|
||||||
|
<select id="userDropdown" onchange="selectUserAction(this.value)" style="width: 100%;">
|
||||||
|
<option value="">-- Select a user to remove --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Delete Users</h3>
|
<!-- Word Lists Tab -->
|
||||||
<select id="userDropdown" onchange="selectUserAction(this.value)">
|
<div class="adminTab hidden" id="tabWords">
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||||
|
<div class="card">
|
||||||
|
<h4>Select User</h4>
|
||||||
|
<select class="word-user-dropdown" id="wordUserSelect" onchange="loadWordsForSelectedUser()" style="width: 100%;">
|
||||||
<option value="">-- Select a user --</option>
|
<option value="">-- Select a user --</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 📝 Word List Tab -->
|
<div class="card">
|
||||||
<div id="tabWords" class="adminTab hidden">
|
<h4>Manage Word List</h4>
|
||||||
<h3>Edit Word List for:</h3>
|
<textarea id="wordInput" placeholder="Enter one word per line..."></textarea>
|
||||||
<select id="wordUserSelect" class="word-user-dropdown" onchange="loadWordsForSelectedUser()">
|
<div style="display: flex; gap: 1rem; margin-top: 1rem;">
|
||||||
<option value="">-- Select a user --</option>
|
<button onclick="saveWords()" class="btn-success">💾 Save Words</button>
|
||||||
</select>
|
<button onclick="clearWords()" class="btn-error">🗑️ Clear All</button>
|
||||||
|
|
||||||
<h2>Enter Spelling Words</h2>
|
|
||||||
<textarea id="wordInput" placeholder="Enter one word per line..."></textarea><br/>
|
|
||||||
<button onclick="saveWords()">Save Word List</button>
|
|
||||||
<button onclick="clearWords()">Clear Saved Words</button>
|
|
||||||
|
|
||||||
<h3>Upload a Word File</h3>
|
|
||||||
<input type="file" accept=".txt,.csv" onchange="handleFileUpload(event)" />
|
|
||||||
|
|
||||||
<p><strong>Current Word List:</strong></p>
|
|
||||||
<ul id="wordListDisplay"></ul>
|
|
||||||
|
|
||||||
<h3>📋 View Word List by User:</h3>
|
|
||||||
<select id="viewUserSelect" class="word-user-dropdown" onchange="displayWordListForSelectedUser()">
|
|
||||||
<option value="">-- Select a user --</option>
|
|
||||||
</select>
|
|
||||||
<ul id="userWordListDisplay"></ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 📊 Results Tab -->
|
<div style="margin-top: 1.5rem;">
|
||||||
<div id="tabResults" class="adminTab hidden">
|
<h5>Upload Word File</h5>
|
||||||
<h3>🧾 Student Results Overview</h3>
|
<input accept=".txt,.csv" onchange="handleFileUpload(event)" type="file" style="margin: 0.5rem 0;"/>
|
||||||
<button onclick="loadStudentResults()">Refresh Results</button>
|
</div>
|
||||||
<ul id="resultsList"></ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 📊 Analytics Tab -->
|
<div class="card">
|
||||||
<div id="tabAnalytics" class="adminTab hidden">
|
<h4>Current Word List</h4>
|
||||||
<h3>Smart Analytics Overview</h3>
|
<ul id="wordListDisplay" style="max-height: 200px; overflow-y: auto;"></ul>
|
||||||
<ul id="analyticsSummary">
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h4>View by Student</h4>
|
||||||
|
<select class="word-user-dropdown" id="viewUserSelect" onchange="displayWordListForSelectedUser()" style="width: 100%;">
|
||||||
|
<option value="">-- Select a student --</option>
|
||||||
|
</select>
|
||||||
|
<ul id="userWordListDisplay" style="max-height: 200px; overflow-y: auto; margin-top: 1rem;"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results Tab -->
|
||||||
|
<div class="adminTab hidden" id="tabResults">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>📋 Student Results Overview</h3>
|
||||||
|
<button onclick="loadStudentResults()" class="btn-primary">🔄 Refresh Results</button>
|
||||||
|
</div>
|
||||||
|
<ul id="resultsList" style="max-height: 400px; overflow-y: auto;"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Analytics Tab -->
|
||||||
|
<div class="adminTab hidden" id="tabAnalytics">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>📊 Performance Analytics</h3>
|
||||||
|
<button onclick="refreshAnalytics()" class="btn-primary">🔄 Refresh Analytics</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||||
|
<div>
|
||||||
|
<h4>Summary Statistics</h4>
|
||||||
|
<ul id="analyticsSummary" style="background: var(--bg-secondary); border-radius: var(--radius); padding: 1rem;">
|
||||||
<li><strong>Average Score:</strong> –</li>
|
<li><strong>Average Score:</strong> –</li>
|
||||||
<li><strong>Total Completed Sessions:</strong> –</li>
|
<li><strong>Total Sessions:</strong> –</li>
|
||||||
<li><strong>Most Missed Word:</strong> –</li>
|
<li><strong>Most Missed Word:</strong> –</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h4>Per-Student Breakdown</h4>
|
|
||||||
<ul id="studentBreakdown"></ul>
|
|
||||||
|
|
||||||
<button onclick="refreshAnalytics()">🔄 Refresh</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 🎖 Badges -->
|
<div>
|
||||||
<div id="tabBadges" class="adminTab hidden">
|
<h4>Student Breakdown</h4>
|
||||||
<h3>🎖 Badge Viewer</h3>
|
<ul id="studentBreakdown" style="max-height: 300px; overflow-y: auto;"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Badges Tab -->
|
||||||
|
<div class="adminTab hidden" id="tabBadges">
|
||||||
|
<div class="card">
|
||||||
|
<h3>🏅 Student Badges</h3>
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 2rem;">
|
||||||
|
<div>
|
||||||
|
<h4>Select Student</h4>
|
||||||
<ul id="badgeUserList"></ul>
|
<ul id="badgeUserList"></ul>
|
||||||
<div id="badgeDetails" class="hidden">
|
</div>
|
||||||
|
<div class="hidden" id="badgeDetails">
|
||||||
<h4 id="badgeUserTitle"></h4>
|
<h4 id="badgeUserTitle"></h4>
|
||||||
<ul id="badgeUserBadges"></ul>
|
<ul id="badgeUserBadges"></ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 🎨 Theme Tab -->
|
<!-- Theme Tab -->
|
||||||
<div id="tabTheme" class="adminTab hidden">
|
<!-- Passwords Tab -->
|
||||||
<h3>🎨 Select Theme</h3>
|
<div class="adminTab hidden" id="tabPasswords">
|
||||||
<select id="themeSelect" onchange="changeTheme(this.value)">
|
<div class="card">
|
||||||
<option value="playground">Playground</option>
|
<div class="card-header">
|
||||||
<option value="chalkboard">Chalkboard</option>
|
<h3>🔐 Password Management</h3>
|
||||||
<option value="galaxy">Galaxy</option>
|
<p style="color: var(--text-secondary);">Change user passwords</p>
|
||||||
<option value="nature">Nature</option>
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
|
||||||
|
<!-- Change Password Form -->
|
||||||
|
<div class="card">
|
||||||
|
<h4>Change User Password</h4>
|
||||||
|
<form onsubmit="event.preventDefault(); changeUserPassword();" style="display: flex; flex-direction: column; gap: 1rem;">
|
||||||
|
<label for="passwordUsername">Select User:</label>
|
||||||
|
<select id="passwordUsername" required style="width: 100%;">
|
||||||
|
<option value="">-- Select a user --</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="newUserPassword">New Password:</label>
|
||||||
|
<input id="newUserPassword" placeholder="Enter new password" required type="password"/>
|
||||||
|
|
||||||
|
<label for="confirmPassword">Confirm Password:</label>
|
||||||
|
<input id="confirmPassword" placeholder="Confirm new password" required type="password"/>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-warning">🔐 Change Password</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Password History -->
|
||||||
|
<div class="card">
|
||||||
|
<h4>Password Change Log</h4>
|
||||||
|
<div id="passwordLog" style="max-height: 300px; overflow-y: auto; background: var(--bg-secondary); padding: 1rem; border-radius: var(--radius);">
|
||||||
|
<p style="color: var(--text-secondary); font-style: italic;">No password changes recorded yet.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Themes Tab -->
|
||||||
|
<div class="adminTab hidden" id="tabTheme">
|
||||||
|
<div class="card">
|
||||||
|
<h3>🎨 Application Themes</h3>
|
||||||
|
<p style="color: var(--text-secondary); margin-bottom: 1.5rem;">Choose a visual theme for the application</p>
|
||||||
|
<select id="themeSelect" onchange="changeTheme(this.value)" style="width: 100%; max-width: 300px;">
|
||||||
|
<option value="default">✨ Modern (Default)</option>
|
||||||
|
<option value="playground">🎪 Playground</option>
|
||||||
|
<option value="chalkboard">🎓 Chalkboard</option>
|
||||||
|
<option value="galaxy">🌌 Galaxy</option>
|
||||||
|
<option value="nature">🌿 Nature</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 🧾 Student Report Viewer -->
|
<!-- Reports Tab -->
|
||||||
<div id="tabReports" class="adminTab hidden">
|
<div class="adminTab hidden" id="tabReports">
|
||||||
<h3>📘 Student Progress Report</h3>
|
<div class="card">
|
||||||
|
<h3>📘 Student Progress Reports</h3>
|
||||||
|
<div style="margin-bottom: 2rem;">
|
||||||
<label for="reportUser">Select Student:</label>
|
<label for="reportUser">Select Student:</label>
|
||||||
<select id="reportUser" onchange="loadStudentReport(this.value)">
|
<select id="reportUser" onchange="loadStudentReport(this.value)" style="width: 100%; max-width: 300px;">
|
||||||
<option value="">-- Select --</option>
|
<option value="">-- Select a student --</option>
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="reportContent" class="hidden">
|
<div class="hidden" id="reportContent">
|
||||||
|
<div class="card" style="background: var(--bg-secondary);">
|
||||||
<h4 id="reportTitle"></h4>
|
<h4 id="reportTitle"></h4>
|
||||||
<p><strong>Last Score:</strong> <span id="reportScore"></span></p>
|
<p><strong>Latest Score:</strong> <span id="reportScore"></span></p>
|
||||||
<p><strong>Words Attempted:</strong></p>
|
|
||||||
<ul id="reportWords"></ul>
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1.5rem;">
|
||||||
<p><strong>Badges Earned:</strong></p>
|
<div>
|
||||||
|
<h5>Words Attempted</h5>
|
||||||
|
<ul id="reportWords" style="max-height: 200px; overflow-y: auto;"></ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5>Badges Earned</h5>
|
||||||
<ul id="reportBadges"></ul>
|
<ul id="reportBadges"></ul>
|
||||||
|
|
||||||
<button onclick="window.print()">🖨️ Print Report</button>
|
|
||||||
<button onclick="copyReportToClipboard()">📋 Copy to Clipboard</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tabSessions" class="adminTab hidden">
|
<div style="margin-top: 2rem; display: flex; gap: 1rem;">
|
||||||
<h3>📘 Archived Sessions</h3>
|
<button onclick="window.print()" class="btn-secondary">🖨️ Print Report</button>
|
||||||
|
<button onclick="copyReportToClipboard()" class="btn-primary">📋 Copy to Clipboard</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sessions Tab -->
|
||||||
|
<div class="adminTab hidden" id="tabSessions">
|
||||||
|
<div class="card">
|
||||||
|
<h3>📅 Session History</h3>
|
||||||
|
<div style="margin-bottom: 2rem;">
|
||||||
<label for="sessionUserSelect">Select Student:</label>
|
<label for="sessionUserSelect">Select Student:</label>
|
||||||
<select id="sessionUserSelect" onchange="loadUserSessions(this.value)">
|
<select id="sessionUserSelect" onchange="loadUserSessions(this.value)" style="width: 100%; max-width: 300px;">
|
||||||
<option value="">-- Select --</option>
|
<option value="">-- Select a student --</option>
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
<ul id="userSessionList"></ul>
|
<ul id="userSessionList" style="max-height: 400px; overflow-y: auto;"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Student Panel -->
|
||||||
<!-- 👩🎓 Student Panel -->
|
<div class="hidden" id="studentPanel">
|
||||||
<div id="studentPanel" class="hidden">
|
<div class="card">
|
||||||
<div id="toast" class="toast">Welcome, student!</div>
|
<div class="card-header">
|
||||||
|
<h2>🎓 Student Dashboard</h2>
|
||||||
<button id="startGameBtn" class="hidden" onclick="startGame()">Start Game</button>
|
<p style="color: var(--text-secondary);">Choose an activity to start practicing</p>
|
||||||
|
|
||||||
<div id="wordBox" class="hidden">
|
|
||||||
<div id="word" class="word-box"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="inputSection" class="hidden">
|
<!-- Theme Selection for Students -->
|
||||||
<input type="text" id="userInput" placeholder="Type your answer" autocomplete="off" />
|
<div class="card" style="margin-bottom: 1.5rem;">
|
||||||
<button onclick="submitAnswer()">Submit</button>
|
<h4>🎨 Choose Your Theme</h4>
|
||||||
|
<select id="studentThemeSelect" onchange="changeTheme(this.value)" style="width: 100%; max-width: 300px;">
|
||||||
|
<option value="default">✨ Modern (Default)</option>
|
||||||
|
<option value="playground">🎪 Playground</option>
|
||||||
|
<option value="chalkboard">🎓 Chalkboard</option>
|
||||||
|
<option value="galaxy">🌌 Galaxy</option>
|
||||||
|
<option value="nature">🌿 Nature</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="badgeDisplay" class="badgeSection hidden">
|
|
||||||
<h3>🎖 Badges Earned</h3>
|
<div class="student-controls">
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; max-width: 900px; margin: 0 auto;">
|
||||||
|
<button id="startGameBtn" onclick="startGame()" class="btn-success">
|
||||||
|
🎮 Spelling Game
|
||||||
|
</button>
|
||||||
|
<button id="startTypingBtn" onclick="startTypingPractice()" class="btn-warning">
|
||||||
|
⌨️ Typing Practice
|
||||||
|
</button>
|
||||||
|
<button id="startBibleBtn" onclick="startBibleTyping()" class="btn-primary">
|
||||||
|
📖 Bible Typing
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Game Section -->
|
||||||
|
<div class="hidden" id="gameSection">
|
||||||
|
<div class="card">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||||
|
<h3>🎮 Spelling Challenge</h3>
|
||||||
|
<button onclick="backToStudentDashboard()" class="btn-secondary" style="padding: 0.5rem 1rem;">
|
||||||
|
← Back to Dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="word-box" id="wordBox"></div>
|
||||||
|
<div class="hidden" id="inputSection" style="text-align: center;">
|
||||||
|
<input id="userInput" placeholder="Type the word you saw..." type="text" style="max-width: 400px;"/>
|
||||||
|
<button onclick="submitAnswer()" class="btn-primary">Submit Answer</button>
|
||||||
|
</div>
|
||||||
|
<!-- Game Summary -->
|
||||||
|
<div class="hidden" id="summary" class="card"></div>
|
||||||
|
<!-- Badge Display -->
|
||||||
|
<div class="hidden" id="badgeDisplay" class="card">
|
||||||
|
<h3>🏅 Congratulations!</h3>
|
||||||
|
<p>You've earned new badges:</p>
|
||||||
<ul id="badgeList"></ul>
|
<ul id="badgeList"></ul>
|
||||||
</div>
|
</div>
|
||||||
<div id="summary" class="hidden"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Typing Practice Section -->
|
||||||
|
<div class="hidden" id="typingSection">
|
||||||
|
<div class="card">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||||
|
<h3>⌨️ Typing Practice</h3>
|
||||||
|
<button onclick="backToStudentDashboard()" class="btn-secondary" style="padding: 0.5rem 1rem;">
|
||||||
|
← Back to Dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="typingPrompt">Get ready to type...</div>
|
||||||
|
<div style="text-align: center; margin: 2rem 0;">
|
||||||
|
<input id="typingInput" type="text" style="max-width: 500px;"/>
|
||||||
|
<button onclick="submitTyping()" class="btn-primary">Submit</button>
|
||||||
|
</div>
|
||||||
|
<p id="typingFeedback" style="text-align: center; font-weight: 600;"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bible Typing Section -->
|
||||||
|
<div class="hidden" id="bibleSection">
|
||||||
|
<div class="card">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||||
|
<h3>📖 Bible Typing (ESV)</h3>
|
||||||
|
<button onclick="backToStudentDashboard()" class="btn-secondary" style="padding: 0.5rem 1rem;">
|
||||||
|
← Back to Dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Verse Selection -->
|
||||||
|
<div style="margin-bottom: 2rem;">
|
||||||
|
<h4>Select a Bible Verse:</h4>
|
||||||
|
<select id="verseSelect" onchange="loadSelectedVerse()" style="width: 100%; max-width: 500px; margin-bottom: 1rem;">
|
||||||
|
<option value="">-- Choose a verse --</option>
|
||||||
|
</select>
|
||||||
|
<button onclick="getRandomVerse()" class="btn-secondary" style="margin-left: 1rem;">🎲 Random Verse</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Verse Display -->
|
||||||
|
<div id="bibleVerseDisplay" style="font-size: 1.2rem; line-height: 1.8; padding: 2rem; background: var(--bg-secondary); border-radius: var(--radius); margin-bottom: 2rem; min-height: 100px; display: flex; align-items: center; justify-content: center; text-align: center;">
|
||||||
|
Select a verse to begin typing practice
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Typing Area -->
|
||||||
|
<div style="text-align: center; margin: 2rem 0;">
|
||||||
|
<textarea id="bibleInput" placeholder="Type the verse here..." style="width: 100%; max-width: 700px; min-height: 120px; font-size: 1.1rem; line-height: 1.6;"></textarea>
|
||||||
|
<div style="margin-top: 1rem;">
|
||||||
|
<button onclick="checkBibleVerse()" class="btn-primary">✓ Check Typing</button>
|
||||||
|
<button onclick="clearBibleInput()" class="btn-secondary">🗑️ Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feedback -->
|
||||||
|
<div id="bibleFeedback" style="text-align: center; margin-top: 2rem;"></div>
|
||||||
|
|
||||||
|
<!-- Progress -->
|
||||||
|
<div id="bibleProgress" style="margin-top: 2rem;">
|
||||||
|
<h4>📊 Typing Progress</h4>
|
||||||
|
<div id="bibleStats" style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; margin-top: 1rem;">
|
||||||
|
<div style="text-align: center; padding: 1rem; background: var(--bg-secondary); border-radius: var(--radius);">
|
||||||
|
<div style="font-size: 2rem; font-weight: bold; color: var(--primary);" id="accuracyPercent">0%</div>
|
||||||
|
<div>Accuracy</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center; padding: 1rem; background: var(--bg-secondary); border-radius: var(--radius);">
|
||||||
|
<div style="font-size: 2rem; font-weight: bold; color: var(--success);" id="correctWords">0</div>
|
||||||
|
<div>Correct Words</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center; padding: 1rem; background: var(--bg-secondary); border-radius: var(--radius);">
|
||||||
|
<div style="font-size: 2rem; font-weight: bold; color: var(--error);" id="incorrectWords">0</div>
|
||||||
|
<div>Errors</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast notification -->
|
||||||
|
<div id="toast" class="toast"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Initialize the application when DOM is loaded
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||||
|
if (user) {
|
||||||
|
if (user.role === 'admin') {
|
||||||
|
showAdmin();
|
||||||
|
} else {
|
||||||
|
showStudent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+290
-48
@@ -1,3 +1,87 @@
|
|||||||
|
// Global variables for admin functionality
|
||||||
|
let allWords = [];
|
||||||
|
let adminUsers = [];
|
||||||
|
|
||||||
|
// 🧭 Admin tab switcher (make it globally accessible)
|
||||||
|
window.switchTab = function switchTab(targetId) {
|
||||||
|
console.log('🔄 Switching to tab:', targetId);
|
||||||
|
|
||||||
|
// Hide all tabs
|
||||||
|
document.querySelectorAll('.adminTab').forEach(tab => {
|
||||||
|
tab.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove active class from all buttons
|
||||||
|
document.querySelectorAll('#adminTabs button').forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the selected tab
|
||||||
|
const targetTab = document.getElementById(targetId);
|
||||||
|
if (targetTab) {
|
||||||
|
targetTab.classList.remove('hidden');
|
||||||
|
console.log('✅ Showing tab:', targetId);
|
||||||
|
} else {
|
||||||
|
console.error('❌ Tab not found:', targetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight the active button
|
||||||
|
document.querySelectorAll('#adminTabs button').forEach(btn => {
|
||||||
|
const onclick = btn.getAttribute('onclick');
|
||||||
|
if (onclick && onclick.includes(targetId)) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔁 Trigger tab-specific logic
|
||||||
|
switch(targetId) {
|
||||||
|
case 'tabSessions':
|
||||||
|
if (typeof populateSessionUserDropdown === 'function') {
|
||||||
|
populateSessionUserDropdown();
|
||||||
|
}
|
||||||
|
const sessionList = document.getElementById("userSessionList");
|
||||||
|
if (sessionList) sessionList.innerHTML = "";
|
||||||
|
break;
|
||||||
|
case 'tabReports':
|
||||||
|
if (typeof loadStudentNamesForReport === 'function') {
|
||||||
|
loadStudentNamesForReport();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'tabPasswords':
|
||||||
|
populatePasswordUserDropdown();
|
||||||
|
break;
|
||||||
|
case 'tabBadges':
|
||||||
|
if (typeof loadBadgeViewer === 'function') {
|
||||||
|
loadBadgeViewer();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'tabAnalytics':
|
||||||
|
if (typeof refreshAnalytics === 'function') {
|
||||||
|
refreshAnalytics();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'tabResults':
|
||||||
|
if (typeof loadStudentResults === 'function') {
|
||||||
|
loadStudentResults();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'tabWords':
|
||||||
|
loadUserDropdowns();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load users from server
|
||||||
|
async function loadUsers() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/getUsers');
|
||||||
|
adminUsers = await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load users:", err);
|
||||||
|
adminUsers = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ➕ Add a new user
|
// ➕ Add a new user
|
||||||
async function addNewUser() {
|
async function addNewUser() {
|
||||||
const username = document.getElementById("newUsername").value.trim();
|
const username = document.getElementById("newUsername").value.trim();
|
||||||
@@ -28,6 +112,7 @@ async function addNewUser() {
|
|||||||
|
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
displayUserDropdown();
|
displayUserDropdown();
|
||||||
|
populateWordUserDropdown();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
msg.textContent = "❌ Failed to add user.";
|
msg.textContent = "❌ Failed to add user.";
|
||||||
console.error("Add user error:", e);
|
console.error("Add user error:", e);
|
||||||
@@ -38,7 +123,7 @@ async function addNewUser() {
|
|||||||
function displayUserDropdown() {
|
function displayUserDropdown() {
|
||||||
const dropdown = document.getElementById("userDropdown");
|
const dropdown = document.getElementById("userDropdown");
|
||||||
dropdown.innerHTML = `<option value="">-- Select a user --</option>`;
|
dropdown.innerHTML = `<option value="">-- Select a user --</option>`;
|
||||||
users.forEach(user => {
|
adminUsers.forEach(user => {
|
||||||
dropdown.innerHTML += `<option value="${user.username}">${user.username} (${user.role})</option>`;
|
dropdown.innerHTML += `<option value="${user.username}">${user.username} (${user.role})</option>`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -104,8 +189,9 @@ async function loadWordsForSelectedUser() {
|
|||||||
const selectedUser = document.getElementById("wordUserSelect").value;
|
const selectedUser = document.getElementById("wordUserSelect").value;
|
||||||
if (!selectedUser) return;
|
if (!selectedUser) return;
|
||||||
|
|
||||||
const res = await fetch(`/getWordList?user=${encodeURIComponent(selectedUser)}`);
|
const res = await fetch(`/getWordList?username=${encodeURIComponent(selectedUser)}`);
|
||||||
allWords = await res.json();
|
const data = await res.json();
|
||||||
|
allWords = data.words || [];
|
||||||
displayWordList();
|
displayWordList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +204,7 @@ function populateWordUserDropdown() {
|
|||||||
const selects = document.querySelectorAll(".word-user-dropdown");
|
const selects = document.querySelectorAll(".word-user-dropdown");
|
||||||
selects.forEach(select => {
|
selects.forEach(select => {
|
||||||
select.innerHTML = '<option value="">-- Select a user --</option>';
|
select.innerHTML = '<option value="">-- Select a user --</option>';
|
||||||
users.forEach(user => {
|
adminUsers.forEach(user => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = user.username;
|
option.value = user.username;
|
||||||
option.textContent = user.username;
|
option.textContent = user.username;
|
||||||
@@ -136,8 +222,9 @@ async function displayWordListForSelectedUser() {
|
|||||||
if (!username) return;
|
if (!username) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/getWordList?user=${username}`);
|
const res = await fetch(`/getWordList?username=${username}`);
|
||||||
const words = await res.json();
|
const data = await res.json();
|
||||||
|
const words = data.words || [];
|
||||||
if (!words.length) {
|
if (!words.length) {
|
||||||
list.innerHTML = "<li><em>No words found for this user.</em></li>";
|
list.innerHTML = "<li><em>No words found for this user.</em></li>";
|
||||||
return;
|
return;
|
||||||
@@ -154,7 +241,7 @@ async function displayWordListForSelectedUser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 📊 Student result viewer
|
// 📊 Student result viewer
|
||||||
async function loadStudentResults() {
|
window.loadStudentResults = async function() {
|
||||||
const list = document.getElementById("resultsList");
|
const list = document.getElementById("resultsList");
|
||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
|
|
||||||
@@ -167,19 +254,38 @@ async function loadStudentResults() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [username, result] of Object.entries(results)) {
|
for (const [username, userResults] of Object.entries(results)) {
|
||||||
|
// Handle array of results per user
|
||||||
|
if (Array.isArray(userResults) && userResults.length > 0) {
|
||||||
|
userResults.forEach((result, index) => {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
let answersHTML = '';
|
let answersHTML = '';
|
||||||
|
|
||||||
|
// Check if result and answers exist and are valid
|
||||||
|
if (result && result.answers && Array.isArray(result.answers)) {
|
||||||
result.answers.forEach(entry => {
|
result.answers.forEach(entry => {
|
||||||
const status = entry.correct ? '✅' : '❌';
|
const status = entry.correct ? '✅' : '❌';
|
||||||
answersHTML += `${status} ${entry.word}<br>`;
|
answersHTML += `${status} ${entry.word}<br>`;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
answersHTML = '<em>No answer data available</em>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = result.timestamp ? new Date(result.timestamp).toLocaleString() : 'Unknown time';
|
||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
<strong>${username}</strong> — Score: ${result.score}, Completed: ${result.completed ? '✅' : '❌'}<br>
|
<strong>${username}</strong> (Session ${index + 1}) — Score: ${result?.score || 'N/A'}, Completed: ${result?.completed ? '✅' : '❌'}<br>
|
||||||
|
<em>Date:</em> ${timestamp}<br>
|
||||||
<em>Answers:</em><br>${answersHTML}
|
<em>Answers:</em><br>${answersHTML}
|
||||||
|
<hr style="margin: 1rem 0; opacity: 0.3;">
|
||||||
`;
|
`;
|
||||||
list.appendChild(li);
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback for single result format
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.innerHTML = `<strong>${username}</strong> — No valid results found`;
|
||||||
|
list.appendChild(li);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("🔴 Failed to load results:", err);
|
console.error("🔴 Failed to load results:", err);
|
||||||
@@ -187,40 +293,6 @@ async function loadStudentResults() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🧭 Admin tab switcher
|
|
||||||
function switchTab(targetId) {
|
|
||||||
document.querySelectorAll('.adminTab').forEach(tab =>
|
|
||||||
tab.classList.add('hidden')
|
|
||||||
);
|
|
||||||
document.querySelectorAll('#adminTabs button').forEach(btn =>
|
|
||||||
btn.classList.remove('active')
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show the selected tab
|
|
||||||
document.getElementById(targetId).classList.remove('hidden');
|
|
||||||
|
|
||||||
// Highlight the active button
|
|
||||||
document.querySelectorAll('#adminTabs button').forEach(btn => {
|
|
||||||
if (btn.getAttribute('onclick')?.includes(targetId)) {
|
|
||||||
btn.classList.add('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🔁 Trigger tab-specific logic
|
|
||||||
if (targetId === 'tabSessions') {
|
|
||||||
loadSessionHistory();
|
|
||||||
}
|
|
||||||
if (targetId === 'tabReports') {
|
|
||||||
loadStudentNamesForReport();
|
|
||||||
}
|
|
||||||
if (targetId === 'tabBadges') {
|
|
||||||
loadBadgeViewer();
|
|
||||||
}
|
|
||||||
if (targetId === 'tabSessions') {
|
|
||||||
populateSessionUserDropdown();
|
|
||||||
document.getElementById("userSessionList").innerHTML = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function loadUserDropdowns() {
|
async function loadUserDropdowns() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/getUsers');
|
const res = await fetch('/getUsers');
|
||||||
@@ -243,6 +315,8 @@ async function loadUserDropdowns() {
|
|||||||
drop.appendChild(opt);
|
drop.appendChild(opt);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('✅ User dropdowns loaded');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("⚠️ Failed to load user list:", err);
|
console.error("⚠️ Failed to load user list:", err);
|
||||||
}
|
}
|
||||||
@@ -352,27 +426,52 @@ async function loadStudentNamesForReport() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStudentReport(username) {
|
window.loadStudentReport = async function(username) {
|
||||||
if (!username) return;
|
if (!username) return;
|
||||||
|
|
||||||
|
try {
|
||||||
const res1 = await fetch('/getResults');
|
const res1 = await fetch('/getResults');
|
||||||
const results = await res1.json();
|
const results = await res1.json();
|
||||||
const res2 = await fetch('/getBadges');
|
const res2 = await fetch('/getBadges');
|
||||||
const badges = await res2.json();
|
const badges = await res2.json();
|
||||||
|
|
||||||
const report = results[username];
|
const userResults = results[username];
|
||||||
const badgeList = badges[username] || [];
|
const badgeList = badges[username] || [];
|
||||||
|
|
||||||
|
// Handle case where userResults might be an array or undefined
|
||||||
|
let latestReport = null;
|
||||||
|
if (Array.isArray(userResults) && userResults.length > 0) {
|
||||||
|
// Get the most recent result
|
||||||
|
latestReport = userResults[userResults.length - 1];
|
||||||
|
} else if (userResults && typeof userResults === 'object') {
|
||||||
|
// Handle single result object
|
||||||
|
latestReport = userResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!latestReport) {
|
||||||
|
document.getElementById("reportContent").classList.add("hidden");
|
||||||
|
alert(`No results found for ${username}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("reportTitle").textContent = `Report for ${username}`;
|
document.getElementById("reportTitle").textContent = `Report for ${username}`;
|
||||||
document.getElementById("reportScore").textContent = `${report.score}/${report.answers.length} correct`;
|
|
||||||
|
// Safe access to score and answers
|
||||||
|
const score = latestReport.score || 0;
|
||||||
|
const answers = latestReport.answers || [];
|
||||||
|
document.getElementById("reportScore").textContent = `${score}/${answers.length} correct`;
|
||||||
|
|
||||||
const wordList = document.getElementById("reportWords");
|
const wordList = document.getElementById("reportWords");
|
||||||
wordList.innerHTML = "";
|
wordList.innerHTML = "";
|
||||||
report.answers.forEach(({ word, correct }) => {
|
if (answers.length > 0) {
|
||||||
|
answers.forEach(({ word, correct }) => {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.textContent = `${word} — ${correct ? '✅' : '❌'}`;
|
li.textContent = `${word} — ${correct ? '✅' : '❌'}`;
|
||||||
wordList.appendChild(li);
|
wordList.appendChild(li);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
wordList.innerHTML = "<li><em>No word data available.</em></li>";
|
||||||
|
}
|
||||||
|
|
||||||
const badgeContainer = document.getElementById("reportBadges");
|
const badgeContainer = document.getElementById("reportBadges");
|
||||||
badgeContainer.innerHTML = "";
|
badgeContainer.innerHTML = "";
|
||||||
@@ -387,9 +486,13 @@ async function loadStudentReport(username) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("reportContent").classList.remove("hidden");
|
document.getElementById("reportContent").classList.remove("hidden");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading student report:", error);
|
||||||
|
alert("Error loading student report. Please try again.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyReportToClipboard() {
|
window.copyReportToClipboard = function() {
|
||||||
const name = document.getElementById("reportTitle").textContent;
|
const name = document.getElementById("reportTitle").textContent;
|
||||||
const score = document.getElementById("reportScore").textContent;
|
const score = document.getElementById("reportScore").textContent;
|
||||||
const words = Array.from(document.querySelectorAll("#reportWords li")).map(li => li.textContent).join("\n");
|
const words = Array.from(document.querySelectorAll("#reportWords li")).map(li => li.textContent).join("\n");
|
||||||
@@ -458,3 +561,142 @@ async function loadUserSessions(username) {
|
|||||||
list.appendChild(sessionItem);
|
list.appendChild(sessionItem);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File upload handler for word lists
|
||||||
|
function handleFileUpload(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
const content = e.target.result;
|
||||||
|
const words = content.split(/\r?\n/).map(w => w.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
document.getElementById('wordInput').value = words.join('\n');
|
||||||
|
showToast(`📁 Loaded ${words.length} words from file`);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔐 Password Management Functions
|
||||||
|
window.changeUserPassword = async function() {
|
||||||
|
const username = document.getElementById('passwordUsername').value;
|
||||||
|
const newPassword = document.getElementById('newUserPassword').value;
|
||||||
|
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
alert('Please select a user');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newPassword || newPassword.length < 3) {
|
||||||
|
alert('Password must be at least 3 characters long');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
alert('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hash the new password (same way as registration)
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(newPassword);
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const newPasswordHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
|
||||||
|
// Send password change request
|
||||||
|
const response = await fetch('/changePassword', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, newPasswordHash })
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = await response.text();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert(message);
|
||||||
|
logPasswordChange(username);
|
||||||
|
// Clear form
|
||||||
|
document.getElementById('newUserPassword').value = '';
|
||||||
|
document.getElementById('confirmPassword').value = '';
|
||||||
|
document.getElementById('passwordUsername').value = '';
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error changing password:', error);
|
||||||
|
alert('Failed to change password');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log password changes
|
||||||
|
function logPasswordChange(username) {
|
||||||
|
const passwordLog = document.getElementById('passwordLog');
|
||||||
|
const timestamp = new Date().toLocaleString();
|
||||||
|
|
||||||
|
const logEntry = document.createElement('div');
|
||||||
|
logEntry.style.padding = '0.5rem';
|
||||||
|
logEntry.style.marginBottom = '0.5rem';
|
||||||
|
logEntry.style.backgroundColor = 'var(--bg-primary)';
|
||||||
|
logEntry.style.borderRadius = 'var(--radius)';
|
||||||
|
logEntry.style.borderLeft = '4px solid var(--warning)';
|
||||||
|
|
||||||
|
logEntry.innerHTML = `
|
||||||
|
<strong>🔐 Password Changed</strong><br>
|
||||||
|
<span style="color: var(--text-secondary);">User: ${username}</span><br>
|
||||||
|
<span style="color: var(--text-secondary); font-size: 0.9rem;">${timestamp}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Remove "no changes" message if it exists
|
||||||
|
if (passwordLog.querySelector('p')) {
|
||||||
|
passwordLog.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new entry at the top
|
||||||
|
passwordLog.insertBefore(logEntry, passwordLog.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate password username dropdown
|
||||||
|
function populatePasswordUserDropdown() {
|
||||||
|
const dropdown = document.getElementById('passwordUsername');
|
||||||
|
if (!dropdown || !adminUsers) return;
|
||||||
|
|
||||||
|
dropdown.innerHTML = '<option value="">-- Select a user --</option>';
|
||||||
|
|
||||||
|
adminUsers.forEach(user => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = user.username;
|
||||||
|
option.textContent = `${user.username} (${user.role})`;
|
||||||
|
dropdown.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners for admin tabs as backup to onclick
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Setup tab click listeners
|
||||||
|
setTimeout(() => {
|
||||||
|
const tabButtons = document.querySelectorAll('#adminTabs button');
|
||||||
|
tabButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const onclick = button.getAttribute('onclick');
|
||||||
|
if (onclick) {
|
||||||
|
const match = onclick.match(/switchTab\('(.+?)'\)/);
|
||||||
|
if (match) {
|
||||||
|
const tabId = match[1];
|
||||||
|
console.log('Tab clicked:', tabId);
|
||||||
|
if (typeof window.switchTab === 'function') {
|
||||||
|
window.switchTab(tabId);
|
||||||
|
} else {
|
||||||
|
console.error('switchTab function not available');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log('✅ Tab event listeners added to', tabButtons.length, 'buttons');
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
+84
-11
@@ -1,8 +1,24 @@
|
|||||||
// 🔐 Password hashing using jsSHA
|
// 🔐 Password hashing using jsSHA
|
||||||
function hashPassword(password) {
|
function hashPassword(password) {
|
||||||
|
try {
|
||||||
|
if (typeof jsSHA === 'undefined') {
|
||||||
|
console.error('jsSHA library not loaded');
|
||||||
|
throw new Error('jsSHA library not available');
|
||||||
|
}
|
||||||
const shaObj = new jsSHA("SHA-256", "TEXT");
|
const shaObj = new jsSHA("SHA-256", "TEXT");
|
||||||
shaObj.update(password);
|
shaObj.update(password);
|
||||||
return shaObj.getHash("HEX");
|
return shaObj.getHash("HEX");
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error hashing password:', error);
|
||||||
|
// Fallback: simple hash (not secure, but for debugging)
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < password.length; i++) {
|
||||||
|
const char = password.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + char;
|
||||||
|
hash = hash & hash; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
return Math.abs(hash).toString(16);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🌐 Load users from server
|
// 🌐 Load users from server
|
||||||
@@ -12,28 +28,43 @@ let users = [];
|
|||||||
async function verifyLogin() {
|
async function verifyLogin() {
|
||||||
const uname = document.getElementById('username').value.trim();
|
const uname = document.getElementById('username').value.trim();
|
||||||
const pw = document.getElementById('password').value;
|
const pw = document.getElementById('password').value;
|
||||||
const hash = hashPassword(pw);
|
|
||||||
const msg = document.getElementById('loginMessage');
|
const msg = document.getElementById('loginMessage');
|
||||||
|
|
||||||
|
if (!uname || !pw) {
|
||||||
|
msg.textContent = "Please enter both username and password.";
|
||||||
|
msg.style.color = 'var(--error)';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('Attempting login for:', uname);
|
||||||
|
const hash = hashPassword(pw);
|
||||||
|
console.log('Generated hash:', hash);
|
||||||
|
|
||||||
const res = await fetch('/verifyUser', {
|
const res = await fetch('/verifyUser', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username: uname, hash })
|
body: JSON.stringify({ username: uname, hash })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const responseText = await res.text();
|
||||||
|
console.log('Server response:', res.status, responseText);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
msg.textContent = "Invalid username or password.";
|
msg.textContent = "Invalid username or password.";
|
||||||
|
msg.style.color = 'var(--error)';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await res.json();
|
const user = JSON.parse(responseText);
|
||||||
|
console.log('Login successful for user:', user);
|
||||||
localStorage.setItem('loggedInUser', JSON.stringify(user));
|
localStorage.setItem('loggedInUser', JSON.stringify(user));
|
||||||
msg.textContent = '';
|
msg.textContent = '';
|
||||||
setupSession(user);
|
setupSession(user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Login error:", err);
|
console.error("Login error:", err);
|
||||||
msg.textContent = "Login failed. Please try again.";
|
msg.textContent = "Login failed. Please try again.";
|
||||||
|
msg.style.color = 'var(--error)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,18 +72,60 @@ async function verifyLogin() {
|
|||||||
function logoutUser() {
|
function logoutUser() {
|
||||||
localStorage.removeItem('loggedInUser');
|
localStorage.removeItem('loggedInUser');
|
||||||
|
|
||||||
document.getElementById('adminPanel').classList.add('hidden');
|
// Hide all panels
|
||||||
document.getElementById('studentPanel').classList.add('hidden');
|
document.getElementById('adminPanel')?.classList.add('hidden');
|
||||||
document.getElementById('nav').classList.add('hidden');
|
document.getElementById('studentPanel')?.classList.add('hidden');
|
||||||
document.getElementById('loginPanel').classList.remove('hidden');
|
document.getElementById('nav')?.classList.add('hidden');
|
||||||
document.getElementById('username').value = '';
|
|
||||||
document.getElementById('password').value = '';
|
|
||||||
|
|
||||||
|
// Show login panel
|
||||||
|
const loginPanel = document.getElementById('loginPanel');
|
||||||
|
loginPanel?.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Clear login form
|
||||||
|
const usernameField = document.getElementById('username');
|
||||||
|
const passwordField = document.getElementById('password');
|
||||||
|
const loginMessage = document.getElementById('loginMessage');
|
||||||
|
|
||||||
|
if (usernameField) usernameField.value = '';
|
||||||
|
if (passwordField) passwordField.value = '';
|
||||||
|
if (loginMessage) {
|
||||||
|
loginMessage.textContent = '';
|
||||||
|
loginMessage.style.color = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset banner
|
||||||
const roleBanner = document.getElementById('roleBanner');
|
const roleBanner = document.getElementById('roleBanner');
|
||||||
|
if (roleBanner) {
|
||||||
roleBanner.classList.add('hidden');
|
roleBanner.classList.add('hidden');
|
||||||
roleBanner.classList.remove('admin', 'student');
|
roleBanner.classList.remove('admin', 'student');
|
||||||
roleBanner.textContent = '';
|
roleBanner.textContent = '';
|
||||||
|
|
||||||
const loginMessage = document.getElementById('loginMessage');
|
|
||||||
if (loginMessage) loginMessage.textContent = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Focus on username field
|
||||||
|
if (usernameField) {
|
||||||
|
setTimeout(() => usernameField.focus(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('Logged out successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Enter key support for login form
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const loginForm = document.querySelector('#loginPanel form');
|
||||||
|
const usernameField = document.getElementById('username');
|
||||||
|
const passwordField = document.getElementById('password');
|
||||||
|
|
||||||
|
if (loginForm && usernameField && passwordField) {
|
||||||
|
// Focus username field on page load
|
||||||
|
usernameField.focus();
|
||||||
|
|
||||||
|
// Add Enter key support
|
||||||
|
[usernameField, passwordField].forEach(field => {
|
||||||
|
field.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
verifyLogin();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
// Bible Typing Practice Module
|
||||||
|
// ESV Bible verses for typing practice
|
||||||
|
|
||||||
|
// Popular Bible verses for typing practice
|
||||||
|
const bibleVerses = [
|
||||||
|
{
|
||||||
|
reference: "John 3:16",
|
||||||
|
text: "For God so loved the world, that he gave his only Son, that whoever believes in him should not perish but have eternal life."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "Philippians 4:13",
|
||||||
|
text: "I can do all things through him who strengthens me."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "Romans 8:28",
|
||||||
|
text: "And we know that for those who love God all things work together for good, for those who are called according to his purpose."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "Jeremiah 29:11",
|
||||||
|
text: "For I know the plans I have for you, declares the Lord, plans for welfare and not for evil, to give you a future and a hope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "Proverbs 3:5-6",
|
||||||
|
text: "Trust in the Lord with all your heart, and do not lean on your own understanding. In all your ways acknowledge him, and he will make straight your paths."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "Psalm 23:1",
|
||||||
|
text: "The Lord is my shepherd; I shall not want."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "Isaiah 40:31",
|
||||||
|
text: "But they who wait for the Lord shall renew their strength; they shall mount up with wings like eagles; they shall run and not be weary; they shall walk and not faint."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "Matthew 28:19-20",
|
||||||
|
text: "Go therefore and make disciples of all nations, baptizing them in the name of the Father and of the Son and of the Holy Spirit, teaching them to observe all that I have commanded you. And behold, I am with you always, to the end of the age."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "1 Corinthians 13:4-5",
|
||||||
|
text: "Love is patient and kind; love does not envy or boast; it is not arrogant or rude. It does not insist on its own way; it is not irritable or resentful."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "Ephesians 2:8-9",
|
||||||
|
text: "For by grace you have been saved through faith. And this is not your own doing; it is the gift of God, not a result of works, so that no one may boast."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "Psalm 119:105",
|
||||||
|
text: "Your word is a lamp to my feet and a light to my path."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "Romans 12:2",
|
||||||
|
text: "Do not be conformed to this world, but be transformed by the renewal of your mind, that by testing you may discern what is the will of God, what is good and acceptable and perfect."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "2 Timothy 3:16",
|
||||||
|
text: "All Scripture is breathed out by God and profitable for teaching, for reproof, for correction, and for training in righteousness."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "Psalm 46:10",
|
||||||
|
text: "Be still, and know that I am God. I will be exalted among the nations, I will be exalted in the earth!"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reference: "Matthew 6:33",
|
||||||
|
text: "But seek first the kingdom of God and his righteousness, and all these things will be added to you."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Global state for Bible typing
|
||||||
|
let currentVerse = null;
|
||||||
|
let typingStartTime = null;
|
||||||
|
|
||||||
|
// Initialize Bible typing
|
||||||
|
window.startBibleTyping = function() {
|
||||||
|
console.log('📖 Starting Bible typing practice');
|
||||||
|
|
||||||
|
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||||
|
if (!user) {
|
||||||
|
alert('Please log in first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide other sections
|
||||||
|
document.getElementById('studentPanel').style.display = 'none';
|
||||||
|
document.getElementById('gameSection').style.display = 'none';
|
||||||
|
document.getElementById('typingSection').style.display = 'none';
|
||||||
|
|
||||||
|
// Show Bible section
|
||||||
|
document.getElementById('bibleSection').classList.remove('hidden');
|
||||||
|
document.getElementById('bibleSection').style.display = 'block';
|
||||||
|
|
||||||
|
// Populate verse dropdown
|
||||||
|
populateVerseDropdown();
|
||||||
|
|
||||||
|
// Reset the interface
|
||||||
|
resetBibleInterface();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the verse selection dropdown
|
||||||
|
function populateVerseDropdown() {
|
||||||
|
const select = document.getElementById('verseSelect');
|
||||||
|
select.innerHTML = '<option value="">-- Choose a verse --</option>';
|
||||||
|
|
||||||
|
bibleVerses.forEach((verse, index) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = index;
|
||||||
|
option.textContent = verse.reference;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load selected verse
|
||||||
|
window.loadSelectedVerse = function() {
|
||||||
|
const select = document.getElementById('verseSelect');
|
||||||
|
const index = parseInt(select.value);
|
||||||
|
|
||||||
|
if (isNaN(index) || index < 0 || index >= bibleVerses.length) {
|
||||||
|
resetBibleInterface();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentVerse = bibleVerses[index];
|
||||||
|
displayVerse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get random verse
|
||||||
|
window.getRandomVerse = function() {
|
||||||
|
const randomIndex = Math.floor(Math.random() * bibleVerses.length);
|
||||||
|
currentVerse = bibleVerses[randomIndex];
|
||||||
|
|
||||||
|
// Update dropdown
|
||||||
|
document.getElementById('verseSelect').value = randomIndex;
|
||||||
|
|
||||||
|
displayVerse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the selected verse
|
||||||
|
function displayVerse() {
|
||||||
|
if (!currentVerse) return;
|
||||||
|
|
||||||
|
const display = document.getElementById('bibleVerseDisplay');
|
||||||
|
display.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: bold; color: var(--primary); margin-bottom: 1rem; font-size: 1.3rem;">
|
||||||
|
${currentVerse.reference}
|
||||||
|
</div>
|
||||||
|
<div style="font-style: italic;">
|
||||||
|
"${currentVerse.text}"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Clear input and reset stats
|
||||||
|
document.getElementById('bibleInput').value = '';
|
||||||
|
document.getElementById('bibleFeedback').innerHTML = '';
|
||||||
|
resetStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check typed verse against original
|
||||||
|
window.checkBibleVerse = function() {
|
||||||
|
if (!currentVerse) {
|
||||||
|
alert('Please select a verse first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typedText = document.getElementById('bibleInput').value.trim();
|
||||||
|
if (!typedText) {
|
||||||
|
alert('Please type something first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalText = currentVerse.text;
|
||||||
|
const feedback = document.getElementById('bibleFeedback');
|
||||||
|
|
||||||
|
// Calculate accuracy
|
||||||
|
const { accuracy, correctWords, totalWords, errors } = calculateAccuracy(originalText, typedText);
|
||||||
|
|
||||||
|
// Update stats
|
||||||
|
updateStats(accuracy, correctWords, errors);
|
||||||
|
|
||||||
|
// Show feedback
|
||||||
|
if (accuracy === 100) {
|
||||||
|
feedback.innerHTML = `
|
||||||
|
<div style="color: var(--success); font-size: 1.2rem; font-weight: bold;">
|
||||||
|
🎉 Perfect! You typed the verse correctly!
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 1rem;">
|
||||||
|
<button onclick="getRandomVerse()" class="btn-primary">📖 Try Another Verse</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
feedback.innerHTML = `
|
||||||
|
<div style="color: var(--warning); font-size: 1.1rem; font-weight: bold;">
|
||||||
|
📝 ${accuracy.toFixed(1)}% Accuracy - Keep practicing!
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 1rem; color: var(--text-secondary);">
|
||||||
|
<div>Correct words: ${correctWords} / ${totalWords}</div>
|
||||||
|
<div>Errors: ${errors}</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 1rem;">
|
||||||
|
<button onclick="clearBibleInput()" class="btn-secondary">🔄 Try Again</button>
|
||||||
|
<button onclick="showDifferences()" class="btn-primary">👀 Show Differences</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate typing accuracy
|
||||||
|
function calculateAccuracy(original, typed) {
|
||||||
|
const originalWords = original.toLowerCase().split(/\s+/);
|
||||||
|
const typedWords = typed.toLowerCase().split(/\s+/);
|
||||||
|
|
||||||
|
let correctWords = 0;
|
||||||
|
let errors = 0;
|
||||||
|
const maxLength = Math.max(originalWords.length, typedWords.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLength; i++) {
|
||||||
|
const originalWord = originalWords[i] || '';
|
||||||
|
const typedWord = typedWords[i] || '';
|
||||||
|
|
||||||
|
if (originalWord === typedWord) {
|
||||||
|
correctWords++;
|
||||||
|
} else {
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accuracy = originalWords.length > 0 ? (correctWords / originalWords.length) * 100 : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
accuracy,
|
||||||
|
correctWords,
|
||||||
|
totalWords: originalWords.length,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update statistics display
|
||||||
|
function updateStats(accuracy, correctWords, errors) {
|
||||||
|
document.getElementById('accuracyPercent').textContent = accuracy.toFixed(1) + '%';
|
||||||
|
document.getElementById('correctWords').textContent = correctWords;
|
||||||
|
document.getElementById('incorrectWords').textContent = errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset statistics
|
||||||
|
function resetStats() {
|
||||||
|
document.getElementById('accuracyPercent').textContent = '0%';
|
||||||
|
document.getElementById('correctWords').textContent = '0';
|
||||||
|
document.getElementById('incorrectWords').textContent = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show word-by-word differences
|
||||||
|
window.showDifferences = function() {
|
||||||
|
if (!currentVerse) return;
|
||||||
|
|
||||||
|
const typedText = document.getElementById('bibleInput').value.trim();
|
||||||
|
const originalWords = currentVerse.text.split(/\s+/);
|
||||||
|
const typedWords = typedText.split(/\s+/);
|
||||||
|
|
||||||
|
let comparisonHTML = '<div style="text-align: left; margin-top: 1rem; padding: 1rem; background: var(--bg-secondary); border-radius: var(--radius);">';
|
||||||
|
comparisonHTML += '<h4>Word-by-word comparison:</h4><div style="line-height: 2;">';
|
||||||
|
|
||||||
|
const maxLength = Math.max(originalWords.length, typedWords.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLength; i++) {
|
||||||
|
const originalWord = originalWords[i] || '';
|
||||||
|
const typedWord = typedWords[i] || '';
|
||||||
|
|
||||||
|
if (originalWord.toLowerCase() === typedWord.toLowerCase()) {
|
||||||
|
comparisonHTML += `<span style="background: var(--success); color: white; padding: 2px 6px; margin: 2px; border-radius: 4px;">${originalWord}</span> `;
|
||||||
|
} else if (typedWord === '') {
|
||||||
|
comparisonHTML += `<span style="background: var(--error); color: white; padding: 2px 6px; margin: 2px; border-radius: 4px; text-decoration: line-through;">${originalWord}</span> `;
|
||||||
|
} else {
|
||||||
|
comparisonHTML += `<span style="background: var(--warning); color: white; padding: 2px 6px; margin: 2px; border-radius: 4px;">${originalWord} (typed: ${typedWord})</span> `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comparisonHTML += '</div></div>';
|
||||||
|
|
||||||
|
document.getElementById('bibleFeedback').innerHTML += comparisonHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear Bible input
|
||||||
|
window.clearBibleInput = function() {
|
||||||
|
document.getElementById('bibleInput').value = '';
|
||||||
|
document.getElementById('bibleFeedback').innerHTML = '';
|
||||||
|
resetStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset Bible interface
|
||||||
|
function resetBibleInterface() {
|
||||||
|
currentVerse = null;
|
||||||
|
document.getElementById('verseSelect').value = '';
|
||||||
|
document.getElementById('bibleVerseDisplay').innerHTML = 'Select a verse to begin typing practice';
|
||||||
|
document.getElementById('bibleInput').value = '';
|
||||||
|
document.getElementById('bibleFeedback').innerHTML = '';
|
||||||
|
resetStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export reset function for use by main dashboard
|
||||||
|
window.resetBibleInterface = resetBibleInterface;
|
||||||
+200
-41
@@ -1,64 +1,142 @@
|
|||||||
// 🌍 Game state
|
// 🌍 Game state
|
||||||
let allWords = [], words = [], current = 0, results = [];
|
let words = [];
|
||||||
|
let currentWord = '';
|
||||||
|
let currentIndex = 0;
|
||||||
|
let results = [];
|
||||||
|
// 📝 Check user's answer and move to next
|
||||||
|
window.submitAnswer = function() {
|
||||||
|
const input = document.getElementById('userInput').value.trim();
|
||||||
|
|
||||||
// 👩🎓 Show student interface
|
// Record the attempt
|
||||||
function showStudent() {
|
results.push({
|
||||||
console.log("🔊 showStudent() executed");
|
word: currentWord,
|
||||||
|
attempt: input
|
||||||
|
});
|
||||||
|
|
||||||
const studentPanel = document.getElementById('studentPanel');
|
if (input.toLowerCase() === currentWord.toLowerCase()) {
|
||||||
const adminPanel = document.getElementById('adminPanel');
|
currentIndex++;
|
||||||
studentPanel?.classList.remove('hidden');
|
document.getElementById('inputSection').classList.add('hidden');
|
||||||
adminPanel?.classList.add('hidden');
|
setTimeout(() => showWord(), 500); // Brief pause before next word
|
||||||
|
} else {
|
||||||
|
// Show feedback for incorrect answer
|
||||||
|
const inputEl = document.getElementById('userInput');
|
||||||
|
inputEl.style.borderColor = 'var(--error)';
|
||||||
|
inputEl.style.backgroundColor = '#fef2f2';
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const btn = document.getElementById('startGameBtn');
|
inputEl.style.borderColor = '';
|
||||||
if (btn) {
|
inputEl.style.backgroundColor = '';
|
||||||
console.log("🎯 Button found in delayed reveal");
|
inputEl.focus();
|
||||||
btn.classList.remove('hidden');
|
}, 1000);
|
||||||
btn.style.display = 'inline-block';
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
} else {
|
||||||
console.warn("⚠️ Button still missing after timeout");
|
console.error("Could not find game buttons");
|
||||||
}
|
}
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚀 Begin game session
|
// 🚀 Begin game session
|
||||||
async function startGame() {
|
window.startGame = async function() {
|
||||||
|
console.log('🎮 Starting spelling game');
|
||||||
|
|
||||||
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||||
if (!user) return;
|
if (!user) {
|
||||||
|
alert('Please log in first');
|
||||||
const res = await fetch(`/getWordList?user=${encodeURIComponent(user.username)}`);
|
|
||||||
allWords = await res.json();
|
|
||||||
|
|
||||||
if (!allWords.length) {
|
|
||||||
alert("No words found. Please add words in Admin Panel.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
words = [...allWords];
|
try {
|
||||||
current = 0;
|
// Hide student dashboard and other sections
|
||||||
results = [];
|
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('summary')?.classList.add('hidden');
|
||||||
document.getElementById('badgeDisplay')?.classList.add('hidden');
|
document.getElementById('badgeDisplay')?.classList.add('hidden');
|
||||||
|
|
||||||
|
// Fetch words
|
||||||
|
const response = await fetch(`/getWordList?username=${user.username}`);
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Received words:', data);
|
||||||
|
|
||||||
|
if (!data.words || data.words.length === 0) {
|
||||||
|
alert('No words found for practice');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start game
|
||||||
|
words = [...data.words];
|
||||||
|
typingWords = [...data.words]; // Initialize typing words too
|
||||||
|
currentIndex = 0;
|
||||||
|
results = []; // Reset results array
|
||||||
showWord();
|
showWord();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Failed to load words');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔤 Show a word, then hide after timeout
|
|
||||||
function showWord() {
|
function showWord() {
|
||||||
const wordBox = document.getElementById('wordBox');
|
if (currentIndex >= words.length) {
|
||||||
const wordEl = document.getElementById('word');
|
showSummary();
|
||||||
|
return;
|
||||||
if (!words[current]) {
|
|
||||||
endGame(); return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wordEl.textContent = words[current];
|
currentWord = words[currentIndex];
|
||||||
wordBox.classList.remove('hidden');
|
console.log('Current word:', currentWord);
|
||||||
|
|
||||||
|
// Show word briefly then hide
|
||||||
|
const wordBox = document.getElementById('wordBox');
|
||||||
|
const inputSection = document.getElementById('inputSection');
|
||||||
|
|
||||||
|
wordBox.textContent = currentWord;
|
||||||
|
inputSection.classList.add('hidden');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
wordBox.classList.add('hidden');
|
wordBox.textContent = '';
|
||||||
document.getElementById('inputSection').classList.remove('hidden');
|
inputSection.classList.remove('hidden');
|
||||||
document.getElementById('userInput').value = '';
|
document.getElementById('userInput').value = '';
|
||||||
document.getElementById('userInput').focus();
|
document.getElementById('userInput').focus();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -67,15 +145,19 @@ function showWord() {
|
|||||||
// 📝 Check user’s answer and move to next
|
// 📝 Check user’s answer and move to next
|
||||||
function submitAnswer() {
|
function submitAnswer() {
|
||||||
const input = document.getElementById('userInput').value.trim();
|
const input = document.getElementById('userInput').value.trim();
|
||||||
results.push({ word: words[current], attempt: input });
|
|
||||||
current++;
|
|
||||||
|
|
||||||
|
// Record the attempt
|
||||||
|
results.push({
|
||||||
|
word: currentWord,
|
||||||
|
attempt: input
|
||||||
|
});
|
||||||
|
|
||||||
|
if (input.toLowerCase() === currentWord.toLowerCase()) {
|
||||||
|
currentIndex++;
|
||||||
document.getElementById('inputSection').classList.add('hidden');
|
document.getElementById('inputSection').classList.add('hidden');
|
||||||
|
|
||||||
if (current < words.length) {
|
|
||||||
showWord();
|
showWord();
|
||||||
} else {
|
} else {
|
||||||
showSummary();
|
alert('Try again!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,3 +246,80 @@ function retryMissed(missedWords) {
|
|||||||
document.getElementById('badgeDisplay')?.classList.add('hidden');
|
document.getElementById('badgeDisplay')?.classList.add('hidden');
|
||||||
showWord();
|
showWord();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New function to start typing practice
|
||||||
|
async function startTypingPractice() {
|
||||||
|
console.log("⌨️ Starting typing practice...");
|
||||||
|
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||||
|
if (!user) {
|
||||||
|
console.error("No user logged in");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/getWordList?username=${encodeURIComponent(user.username)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
const words = data.words || [];
|
||||||
|
|
||||||
|
if (!words.length) {
|
||||||
|
alert("No words found for practice. Please contact your teacher.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide game section, show typing section
|
||||||
|
document.getElementById('gameSection')?.classList.add('hidden');
|
||||||
|
document.getElementById('typingSection').classList.remove('hidden');
|
||||||
|
|
||||||
|
// Initialize typing practice
|
||||||
|
localStorage.setItem('typingWords', JSON.stringify(words));
|
||||||
|
showNextTypingSentence();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting typing practice:', error);
|
||||||
|
alert('Error loading typing practice. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSentenceWithWord(word) {
|
||||||
|
const templates = [
|
||||||
|
`The word to spell is "${word}."`,
|
||||||
|
`Please spell the word "${word}."`,
|
||||||
|
`Type the word "${word}" carefully.`,
|
||||||
|
`Practice spelling "${word}" in this sentence.`
|
||||||
|
];
|
||||||
|
return templates[Math.floor(Math.random() * templates.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNextTypingSentence() {
|
||||||
|
console.log('Showing next sentence...'); // Debug log
|
||||||
|
|
||||||
|
if (!typingWords.length) {
|
||||||
|
document.getElementById('typingPrompt').textContent = '🎉 All done! Great job!';
|
||||||
|
document.getElementById('typingInput').disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const word = typingWords[0];
|
||||||
|
currentSentence = createSentenceWithWord(word);
|
||||||
|
console.log('Current sentence:', currentSentence); // Debug log
|
||||||
|
|
||||||
|
document.getElementById('typingPrompt').textContent = currentSentence;
|
||||||
|
document.getElementById('typingInput').value = '';
|
||||||
|
document.getElementById('typingInput').disabled = false;
|
||||||
|
document.getElementById('typingInput').focus();
|
||||||
|
document.getElementById('typingFeedback').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitTyping() {
|
||||||
|
const input = document.getElementById('typingInput').value.trim();
|
||||||
|
const feedback = document.getElementById('typingFeedback');
|
||||||
|
|
||||||
|
if (input.toLowerCase() === currentSentence.toLowerCase()) {
|
||||||
|
feedback.textContent = '✅ Perfect!';
|
||||||
|
feedback.style.color = 'green';
|
||||||
|
typingWords.shift(); // Remove completed word
|
||||||
|
setTimeout(showNextTypingSentence, 1500);
|
||||||
|
} else {
|
||||||
|
feedback.textContent = '❌ Try again. Type the exact sentence as shown.';
|
||||||
|
feedback.style.color = 'red';
|
||||||
|
}
|
||||||
|
}
|
||||||
+118
-12
@@ -1,22 +1,128 @@
|
|||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
// 🌐 Load users.json for login validation
|
console.log('🚀 Application starting...');
|
||||||
// await loadUsers();
|
|
||||||
displayUserDropdown();
|
|
||||||
populateWordUserDropdown();
|
|
||||||
|
|
||||||
// 🎨 Restore saved theme
|
// Ensure all panels are hidden initially
|
||||||
|
document.getElementById('adminPanel')?.classList.add('hidden');
|
||||||
|
document.getElementById('studentPanel')?.classList.add('hidden');
|
||||||
|
document.getElementById('gameSection')?.classList.add('hidden');
|
||||||
|
document.getElementById('typingSection')?.classList.add('hidden');
|
||||||
|
document.getElementById('bibleSection')?.classList.add('hidden');
|
||||||
|
document.getElementById('nav')?.classList.add('hidden');
|
||||||
|
document.getElementById('roleBanner')?.classList.add('hidden');
|
||||||
|
|
||||||
|
// Restore saved theme
|
||||||
applySavedTheme();
|
applySavedTheme();
|
||||||
|
|
||||||
// 🔁 Restore session if user was logged in
|
// Restore session if user was logged in
|
||||||
const savedUser = localStorage.getItem("loggedInUser");
|
const savedUser = localStorage.getItem("loggedInUser");
|
||||||
if (savedUser) {
|
if (savedUser) {
|
||||||
setupSession(JSON.parse(savedUser));
|
try {
|
||||||
|
const user = JSON.parse(savedUser);
|
||||||
|
console.log('👤 Restoring session for:', user.username);
|
||||||
|
|
||||||
|
// Load users for admin functionality only if user is admin
|
||||||
|
if (user.role === 'admin') {
|
||||||
|
try {
|
||||||
|
await loadUsers();
|
||||||
|
displayUserDropdown();
|
||||||
|
populateWordUserDropdown();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not load admin data:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🧭 Tab switching for Admin Panel
|
setupSession(user);
|
||||||
const defaultTab = document.querySelector('#adminTabs button.active');
|
} catch (e) {
|
||||||
if (defaultTab) {
|
console.error('Error restoring session:', e);
|
||||||
const targetId = defaultTab.getAttribute("onclick")?.match(/'(.*?)'/)?.[1];
|
localStorage.removeItem("loggedInUser");
|
||||||
if (targetId) switchTab(targetId);
|
showLoginOnly();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('👤 No saved session found');
|
||||||
|
showLoginOnly();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Function to show only the login panel
|
||||||
|
function showLoginOnly() {
|
||||||
|
// Hide all panels
|
||||||
|
document.getElementById('adminPanel')?.classList.add('hidden');
|
||||||
|
document.getElementById('studentPanel')?.classList.add('hidden');
|
||||||
|
document.getElementById('gameSection')?.classList.add('hidden');
|
||||||
|
document.getElementById('typingSection')?.classList.add('hidden');
|
||||||
|
document.getElementById('bibleSection')?.classList.add('hidden');
|
||||||
|
document.getElementById('nav')?.classList.add('hidden');
|
||||||
|
document.getElementById('roleBanner')?.classList.add('hidden');
|
||||||
|
|
||||||
|
// Show login panel
|
||||||
|
document.getElementById('loginPanel')?.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Focus on login form
|
||||||
|
const usernameField = document.getElementById('username');
|
||||||
|
if (usernameField) {
|
||||||
|
setTimeout(() => usernameField.focus(), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to return to student dashboard
|
||||||
|
window.backToStudentDashboard = function() {
|
||||||
|
console.log('🔙 Returning to student dashboard');
|
||||||
|
|
||||||
|
// Hide all sections
|
||||||
|
document.getElementById('gameSection').classList.add('hidden');
|
||||||
|
document.getElementById('gameSection').style.display = 'none';
|
||||||
|
document.getElementById('typingSection').classList.add('hidden');
|
||||||
|
document.getElementById('typingSection').style.display = 'none';
|
||||||
|
document.getElementById('bibleSection').classList.add('hidden');
|
||||||
|
document.getElementById('bibleSection').style.display = 'none';
|
||||||
|
|
||||||
|
// Show student panel
|
||||||
|
document.getElementById('studentPanel').classList.remove('hidden');
|
||||||
|
document.getElementById('studentPanel').style.display = 'block';
|
||||||
|
|
||||||
|
// Reset any game/typing state
|
||||||
|
const userInput = document.getElementById('userInput');
|
||||||
|
const typingInput = document.getElementById('typingInput');
|
||||||
|
const wordBox = document.getElementById('wordBox');
|
||||||
|
const typingPrompt = document.getElementById('typingPrompt');
|
||||||
|
const typingFeedback = document.getElementById('typingFeedback');
|
||||||
|
|
||||||
|
if (userInput) userInput.value = '';
|
||||||
|
if (typingInput) typingInput.value = '';
|
||||||
|
if (wordBox) wordBox.textContent = '';
|
||||||
|
if (typingPrompt) typingPrompt.textContent = 'Get ready to type...';
|
||||||
|
if (typingFeedback) typingFeedback.textContent = '';
|
||||||
|
|
||||||
|
// Reset Bible typing state
|
||||||
|
if (typeof window.resetBibleInterface === 'function') {
|
||||||
|
window.resetBibleInterface();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to change theme (accessible to students)
|
||||||
|
window.changeTheme = function(themeName) {
|
||||||
|
console.log('🎨 Changing theme to:', themeName);
|
||||||
|
|
||||||
|
// Apply the theme
|
||||||
|
applyTheme(themeName);
|
||||||
|
|
||||||
|
// Save the preference in both possible keys
|
||||||
|
localStorage.setItem('selectedTheme', themeName);
|
||||||
|
localStorage.setItem('theme', themeName);
|
||||||
|
|
||||||
|
// Update both theme selects if they exist
|
||||||
|
const adminThemeSelect = document.getElementById('themeSelect');
|
||||||
|
const studentThemeSelect = document.getElementById('studentThemeSelect');
|
||||||
|
|
||||||
|
if (adminThemeSelect) {
|
||||||
|
adminThemeSelect.value = themeName;
|
||||||
|
}
|
||||||
|
if (studentThemeSelect) {
|
||||||
|
studentThemeSelect.value = themeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show toast notification
|
||||||
|
if (typeof showToast === 'function') {
|
||||||
|
showToast(`🎨 Theme changed to ${themeName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
-5
@@ -1,14 +1,77 @@
|
|||||||
// 🎨 Apply saved theme on load
|
// Modern theme system
|
||||||
function applySavedTheme() {
|
function applySavedTheme() {
|
||||||
const savedTheme = localStorage.getItem('theme') || 'playground';
|
const savedTheme = localStorage.getItem('theme') || localStorage.getItem('selectedTheme') || 'default';
|
||||||
document.body.className = savedTheme;
|
applyTheme(savedTheme);
|
||||||
|
|
||||||
|
// Update both theme selectors
|
||||||
const themeSelector = document.getElementById('themeSelect');
|
const themeSelector = document.getElementById('themeSelect');
|
||||||
|
const studentThemeSelector = document.getElementById('studentThemeSelect');
|
||||||
|
|
||||||
if (themeSelector) themeSelector.value = savedTheme;
|
if (themeSelector) themeSelector.value = savedTheme;
|
||||||
|
if (studentThemeSelector) studentThemeSelector.value = savedTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🎨 Change theme dynamically
|
|
||||||
function changeTheme(themeName) {
|
function changeTheme(themeName) {
|
||||||
document.body.className = themeName;
|
applyTheme(themeName);
|
||||||
localStorage.setItem('theme', themeName);
|
localStorage.setItem('theme', themeName);
|
||||||
|
showToast(`🎨 Theme changed to ${themeName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(themeName) {
|
||||||
|
// Remove existing theme classes
|
||||||
|
document.body.className = '';
|
||||||
|
|
||||||
|
// Apply theme-specific styles
|
||||||
|
switch(themeName) {
|
||||||
|
case 'playground':
|
||||||
|
document.body.className = 'theme-playground';
|
||||||
|
document.documentElement.style.setProperty('--primary', '#ff6b6b');
|
||||||
|
document.documentElement.style.setProperty('--accent', '#4ecdc4');
|
||||||
|
document.documentElement.style.setProperty('--bg-primary', '#fff5f5');
|
||||||
|
document.documentElement.style.setProperty('--bg-secondary', '#ffebee');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'chalkboard':
|
||||||
|
document.body.className = 'theme-chalkboard';
|
||||||
|
document.documentElement.style.setProperty('--primary', '#4ade80');
|
||||||
|
document.documentElement.style.setProperty('--accent', '#22d3ee');
|
||||||
|
document.documentElement.style.setProperty('--bg-primary', '#1f2937');
|
||||||
|
document.documentElement.style.setProperty('--bg-secondary', '#374151');
|
||||||
|
document.documentElement.style.setProperty('--text-primary', '#f9fafb');
|
||||||
|
document.documentElement.style.setProperty('--text-secondary', '#d1d5db');
|
||||||
|
document.documentElement.style.setProperty('--border', '#4b5563');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'galaxy':
|
||||||
|
document.body.className = 'theme-galaxy';
|
||||||
|
document.documentElement.style.setProperty('--primary', '#8b5cf6');
|
||||||
|
document.documentElement.style.setProperty('--accent', '#06b6d4');
|
||||||
|
document.documentElement.style.setProperty('--bg-primary', '#0f0f23');
|
||||||
|
document.documentElement.style.setProperty('--bg-secondary', '#1a1a3f');
|
||||||
|
document.documentElement.style.setProperty('--text-primary', '#e0e7ff');
|
||||||
|
document.documentElement.style.setProperty('--text-secondary', '#a5b4fc');
|
||||||
|
document.documentElement.style.setProperty('--border', '#3730a3');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'nature':
|
||||||
|
document.body.className = 'theme-nature';
|
||||||
|
document.documentElement.style.setProperty('--primary', '#059669');
|
||||||
|
document.documentElement.style.setProperty('--accent', '#84cc16');
|
||||||
|
document.documentElement.style.setProperty('--bg-primary', '#f0fdf4');
|
||||||
|
document.documentElement.style.setProperty('--bg-secondary', '#dcfce7');
|
||||||
|
document.documentElement.style.setProperty('--text-primary', '#14532d');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // Modern default theme
|
||||||
|
document.body.className = 'theme-default';
|
||||||
|
// Reset to original CSS custom properties
|
||||||
|
document.documentElement.style.setProperty('--primary', '#3b82f6');
|
||||||
|
document.documentElement.style.setProperty('--accent', '#06b6d4');
|
||||||
|
document.documentElement.style.setProperty('--bg-primary', '#ffffff');
|
||||||
|
document.documentElement.style.setProperty('--bg-secondary', '#f8fafc');
|
||||||
|
document.documentElement.style.setProperty('--text-primary', '#1e293b');
|
||||||
|
document.documentElement.style.setProperty('--text-secondary', '#64748b');
|
||||||
|
document.documentElement.style.setProperty('--border', '#e2e8f0');
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
// js/typing.js
|
||||||
|
// Global state
|
||||||
|
const typingState = {
|
||||||
|
words: [],
|
||||||
|
currentWord: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make functions globally accessible
|
||||||
|
window.startTypingPractice = async function() {
|
||||||
|
console.log('Starting typing practice');
|
||||||
|
|
||||||
|
const user = JSON.parse(localStorage.getItem('loggedInUser'));
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
// Hide student dashboard and other sections
|
||||||
|
document.getElementById('studentPanel').style.display = 'none';
|
||||||
|
document.getElementById('gameSection').classList.add('hidden');
|
||||||
|
document.getElementById('gameSection').style.display = 'none';
|
||||||
|
document.getElementById('bibleSection').style.display = 'none';
|
||||||
|
document.getElementById('typingSection').classList.remove('hidden');
|
||||||
|
document.getElementById('typingSection').style.display = 'block';
|
||||||
|
|
||||||
|
const typingPrompt = document.getElementById('typingPrompt');
|
||||||
|
typingPrompt.textContent = 'Loading...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/getWordList?username=${encodeURIComponent(user.username)}`);
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Words received:', data);
|
||||||
|
|
||||||
|
if (data.words?.length) {
|
||||||
|
typingState.words = [...data.words];
|
||||||
|
showNextWord();
|
||||||
|
} else {
|
||||||
|
typingPrompt.textContent = 'No words available';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
typingPrompt.textContent = 'Error loading words';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.showNextWord = function() {
|
||||||
|
const promptEl = document.getElementById('typingPrompt');
|
||||||
|
const inputEl = document.getElementById('typingInput');
|
||||||
|
if (!promptEl || !inputEl) return;
|
||||||
|
|
||||||
|
if (!typingState.words.length) {
|
||||||
|
promptEl.textContent = '🎉 Practice complete!';
|
||||||
|
inputEl.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle both string and object formats
|
||||||
|
const wordObj = typingState.words[0];
|
||||||
|
typingState.currentWord = typeof wordObj === 'string' ? wordObj : wordObj.word;
|
||||||
|
|
||||||
|
console.log('Showing word:', typingState.currentWord);
|
||||||
|
promptEl.textContent = typingState.currentWord;
|
||||||
|
promptEl.style.display = 'block';
|
||||||
|
|
||||||
|
inputEl.value = '';
|
||||||
|
inputEl.disabled = false;
|
||||||
|
inputEl.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.submitTyping = function() {
|
||||||
|
const input = document.getElementById('typingInput')?.value.trim() || '';
|
||||||
|
const feedback = document.getElementById('typingFeedback');
|
||||||
|
if (!feedback) return;
|
||||||
|
|
||||||
|
if (input.toLowerCase() === typingState.currentWord.toLowerCase()) {
|
||||||
|
feedback.textContent = '✅ Correct!';
|
||||||
|
feedback.style.color = 'green';
|
||||||
|
typingState.words.shift();
|
||||||
|
setTimeout(showNextWord, 1500);
|
||||||
|
} else {
|
||||||
|
feedback.textContent = '❌ Try again';
|
||||||
|
feedback.style.color = 'red';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener for Enter key
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const input = document.getElementById('typingInput');
|
||||||
|
if (input) {
|
||||||
|
input.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') submitTyping();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
+59
-7
@@ -46,16 +46,68 @@ function showToast(message, duration = 3000) {
|
|||||||
toast.classList.remove("show");
|
toast.classList.remove("show");
|
||||||
}, duration);
|
}, duration);
|
||||||
}
|
}
|
||||||
|
function showStudent() {
|
||||||
|
console.log("🎓 Showing student panel");
|
||||||
|
|
||||||
|
// Hide other panels
|
||||||
|
document.getElementById('adminPanel')?.classList.add('hidden');
|
||||||
|
document.getElementById('loginPanel')?.classList.add('hidden');
|
||||||
|
|
||||||
|
// Show student panel
|
||||||
|
const studentPanel = document.getElementById('studentPanel');
|
||||||
|
if (studentPanel) {
|
||||||
|
studentPanel.classList.remove('hidden');
|
||||||
|
studentPanel.style.display = 'block';
|
||||||
|
|
||||||
|
// Ensure buttons are visible and functional
|
||||||
|
const gameBtn = document.getElementById('startGameBtn');
|
||||||
|
const typingBtn = document.getElementById('startTypingBtn');
|
||||||
|
const bibleBtn = document.getElementById('startBibleBtn');
|
||||||
|
|
||||||
|
if (gameBtn && typingBtn && bibleBtn) {
|
||||||
|
gameBtn.classList.remove('hidden');
|
||||||
|
typingBtn.classList.remove('hidden');
|
||||||
|
bibleBtn.classList.remove('hidden');
|
||||||
|
console.log("✅ All student buttons are now visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update student theme selector to match current theme
|
||||||
|
const savedTheme = localStorage.getItem('theme') || localStorage.getItem('selectedTheme') || 'default';
|
||||||
|
const studentThemeSelect = document.getElementById('studentThemeSelect');
|
||||||
|
if (studentThemeSelect) {
|
||||||
|
studentThemeSelect.value = savedTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showAdmin() {
|
function showAdmin() {
|
||||||
console.log("showAdmin fired");
|
console.log("👨💼 Showing admin panel");
|
||||||
|
|
||||||
|
// Hide other panels
|
||||||
|
document.getElementById('studentPanel')?.classList.add('hidden');
|
||||||
|
document.getElementById('loginPanel')?.classList.add('hidden');
|
||||||
|
|
||||||
const adminPanel = document.getElementById("adminPanel");
|
const adminPanel = document.getElementById("adminPanel");
|
||||||
const studentPanel = document.getElementById("studentPanel");
|
if (adminPanel) {
|
||||||
|
adminPanel.classList.remove("hidden");
|
||||||
|
|
||||||
loadUserDropdowns();
|
// Load user data for dropdowns
|
||||||
|
loadUserDropdowns().then(() => {
|
||||||
|
console.log("✅ User dropdowns loaded");
|
||||||
|
}).catch(e => {
|
||||||
|
console.warn("Could not load user dropdowns:", e);
|
||||||
|
});
|
||||||
|
|
||||||
adminPanel?.classList.remove("hidden");
|
// Set default tab - use a slight delay to ensure DOM is ready
|
||||||
studentPanel?.classList.add("hidden");
|
setTimeout(() => {
|
||||||
|
if (typeof window.switchTab === 'function') {
|
||||||
switchTab("tabUsers"); // optional default tab
|
window.switchTab("tabUsers");
|
||||||
|
console.log("✅ Default tab set");
|
||||||
|
} else {
|
||||||
|
console.error("switchTab function not available");
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
console.error("Admin panel not found");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+540
-128
@@ -1,152 +1,564 @@
|
|||||||
/* 📋 Global Defaults */
|
/* Modern Spelling App Styles */
|
||||||
body {
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
font-family: sans-serif;
|
|
||||||
text-align: center;
|
/* Reset and base styles */
|
||||||
padding: 2rem;
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Modern color palette */
|
||||||
|
--primary: #3b82f6;
|
||||||
|
--primary-dark: #2563eb;
|
||||||
|
--secondary: #f1f5f9;
|
||||||
|
--accent: #06b6d4;
|
||||||
|
--success: #10b981;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--error: #ef4444;
|
||||||
|
--text-primary: #1e293b;
|
||||||
|
--text-secondary: #64748b;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--bg-primary: #ffffff;
|
||||||
|
--bg-secondary: #f8fafc;
|
||||||
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||||
|
--radius: 0.75rem;
|
||||||
|
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--bg-secondary) 0%, #ffffff 100%);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container and layout */
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form elements */
|
||||||
|
input, textarea, select {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin: 0.5rem;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons - Global enhanced readability */
|
||||||
|
button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
margin: 0.25rem;
|
||||||
|
background: var(--primary);
|
||||||
|
color: #ffffff !important;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
color: #ffffff !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button variants with enhanced readability */
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f1f5f9 !important;
|
||||||
|
color: #1e293b !important;
|
||||||
|
border: 2px solid #e2e8f0 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #e2e8f0 !important;
|
||||||
|
color: #1e293b !important;
|
||||||
|
border-color: #cbd5e1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #3b82f6 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border: 2px solid transparent !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
|
||||||
|
min-height: 44px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #2563eb !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #10b981 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border: 2px solid transparent !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
|
||||||
|
min-height: 44px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #059669 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: #f59e0b !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border: 2px solid transparent !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
|
||||||
|
min-height: 44px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover {
|
||||||
|
background: #d97706 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-error {
|
||||||
|
background: #ef4444 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border: 2px solid transparent !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
|
||||||
|
min-height: 44px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-error:hover {
|
||||||
|
background: #dc2626 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional button contrast fixes */
|
||||||
|
#startGameBtn, #startTypingBtn {
|
||||||
|
background: var(--success) !important;
|
||||||
|
color: white !important;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
min-height: 48px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#startGameBtn:hover, #startTypingBtn:hover {
|
||||||
|
background: #059669 !important;
|
||||||
|
color: white !important;
|
||||||
|
transform: translateY(-2px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typing Practice button specific styling */
|
||||||
|
#startTypingBtn {
|
||||||
|
background: var(--warning) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#startTypingBtn:hover {
|
||||||
|
background: #d97706 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bible typing button specific styling */
|
||||||
|
#startBibleBtn {
|
||||||
|
background: var(--primary) !important;
|
||||||
|
color: white !important;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
min-height: 60px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #2563eb) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#startBibleBtn:hover {
|
||||||
|
background: linear-gradient(135deg, #2563eb, #1d4ed8) !important;
|
||||||
|
color: white !important;
|
||||||
|
transform: translateY(-3px) !important;
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bible typing interface styling with enhanced button readability */
|
||||||
|
#bibleSection textarea {
|
||||||
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bibleSection textarea:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#verseSelect {
|
||||||
|
max-width: 500px !important;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Bible typing button styles - Force override all button styling */
|
||||||
|
#bibleSection .btn-primary,
|
||||||
|
#bibleSection .btn-secondary,
|
||||||
|
#bibleFeedback .btn-primary,
|
||||||
|
#bibleFeedback .btn-secondary,
|
||||||
|
#bibleSection button,
|
||||||
|
#bibleFeedback button {
|
||||||
|
min-height: 44px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 1rem !important;
|
||||||
|
padding: 0.75rem 1.5rem !important;
|
||||||
|
border-radius: var(--radius) !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 0.5rem !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
border: none !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bible section primary buttons (Check Typing, Show Differences) */
|
||||||
|
#bibleSection .btn-primary,
|
||||||
|
#bibleFeedback .btn-primary {
|
||||||
|
background: #10b981 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3) !important;
|
||||||
|
border: 2px solid #10b981 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bibleSection .btn-primary:hover,
|
||||||
|
#bibleFeedback .btn-primary:hover {
|
||||||
|
background: #059669 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4) !important;
|
||||||
|
border: 2px solid #059669 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bible section secondary buttons (Clear, Random) */
|
||||||
|
#bibleSection .btn-secondary,
|
||||||
|
#bibleFeedback .btn-secondary {
|
||||||
|
background: #f1f5f9 !important;
|
||||||
|
color: #1e293b !important;
|
||||||
|
border: 2px solid #e2e8f0 !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
text-shadow: none !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#bibleSection .btn-secondary:hover,
|
||||||
|
#bibleFeedback .btn-secondary:hover {
|
||||||
|
background: #e2e8f0 !important;
|
||||||
|
color: #1e293b !important;
|
||||||
|
border: 2px solid #cbd5e1 !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific component styles */
|
||||||
|
#loginPanel {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginPanel form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Banner */
|
||||||
|
.banner {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner.admin {
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner.student {
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
#nav {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admin tabs */
|
||||||
|
#adminTabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
#adminTabs button {
|
||||||
|
flex: 1;
|
||||||
|
min-width: fit-content;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border: none;
|
||||||
|
border-radius: calc(var(--radius) - 0.25rem);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: var(--transition);
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#adminTabs button:hover {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#adminTabs button.active {
|
||||||
|
background: var(--primary) !important;
|
||||||
|
color: white !important;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.adminTab {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout utilities */
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adminTab.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Game elements */
|
||||||
|
#wordBox, .word-box, #typingPrompt {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--secondary) 100%);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
color: var(--primary);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
min-height: 120px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-controls {
|
||||||
|
text-align: center;
|
||||||
|
margin: 2rem 0;
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Student controls specific styling */
|
||||||
|
.student-controls button {
|
||||||
|
min-height: 60px !important;
|
||||||
|
font-size: 1.1rem !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
text-shadow: none !important;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-controls .btn-success {
|
||||||
|
background: linear-gradient(135deg, #10b981, #059669) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-controls .btn-warning {
|
||||||
|
background: linear-gradient(135deg, #f59e0b, #d97706) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-controls button:hover {
|
||||||
|
transform: translateY(-3px) !important;
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lists */
|
||||||
ul {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin: 0.25rem 0;
|
padding: 0.75rem 1rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: calc(var(--radius) - 0.25rem);
|
||||||
|
border: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
input, textarea, select {
|
/* Toast notifications */
|
||||||
font-size: 1rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
margin: 0.5rem;
|
|
||||||
border: 2px solid #ccc;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🎨 Theme Styles */
|
|
||||||
body.playground {
|
|
||||||
background: #fef9e7;
|
|
||||||
color: #333;
|
|
||||||
font-family: 'Comic Sans MS', cursive, sans-serif;
|
|
||||||
}
|
|
||||||
body.chalkboard {
|
|
||||||
background: #222;
|
|
||||||
color: #f0f0f0;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
}
|
|
||||||
body.galaxy {
|
|
||||||
background: radial-gradient(circle at center, #0d0d2b, #1a1a3f);
|
|
||||||
color: #b3e5fc;
|
|
||||||
font-family: 'Orbitron', sans-serif;
|
|
||||||
}
|
|
||||||
body.nature {
|
|
||||||
background: #e8f5e9;
|
|
||||||
color: #2e7d32;
|
|
||||||
font-family: 'Georgia', serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🔘 Buttons */
|
|
||||||
button {
|
|
||||||
background-color: #4caf50;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.6rem 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin: 0.4rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🌈 Theme-specific Button Colors */
|
|
||||||
body.chalkboard button {
|
|
||||||
background-color: #555;
|
|
||||||
}
|
|
||||||
body.galaxy button {
|
|
||||||
background-color: #6200ea;
|
|
||||||
}
|
|
||||||
body.nature button {
|
|
||||||
background-color: #81c784;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🔳 Panels and Banners */
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#wordBox {
|
|
||||||
margin-top: 5rem;
|
|
||||||
}
|
|
||||||
.word-box {
|
|
||||||
font-size: 3rem;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
color: #2196f3;
|
|
||||||
text-shadow: 1px 1px #ccc;
|
|
||||||
}
|
|
||||||
.banner {
|
|
||||||
padding: 0.5em;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.banner.admin {
|
|
||||||
background-color: #b22222;
|
|
||||||
}
|
|
||||||
.banner.student {
|
|
||||||
background-color: #1e90ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🔔 Toast Messages */
|
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: 2rem;
|
||||||
right: 20px;
|
right: 2rem;
|
||||||
background: #333;
|
background: var(--text-primary);
|
||||||
color: #fff;
|
color: white;
|
||||||
padding: 0.75em 1.25em;
|
padding: 1rem 1.5rem;
|
||||||
border-radius: 5px;
|
border-radius: var(--radius);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
transform: translateY(100%);
|
||||||
transition: opacity 0.4s ease;
|
transition: var(--transition);
|
||||||
z-index: 999;
|
z-index: 1000;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast.show {
|
.toast.show {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 🧭 Admin Tabs */
|
|
||||||
#adminTabs {
|
#adminTabs {
|
||||||
margin: 1em 0;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
#adminTabs button {
|
#adminTabs button {
|
||||||
margin-right: 8px;
|
flex: none;
|
||||||
padding: 10px 16px;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-bottom: none;
|
|
||||||
border-radius: 6px 6px 0 0;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s, color 0.2s;
|
|
||||||
}
|
}
|
||||||
#adminTabs button:hover {
|
|
||||||
background-color: #e0f2ff;
|
.card, .adminTab, #loginPanel {
|
||||||
color: #000;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
#adminTabs button.active {
|
|
||||||
background-color: #ffffff;
|
#wordBox, .word-box, #typingPrompt {
|
||||||
color: #000;
|
font-size: 2rem;
|
||||||
border-bottom: 2px solid #fff;
|
padding: 2rem 1rem;
|
||||||
box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
.adminTab {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.adminTab:not(.hidden) {
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
+3
-1
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"student1": [],
|
||||||
"nate": [
|
"nate": [
|
||||||
"Perfect Round",
|
"Perfect Round",
|
||||||
"No Misses",
|
"No Misses",
|
||||||
"Accuracy Master",
|
"Accuracy Master",
|
||||||
"Practice Streak",
|
"Practice Streak",
|
||||||
"Vocabulary Pro"
|
"Vocabulary Pro"
|
||||||
]
|
],
|
||||||
|
"admin1": []
|
||||||
}
|
}
|
||||||
+7
-7
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"student1": [],
|
"student1": [
|
||||||
|
"apple", "banana", "cherry", "date", "elderberry",
|
||||||
|
"grape", "honeydew", "kiwi", "lemon", "mango"
|
||||||
|
],
|
||||||
"admin1": [
|
"admin1": [
|
||||||
"boy",
|
"administration", "education", "technology", "development", "programming"
|
||||||
"bot",
|
|
||||||
"snot"
|
|
||||||
],
|
],
|
||||||
"nate": [
|
"nate": [
|
||||||
"not",
|
"computer", "keyboard", "mouse", "screen", "printer",
|
||||||
"bot",
|
"software", "hardware", "network", "database", "security"
|
||||||
"snot"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -30,12 +30,59 @@ function ensureFileWithSeed(name, fallback = '{}') {
|
|||||||
if (fs.existsSync(seedPath)) {
|
if (fs.existsSync(seedPath)) {
|
||||||
fs.copyFileSync(seedPath, targetPath);
|
fs.copyFileSync(seedPath, targetPath);
|
||||||
console.log(`🌱 Seeded ${name} from seed/${name}`);
|
console.log(`🌱 Seeded ${name} from seed/${name}`);
|
||||||
|
} else {
|
||||||
|
// Create default data for each file type
|
||||||
|
if (name === 'users.json') {
|
||||||
|
const defaultUsers = [
|
||||||
|
{
|
||||||
|
"username": "admin1",
|
||||||
|
"hash": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", // password
|
||||||
|
"role": "admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "nate",
|
||||||
|
"hash": "5a2a558c78d3717db731600c4f354fa1d9c84b556f108091a891f444f1bdec40", // nate123
|
||||||
|
"role": "student"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "student1",
|
||||||
|
"hash": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92", // 123456
|
||||||
|
"role": "student"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
fs.writeFileSync(targetPath, JSON.stringify(defaultUsers, null, 2));
|
||||||
|
console.log(`👥 Created default users: admin1, nate, student1`);
|
||||||
|
} else if (name === 'wordlists.json') {
|
||||||
|
const defaultWordlists = {
|
||||||
|
"student1": [
|
||||||
|
"apple", "banana", "cherry", "date", "elderberry",
|
||||||
|
"grape", "honeydew", "kiwi", "lemon", "mango"
|
||||||
|
],
|
||||||
|
"admin1": [
|
||||||
|
"administration", "education", "technology", "development", "programming"
|
||||||
|
],
|
||||||
|
"nate": [
|
||||||
|
"computer", "keyboard", "mouse", "screen", "printer",
|
||||||
|
"software", "hardware", "network", "database", "security"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
fs.writeFileSync(targetPath, JSON.stringify(defaultWordlists, null, 2));
|
||||||
|
console.log(`📝 Created default word lists for all users`);
|
||||||
|
} else if (name === 'badges.json') {
|
||||||
|
const defaultBadges = {
|
||||||
|
"student1": [],
|
||||||
|
"nate": [],
|
||||||
|
"admin1": []
|
||||||
|
};
|
||||||
|
fs.writeFileSync(targetPath, JSON.stringify(defaultBadges, null, 2));
|
||||||
|
console.log(`🏆 Created default badge structure`);
|
||||||
} else {
|
} else {
|
||||||
fs.writeFileSync(targetPath, fallback);
|
fs.writeFileSync(targetPath, fallback);
|
||||||
console.log(`📄 Initialized ${name} with default`);
|
console.log(`📄 Initialized ${name} with default`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [key, file] of Object.entries(files)) {
|
for (const [key, file] of Object.entries(files)) {
|
||||||
const defaultData = file === 'users.json' ? '[]' : '{}';
|
const defaultData = file === 'users.json' ? '[]' : '{}';
|
||||||
@@ -54,12 +101,25 @@ function readJsonSafe(filePath, fallback = {}) {
|
|||||||
// 🔐 Verify login
|
// 🔐 Verify login
|
||||||
app.post('/verifyUser', (req, res) => {
|
app.post('/verifyUser', (req, res) => {
|
||||||
const { username, hash } = req.body;
|
const { username, hash } = req.body;
|
||||||
|
console.log('Login attempt - Username:', username, 'Hash:', hash);
|
||||||
|
|
||||||
if (typeof username !== 'string' || typeof hash !== 'string') {
|
if (typeof username !== 'string' || typeof hash !== 'string') {
|
||||||
|
console.log('Invalid credentials format');
|
||||||
return res.status(400).send("Invalid credentials format");
|
return res.status(400).send("Invalid credentials format");
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = readJsonSafe(path.join(DATA_DIR, files.users), []);
|
const users = readJsonSafe(path.join(DATA_DIR, files.users), []);
|
||||||
|
console.log('Available users:', users.map(u => u.username));
|
||||||
|
|
||||||
const user = users.find(u => u.username === username && u.hash === hash);
|
const user = users.find(u => u.username === username && u.hash === hash);
|
||||||
user ? res.json(user) : res.status(401).send("Invalid login");
|
|
||||||
|
if (user) {
|
||||||
|
console.log('Login successful for:', username);
|
||||||
|
res.json(user);
|
||||||
|
} else {
|
||||||
|
console.log('Login failed for:', username);
|
||||||
|
res.status(401).send("Invalid login");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ➕ Add user
|
// ➕ Add user
|
||||||
@@ -97,14 +157,57 @@ app.post('/deleteUser', (req, res) => {
|
|||||||
res.send(`✅ User "${username}" deleted`);
|
res.send(`✅ User "${username}" deleted`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🔐 Change user password
|
||||||
|
app.post('/changePassword', (req, res) => {
|
||||||
|
const { username, newPasswordHash } = req.body;
|
||||||
|
if (typeof username !== 'string' || typeof newPasswordHash !== 'string') {
|
||||||
|
return res.status(400).send("Invalid request data");
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersPath = path.join(DATA_DIR, files.users);
|
||||||
|
const users = readJsonSafe(usersPath, []);
|
||||||
|
const userIndex = users.findIndex(u => u.username === username);
|
||||||
|
|
||||||
|
if (userIndex === -1) {
|
||||||
|
return res.status(404).send("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the password hash
|
||||||
|
users[userIndex].hash = newPasswordHash;
|
||||||
|
|
||||||
|
// Save updated users
|
||||||
|
fs.writeFileSync(usersPath, JSON.stringify(users, null, 2));
|
||||||
|
|
||||||
|
console.log(`Password changed for user: ${username}`);
|
||||||
|
res.send(`✅ Password changed for user "${username}"`);
|
||||||
|
});
|
||||||
|
|
||||||
// 📚 Word list
|
// 📚 Word list
|
||||||
app.get('/getWordList', (req, res) => {
|
app.get('/getWordList', (req, res) => {
|
||||||
const username = req.query.user;
|
try {
|
||||||
if (!username || typeof username !== 'string') {
|
const { username } = req.query;
|
||||||
return res.status(400).send("Username required");
|
if (!username) {
|
||||||
|
return res.status(400).json({ error: 'Username is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add debug logging
|
||||||
|
console.log('Requested username:', username);
|
||||||
|
|
||||||
|
const wordListPath = path.join(DATA_DIR, 'wordlists.json');
|
||||||
|
console.log('Reading from:', wordListPath);
|
||||||
|
|
||||||
|
const wordLists = readJsonSafe(wordListPath, {});
|
||||||
|
console.log('Available wordlists:', Object.keys(wordLists));
|
||||||
|
|
||||||
|
// Get words for specific user
|
||||||
|
const userWords = wordLists[username] || [];
|
||||||
|
console.log(`Words found for ${username}:`, userWords);
|
||||||
|
|
||||||
|
res.json({ words: userWords });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getWordList:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to get word list' });
|
||||||
}
|
}
|
||||||
const wordlists = readJsonSafe(path.join(DATA_DIR, files.wordlists));
|
|
||||||
res.json(wordlists[username] || []);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/saveWordList', (req, res) => {
|
app.post('/saveWordList', (req, res) => {
|
||||||
@@ -191,4 +294,32 @@ app.get('/getBadges', (req, res) => {
|
|||||||
// 🚀 Server start
|
// 🚀 Server start
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`✅ Server listening at http://localhost:${PORT}`);
|
console.log(`✅ Server listening at http://localhost:${PORT}`);
|
||||||
|
|
||||||
|
// Display available users for easy login
|
||||||
|
try {
|
||||||
|
const usersPath = path.join(DATA_DIR, files.users);
|
||||||
|
const users = readJsonSafe(usersPath, []);
|
||||||
|
console.log('\n👥 Available Users:');
|
||||||
|
console.log('┌─────────────┬────────────┬──────────┐');
|
||||||
|
console.log('│ Username │ Password │ Role │');
|
||||||
|
console.log('├─────────────┼────────────┼──────────┤');
|
||||||
|
|
||||||
|
const userInfo = [
|
||||||
|
{ username: 'admin1', password: 'password', role: 'admin' },
|
||||||
|
{ username: 'nate', password: 'nate123', role: 'student' },
|
||||||
|
{ username: 'student1', password: '123456', role: 'student' }
|
||||||
|
];
|
||||||
|
|
||||||
|
userInfo.forEach(user => {
|
||||||
|
const exists = users.find(u => u.username === user.username);
|
||||||
|
const status = exists ? '✅' : '❌';
|
||||||
|
console.log(`│ ${user.username.padEnd(11)} │ ${user.password.padEnd(10)} │ ${user.role.padEnd(8)} │ ${status}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('└─────────────┴────────────┴──────────┘');
|
||||||
|
console.log('\n🔐 Note: Change default passwords after first login!');
|
||||||
|
console.log(`🌐 Access the app at: http://localhost:${PORT}\n`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('⚠️ Could not display user information');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user