Initial commit: Add MathHomeworkHelper project with web and Android components
Some checks reported errors
continuous-integration/drone Build encountered an error
Some checks reported errors
continuous-integration/drone Build encountered an error
This commit is contained in:
commit
f9558008e1
37 changed files with 5318 additions and 0 deletions
60
.drone.yml
Normal file
60
.drone.yml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: android-build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Step 1: Build Debug APK
|
||||||
|
- name: build-debug
|
||||||
|
image: cirrusci/android-sdk:latest
|
||||||
|
environment:
|
||||||
|
GRADLE_USER_HOME: .gradle
|
||||||
|
commands:
|
||||||
|
- cd MathHomeWorkHelper
|
||||||
|
- echo "Building Debug APK..."
|
||||||
|
- chmod +x ./gradlew
|
||||||
|
- ./gradlew clean assembleDebug
|
||||||
|
- echo "Debug APK built successfully!"
|
||||||
|
|
||||||
|
# Step 2: Build Release APK
|
||||||
|
- name: build-release
|
||||||
|
image: cirrusci/android-sdk:latest
|
||||||
|
environment:
|
||||||
|
GRADLE_USER_HOME: .gradle
|
||||||
|
commands:
|
||||||
|
- cd MathHomeWorkHelper
|
||||||
|
- echo "Building Release APK..."
|
||||||
|
- chmod +x ./gradlew
|
||||||
|
- ./gradlew clean assembleRelease
|
||||||
|
- echo "Release APK built successfully!"
|
||||||
|
|
||||||
|
# Step 3: Run Unit Tests
|
||||||
|
- name: run-tests
|
||||||
|
image: cirrusci/android-sdk:latest
|
||||||
|
environment:
|
||||||
|
GRADLE_USER_HOME: .gradle
|
||||||
|
commands:
|
||||||
|
- cd MathHomeWorkHelper
|
||||||
|
- echo "Running unit tests..."
|
||||||
|
- chmod +x ./gradlew
|
||||||
|
- ./gradlew test
|
||||||
|
- echo "Tests completed!"
|
||||||
|
|
||||||
|
# Step 4: Generate Build Report
|
||||||
|
- name: build-report
|
||||||
|
image: cirrusci/android-sdk:latest
|
||||||
|
commands:
|
||||||
|
- cd MathHomeWorkHelper
|
||||||
|
- echo "=== BUILD REPORT ===" > build_report.txt
|
||||||
|
- echo "Build Date: $(date)" >> build_report.txt
|
||||||
|
- echo "Branch: $DRONE_BRANCH" >> build_report.txt
|
||||||
|
- echo "Commit: $DRONE_COMMIT" >> build_report.txt
|
||||||
|
- echo "Author: $DRONE_COMMIT_AUTHOR" >> build_report.txt
|
||||||
|
- echo "" >> build_report.txt
|
||||||
|
- echo "APK Artifacts:" >> build_report.txt
|
||||||
|
- ls -lh app/build/outputs/apk/debug/ >> build_report.txt 2>/dev/null || echo "Debug APK not found" >> build_report.txt
|
||||||
|
- ls -lh app/build/outputs/apk/release/ >> build_report.txt 2>/dev/null || echo "Release APK not found" >> build_report.txt
|
||||||
|
- cat build_report.txt
|
||||||
|
when:
|
||||||
|
status:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy web files to nginx
|
||||||
|
COPY index.html /usr/share/nginx/html/
|
||||||
|
COPY styles.css /usr/share/nginx/html/
|
||||||
|
COPY script.js /usr/share/nginx/html/
|
||||||
|
COPY translations.js /usr/share/nginx/html/
|
||||||
|
COPY translations/ /usr/share/nginx/html/translations/
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
37
MathHomeworkHelper/.gitignore
vendored
Normal file
37
MathHomeworkHelper/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
*.apk
|
||||||
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Log/OS Files
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Android Studio generated files and folders
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Android Studio/IntelliJ
|
||||||
|
out/
|
||||||
|
.idea_modules/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# gradle wrapper
|
||||||
|
gradle/wrapper/gradle-wrapper.jar
|
||||||
360
MathHomeworkHelper/INDEX.md
Normal file
360
MathHomeworkHelper/INDEX.md
Normal file
|
|
@ -0,0 +1,360 @@
|
||||||
|
# Math Homework Helper - Android Project Index
|
||||||
|
|
||||||
|
## 📋 Project Overview
|
||||||
|
|
||||||
|
A complete Android WebView wrapper application for the Math Homework Helper web app. This project wraps your existing HTML/CSS/JavaScript application into a native Android app that can be installed on any Android device.
|
||||||
|
|
||||||
|
**Project Status**: ✅ **READY TO BUILD AND DEPLOY**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentation Files
|
||||||
|
|
||||||
|
Start here based on your needs:
|
||||||
|
|
||||||
|
### 1. **QUICKSTART.md** ⭐ START HERE
|
||||||
|
- **Purpose**: Get up and running in 5 minutes
|
||||||
|
- **Best for**: First-time users
|
||||||
|
- **Contains**: Step-by-step setup instructions
|
||||||
|
- **Time**: 5-10 minutes to complete
|
||||||
|
|
||||||
|
### 2. **INSTALLATION.md** 🔧 DETAILED SETUP
|
||||||
|
- **Purpose**: Complete installation guide with troubleshooting
|
||||||
|
- **Best for**: Detailed setup and configuration
|
||||||
|
- **Contains**: Prerequisites, step-by-step installation, advanced config
|
||||||
|
- **Time**: 30-60 minutes to complete
|
||||||
|
|
||||||
|
### 3. **README.md** 📖 FULL DOCUMENTATION
|
||||||
|
- **Purpose**: Comprehensive project documentation
|
||||||
|
- **Best for**: Understanding the project structure and features
|
||||||
|
- **Contains**: Architecture, features, permissions, troubleshooting
|
||||||
|
- **Time**: Reference document
|
||||||
|
|
||||||
|
### 4. **PROJECT_SUMMARY.md** 📊 PROJECT STATUS
|
||||||
|
- **Purpose**: Overview of what was created
|
||||||
|
- **Best for**: Understanding project components
|
||||||
|
- **Contains**: File structure, features, configuration details
|
||||||
|
- **Time**: 10-15 minutes to read
|
||||||
|
|
||||||
|
### 5. **INDEX.md** (This File) 🗂️ NAVIGATION
|
||||||
|
- **Purpose**: Navigate all project files and documentation
|
||||||
|
- **Best for**: Finding what you need
|
||||||
|
- **Contains**: File listing and descriptions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
MathHomeworkHelper/
|
||||||
|
│
|
||||||
|
├── 📄 Documentation Files
|
||||||
|
│ ├── INDEX.md ← You are here
|
||||||
|
│ ├── QUICKSTART.md ← Start here!
|
||||||
|
│ ├── INSTALLATION.md ← Detailed setup
|
||||||
|
│ ├── README.md ← Full documentation
|
||||||
|
│ └── PROJECT_SUMMARY.md ← Project overview
|
||||||
|
│
|
||||||
|
├── 🔧 Build Configuration
|
||||||
|
│ ├── build.gradle ← Project-level build config
|
||||||
|
│ ├── settings.gradle ← Gradle settings
|
||||||
|
│ ├── gradle.properties ← Gradle properties
|
||||||
|
│ └── .gitignore ← Git ignore rules
|
||||||
|
│
|
||||||
|
└── 📦 app/ (Main Application Module)
|
||||||
|
│
|
||||||
|
├── 🔨 build.gradle ← App-level build config
|
||||||
|
├── proguard-rules.pro ← ProGuard obfuscation rules
|
||||||
|
│
|
||||||
|
└── src/main/
|
||||||
|
│
|
||||||
|
├── 📱 java/
|
||||||
|
│ └── com/example/mathhomeworkhelper/
|
||||||
|
│ └── MainActivity.java ← Main activity with WebView
|
||||||
|
│
|
||||||
|
├── 🎨 res/ (Resources)
|
||||||
|
│ ├── layout/
|
||||||
|
│ │ └── activity_main.xml ← Main layout
|
||||||
|
│ ├── values/
|
||||||
|
│ │ ├── strings.xml ← App strings
|
||||||
|
│ │ └── styles.xml ← Theme & colors
|
||||||
|
│ └── mipmap/ ← App icons (add here)
|
||||||
|
│
|
||||||
|
├── 🌐 assets/ (Web App Files)
|
||||||
|
│ ├── index.html ← Main HTML file
|
||||||
|
│ ├── styles.css ← Stylesheets
|
||||||
|
│ ├── script.js ← JavaScript logic
|
||||||
|
│ ├── translations.js ← Translation system
|
||||||
|
│ └── translations/
|
||||||
|
│ ├── en.json ← English
|
||||||
|
│ ├── es.json ← Spanish
|
||||||
|
│ ├── sv.json ← Swedish
|
||||||
|
│ └── el.json ← Greek
|
||||||
|
│
|
||||||
|
└── AndroidManifest.xml ← App manifest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start Paths
|
||||||
|
|
||||||
|
### Path 1: I Just Want to Build and Run (5 minutes)
|
||||||
|
1. Read: **QUICKSTART.md**
|
||||||
|
2. Open project in Android Studio
|
||||||
|
3. Click Run button
|
||||||
|
4. Done! ✅
|
||||||
|
|
||||||
|
### Path 2: I Want to Understand Everything (30 minutes)
|
||||||
|
1. Read: **PROJECT_SUMMARY.md**
|
||||||
|
2. Read: **README.md**
|
||||||
|
3. Explore the file structure
|
||||||
|
4. Read: **INSTALLATION.md** for detailed setup
|
||||||
|
|
||||||
|
### Path 3: I'm Having Issues (Troubleshooting)
|
||||||
|
1. Check: **README.md** → Troubleshooting section
|
||||||
|
2. Check: **INSTALLATION.md** → Troubleshooting section
|
||||||
|
3. Check: Android Studio Logcat for error messages
|
||||||
|
4. Search: Android Developer Documentation
|
||||||
|
|
||||||
|
### Path 4: I Want to Customize the App (15 minutes)
|
||||||
|
1. Read: **PROJECT_SUMMARY.md** → Customization Options
|
||||||
|
2. Modify files as needed:
|
||||||
|
- App name: `app/src/main/res/values/strings.xml`
|
||||||
|
- App icon: `app/src/main/res/mipmap/`
|
||||||
|
- Theme colors: `app/src/main/res/values/styles.xml`
|
||||||
|
- WebView behavior: `app/src/main/java/.../MainActivity.java`
|
||||||
|
|
||||||
|
### Path 5: I Want to Build for Release (20 minutes)
|
||||||
|
1. Read: **INSTALLATION.md** → Building for Distribution
|
||||||
|
2. Follow steps to create signed APK
|
||||||
|
3. Upload to Google Play Store (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 File Descriptions
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
| File | Purpose | Read Time |
|
||||||
|
|------|---------|-----------|
|
||||||
|
| QUICKSTART.md | Get started in 5 minutes | 5 min |
|
||||||
|
| INSTALLATION.md | Complete setup guide | 30 min |
|
||||||
|
| README.md | Full documentation | 20 min |
|
||||||
|
| PROJECT_SUMMARY.md | Project overview | 10 min |
|
||||||
|
| INDEX.md | This file - navigation | 5 min |
|
||||||
|
|
||||||
|
### Build Configuration
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| build.gradle (root) | Project-level Gradle configuration |
|
||||||
|
| app/build.gradle | App-level Gradle configuration |
|
||||||
|
| settings.gradle | Gradle project settings |
|
||||||
|
| gradle.properties | Gradle properties and JVM settings |
|
||||||
|
| .gitignore | Git ignore rules |
|
||||||
|
|
||||||
|
### Source Code
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| MainActivity.java | Main activity with WebView setup |
|
||||||
|
| activity_main.xml | Main layout with WebView |
|
||||||
|
| AndroidManifest.xml | App manifest and permissions |
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| strings.xml | App strings and labels |
|
||||||
|
| styles.xml | App theme and colors |
|
||||||
|
| mipmap/ | App icons (add your icons here) |
|
||||||
|
|
||||||
|
### Web Assets
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| index.html | Main HTML file |
|
||||||
|
| styles.css | Stylesheets |
|
||||||
|
| script.js | JavaScript logic |
|
||||||
|
| translations.js | Translation system |
|
||||||
|
| translations/*.json | Language files |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Common Tasks
|
||||||
|
|
||||||
|
### Task: Build the App
|
||||||
|
```bash
|
||||||
|
cd MathHomeworkHelper
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
See: **INSTALLATION.md** → Step 6
|
||||||
|
|
||||||
|
### Task: Run on Emulator
|
||||||
|
1. Start Android emulator
|
||||||
|
2. Click Run button in Android Studio
|
||||||
|
3. Select emulator
|
||||||
|
See: **QUICKSTART.md** → Step 3
|
||||||
|
|
||||||
|
### Task: Run on Physical Device
|
||||||
|
1. Enable USB Debugging on device
|
||||||
|
2. Connect via USB
|
||||||
|
3. Click Run button in Android Studio
|
||||||
|
See: **INSTALLATION.md** → Step 7
|
||||||
|
|
||||||
|
### Task: Change App Name
|
||||||
|
Edit: `app/src/main/res/values/strings.xml`
|
||||||
|
```xml
|
||||||
|
<string name="app_name">New App Name</string>
|
||||||
|
```
|
||||||
|
See: **PROJECT_SUMMARY.md** → Customization
|
||||||
|
|
||||||
|
### Task: Change App Icon
|
||||||
|
Replace files in: `app/src/main/res/mipmap/`
|
||||||
|
See: **PROJECT_SUMMARY.md** → Customization
|
||||||
|
|
||||||
|
### Task: Change Theme Colors
|
||||||
|
Edit: `app/src/main/res/values/styles.xml`
|
||||||
|
See: **PROJECT_SUMMARY.md** → Customization
|
||||||
|
|
||||||
|
### Task: Build Release APK
|
||||||
|
```bash
|
||||||
|
./gradlew assembleRelease
|
||||||
|
```
|
||||||
|
See: **INSTALLATION.md** → Building for Distribution
|
||||||
|
|
||||||
|
### Task: Debug Issues
|
||||||
|
1. Check Logcat in Android Studio
|
||||||
|
2. Read troubleshooting sections
|
||||||
|
3. Consult Android documentation
|
||||||
|
See: **README.md** → Troubleshooting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Key Information
|
||||||
|
|
||||||
|
### Project Details
|
||||||
|
- **Type**: Native Android WebView Wrapper
|
||||||
|
- **Min SDK**: 21 (Android 5.0)
|
||||||
|
- **Target SDK**: 34 (Android 14)
|
||||||
|
- **Language**: Java
|
||||||
|
- **Build System**: Gradle
|
||||||
|
- **Package**: com.example.mathhomeworkhelper
|
||||||
|
|
||||||
|
### Features
|
||||||
|
✅ WebView with HTML content
|
||||||
|
✅ JavaScript enabled
|
||||||
|
✅ Local storage support
|
||||||
|
✅ Multi-language support
|
||||||
|
✅ Zoom controls
|
||||||
|
✅ Back button navigation
|
||||||
|
✅ Responsive layout
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
- INTERNET (required for WebView)
|
||||||
|
- ACCESS_NETWORK_STATE (network status)
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- androidx.appcompat:appcompat:1.6.1
|
||||||
|
- com.google.android.material:material:1.10.0
|
||||||
|
- androidx.constraintlayout:constraintlayout:2.1.4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Getting Help
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
1. **QUICKSTART.md** - For quick setup
|
||||||
|
2. **INSTALLATION.md** - For detailed setup
|
||||||
|
3. **README.md** - For comprehensive info
|
||||||
|
4. **PROJECT_SUMMARY.md** - For project overview
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
1. Check Logcat in Android Studio
|
||||||
|
2. Review troubleshooting sections in docs
|
||||||
|
3. Check Android Developer Documentation
|
||||||
|
4. Search Stack Overflow
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
- [Android Developer Docs](https://developer.android.com/docs)
|
||||||
|
- [WebView Guide](https://developer.android.com/guide/webapps/webview)
|
||||||
|
- [Gradle Documentation](https://gradle.org/)
|
||||||
|
- [Android Studio Help](https://developer.android.com/studio/intro)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Verification Checklist
|
||||||
|
|
||||||
|
Before you start, verify:
|
||||||
|
|
||||||
|
- [ ] Android Studio installed
|
||||||
|
- [ ] Android SDK 21+ installed
|
||||||
|
- [ ] Java 11+ installed
|
||||||
|
- [ ] 5GB+ free disk space
|
||||||
|
- [ ] Project folder accessible
|
||||||
|
- [ ] All files present (23 files total)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Project Statistics
|
||||||
|
|
||||||
|
- **Total Files**: 23
|
||||||
|
- **Project Size**: 144 KB
|
||||||
|
- **Documentation Files**: 5
|
||||||
|
- **Source Files**: 1 (MainActivity.java)
|
||||||
|
- **Layout Files**: 1 (activity_main.xml)
|
||||||
|
- **Resource Files**: 2 (strings.xml, styles.xml)
|
||||||
|
- **Configuration Files**: 4 (build.gradle, settings.gradle, etc.)
|
||||||
|
- **Web Assets**: 9 (HTML, CSS, JS, translations)
|
||||||
|
- **Build System**: Gradle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 Learning Path
|
||||||
|
|
||||||
|
### Beginner
|
||||||
|
1. Read QUICKSTART.md
|
||||||
|
2. Build and run the app
|
||||||
|
3. Test features
|
||||||
|
4. Celebrate! 🎉
|
||||||
|
|
||||||
|
### Intermediate
|
||||||
|
1. Read PROJECT_SUMMARY.md
|
||||||
|
2. Read README.md
|
||||||
|
3. Customize app (name, icon, colors)
|
||||||
|
4. Build release APK
|
||||||
|
|
||||||
|
### Advanced
|
||||||
|
1. Read INSTALLATION.md
|
||||||
|
2. Modify MainActivity.java
|
||||||
|
3. Add native Android features
|
||||||
|
4. Publish to Google Play Store
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
|
||||||
|
1. **Start Here**: Read **QUICKSTART.md**
|
||||||
|
2. **Open Project**: Launch Android Studio
|
||||||
|
3. **Build App**: Click Build button
|
||||||
|
4. **Run App**: Click Run button
|
||||||
|
5. **Test Features**: Verify everything works
|
||||||
|
6. **Customize**: Modify as needed
|
||||||
|
7. **Deploy**: Build release APK
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- All HTML files are in `app/src/main/assets/`
|
||||||
|
- All translations are included (EN, ES, SV, EL)
|
||||||
|
- JavaScript is enabled in WebView
|
||||||
|
- Back button navigation is supported
|
||||||
|
- App is ready to build immediately
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: November 25, 2025
|
||||||
|
**Project Status**: ✅ Ready to Build and Deploy
|
||||||
|
**Total Setup Time**: 5-30 minutes (depending on path)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy Coding! 🚀**
|
||||||
|
|
||||||
|
For questions or issues, refer to the appropriate documentation file above.
|
||||||
389
MathHomeworkHelper/INSTALLATION.md
Normal file
389
MathHomeworkHelper/INSTALLATION.md
Normal file
|
|
@ -0,0 +1,389 @@
|
||||||
|
# Installation & Setup Guide
|
||||||
|
|
||||||
|
## Complete Setup Instructions for Math Homework Helper Android App
|
||||||
|
|
||||||
|
### Prerequisites Checklist
|
||||||
|
|
||||||
|
Before you begin, ensure you have:
|
||||||
|
|
||||||
|
- [ ] **Android Studio** (latest version) - [Download here](https://developer.android.com/studio)
|
||||||
|
- [ ] **Java Development Kit (JDK) 11+** - Usually included with Android Studio
|
||||||
|
- [ ] **Android SDK** - Installed via Android Studio SDK Manager
|
||||||
|
- [ ] **Minimum 5GB free disk space** - For SDK and emulator
|
||||||
|
- [ ] **Git** (optional) - For version control
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Install Android Studio
|
||||||
|
|
||||||
|
### Windows/Mac/Linux
|
||||||
|
|
||||||
|
1. Download Android Studio from https://developer.android.com/studio
|
||||||
|
2. Run the installer and follow the setup wizard
|
||||||
|
3. Choose "Standard" installation (recommended)
|
||||||
|
4. Accept the default settings
|
||||||
|
5. Complete the installation
|
||||||
|
|
||||||
|
### Verify Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if Android Studio is installed
|
||||||
|
which android-studio # macOS/Linux
|
||||||
|
# or look for Android Studio in Applications/Programs
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Set Up Android SDK
|
||||||
|
|
||||||
|
### Using Android Studio
|
||||||
|
|
||||||
|
1. Open Android Studio
|
||||||
|
2. Go to **Tools** → **SDK Manager**
|
||||||
|
3. Under "SDK Platforms" tab:
|
||||||
|
- ✅ Check "Android 14 (API 34)" - **Required**
|
||||||
|
- ✅ Check "Android 5.0 (API 21)" - **Minimum**
|
||||||
|
- ✅ Check "Android 12 (API 31)" - **Recommended**
|
||||||
|
4. Under "SDK Tools" tab:
|
||||||
|
- ✅ Check "Android SDK Build-Tools 34.x.x"
|
||||||
|
- ✅ Check "Android Emulator"
|
||||||
|
- ✅ Check "Android SDK Platform-Tools"
|
||||||
|
5. Click "Apply" and wait for downloads to complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Create Android Virtual Device (Emulator)
|
||||||
|
|
||||||
|
### Option A: Using Android Studio GUI
|
||||||
|
|
||||||
|
1. Open Android Studio
|
||||||
|
2. Go to **Tools** → **Device Manager**
|
||||||
|
3. Click **"Create Device"**
|
||||||
|
4. Select a device (e.g., "Pixel 4" or "Pixel 5")
|
||||||
|
5. Click **"Next"**
|
||||||
|
6. Select API level (API 30 or higher recommended)
|
||||||
|
7. Click **"Next"**
|
||||||
|
8. Review settings and click **"Finish"**
|
||||||
|
9. Click the play button to start the emulator
|
||||||
|
|
||||||
|
### Option B: Using Command Line
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List available devices
|
||||||
|
emulator -list-avds
|
||||||
|
|
||||||
|
# Create a new AVD
|
||||||
|
avdmanager create avd -n "Pixel4" -k "system-images;android;30;google_apis"
|
||||||
|
|
||||||
|
# Start the emulator
|
||||||
|
emulator -avd Pixel4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4: Open the Project
|
||||||
|
|
||||||
|
### Method 1: Using Android Studio (Recommended)
|
||||||
|
|
||||||
|
1. Launch **Android Studio**
|
||||||
|
2. Click **"Open an existing Android Studio project"**
|
||||||
|
3. Navigate to the **MathHomeworkHelper** folder
|
||||||
|
4. Click **"Open"**
|
||||||
|
5. Wait for Gradle to sync (first time takes 2-5 minutes)
|
||||||
|
6. If prompted, click **"Trust Project"**
|
||||||
|
|
||||||
|
### Method 2: Using Command Line
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to project directory
|
||||||
|
cd /path/to/MathHomeworkHelper
|
||||||
|
|
||||||
|
# Open in Android Studio
|
||||||
|
open -a "Android Studio" . # macOS
|
||||||
|
# or
|
||||||
|
studio . # Linux
|
||||||
|
# or
|
||||||
|
start . # Windows (if Android Studio is in PATH)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Configure Project
|
||||||
|
|
||||||
|
### Gradle Sync
|
||||||
|
|
||||||
|
After opening the project:
|
||||||
|
|
||||||
|
1. Android Studio will automatically start Gradle sync
|
||||||
|
2. Wait for the sync to complete (check bottom status bar)
|
||||||
|
3. If sync fails:
|
||||||
|
- Click **File** → **Sync Now**
|
||||||
|
- Or **File** → **Invalidate Caches** → **Invalidate and Restart**
|
||||||
|
|
||||||
|
### Verify Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From project root directory
|
||||||
|
./gradlew --version # Should show Gradle version
|
||||||
|
./gradlew tasks # List available tasks
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 6: Build the Project
|
||||||
|
|
||||||
|
### Using Android Studio
|
||||||
|
|
||||||
|
1. Click **Build** → **Build Bundle(s) / APK(s)** → **Build APK(s)**
|
||||||
|
2. Wait for build to complete
|
||||||
|
3. You'll see a notification when build succeeds
|
||||||
|
|
||||||
|
### Using Command Line
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd MathHomeworkHelper
|
||||||
|
|
||||||
|
# Build debug APK
|
||||||
|
./gradlew assembleDebug
|
||||||
|
|
||||||
|
# Build release APK
|
||||||
|
./gradlew assembleRelease
|
||||||
|
|
||||||
|
# Clean and rebuild
|
||||||
|
./gradlew clean build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 7: Run the App
|
||||||
|
|
||||||
|
### On Emulator
|
||||||
|
|
||||||
|
1. Start the Android emulator (if not already running)
|
||||||
|
2. In Android Studio, click the green **Run** button (or press **Shift+F10**)
|
||||||
|
3. Select your emulator from the device list
|
||||||
|
4. Click **OK**
|
||||||
|
5. Wait for app to install and launch
|
||||||
|
|
||||||
|
### On Physical Device
|
||||||
|
|
||||||
|
1. **Enable Developer Mode:**
|
||||||
|
- Go to **Settings** → **About Phone**
|
||||||
|
- Tap **Build Number** 7 times
|
||||||
|
- Go back to **Settings** → **Developer Options**
|
||||||
|
- Enable **USB Debugging**
|
||||||
|
|
||||||
|
2. **Connect Device:**
|
||||||
|
- Connect Android device via USB cable
|
||||||
|
- Allow USB debugging when prompted on device
|
||||||
|
- Android Studio will detect the device
|
||||||
|
|
||||||
|
3. **Run App:**
|
||||||
|
- Click green **Run** button in Android Studio
|
||||||
|
- Select your device from the list
|
||||||
|
- Click **OK**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 8: Verify Installation
|
||||||
|
|
||||||
|
### Check App is Running
|
||||||
|
|
||||||
|
1. You should see the Math Homework Helper interface
|
||||||
|
2. Test the following features:
|
||||||
|
- ✅ Difficulty slider works
|
||||||
|
- ✅ Math problems display
|
||||||
|
- ✅ Answer input works
|
||||||
|
- ✅ Check Answer button functions
|
||||||
|
- ✅ Language selector works
|
||||||
|
- ✅ Back button navigates correctly
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| Gradle sync fails | File → Invalidate Caches → Invalidate and Restart |
|
||||||
|
| Emulator won't start | Check virtualization enabled in BIOS; try different API level |
|
||||||
|
| App crashes on launch | Check Logcat (View → Tool Windows → Logcat) for errors |
|
||||||
|
| WebView shows blank | Verify assets in app/src/main/assets/ |
|
||||||
|
| JavaScript not working | Check MainActivity.java has setJavaScriptEnabled(true) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Configuration
|
||||||
|
|
||||||
|
### Change Minimum SDK Version
|
||||||
|
|
||||||
|
Edit `app/build.gradle`:
|
||||||
|
```gradle
|
||||||
|
defaultConfig {
|
||||||
|
minSdk 21 // Change this value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change Target SDK Version
|
||||||
|
|
||||||
|
Edit `app/build.gradle`:
|
||||||
|
```gradle
|
||||||
|
android {
|
||||||
|
targetSdk 34 // Change this value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enable ProGuard/R8 for Release
|
||||||
|
|
||||||
|
Edit `app/build.gradle`:
|
||||||
|
```gradle
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled true // Enable code shrinking
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building for Distribution
|
||||||
|
|
||||||
|
### Create Signed Release APK
|
||||||
|
|
||||||
|
1. In Android Studio: **Build** → **Generate Signed Bundle / APK**
|
||||||
|
2. Select **APK**
|
||||||
|
3. Click **Next**
|
||||||
|
4. **Create new keystore** (or select existing):
|
||||||
|
- Key store path: Choose location
|
||||||
|
- Password: Create strong password
|
||||||
|
- Key alias: e.g., "release"
|
||||||
|
- Key password: Same as keystore or different
|
||||||
|
- Validity: 25+ years recommended
|
||||||
|
5. Click **Next**
|
||||||
|
6. Select **release** build type
|
||||||
|
7. Click **Finish**
|
||||||
|
8. APK will be generated in `app/build/outputs/apk/release/`
|
||||||
|
|
||||||
|
### Upload to Google Play Store
|
||||||
|
|
||||||
|
1. Create Google Play Developer account ($25 one-time fee)
|
||||||
|
2. Create new app in Play Console
|
||||||
|
3. Fill in app details (description, screenshots, etc.)
|
||||||
|
4. Upload signed APK
|
||||||
|
5. Set pricing and distribution
|
||||||
|
6. Submit for review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Gradle Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clear Gradle cache
|
||||||
|
./gradlew clean
|
||||||
|
|
||||||
|
# Rebuild with verbose output
|
||||||
|
./gradlew build --info
|
||||||
|
|
||||||
|
# Update Gradle wrapper
|
||||||
|
./gradlew wrapper --gradle-version=latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Emulator Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List running emulators
|
||||||
|
emulator -list-avds
|
||||||
|
|
||||||
|
# Kill all emulator processes
|
||||||
|
pkill -f emulator
|
||||||
|
|
||||||
|
# Start emulator with more RAM
|
||||||
|
emulator -avd Pixel4 -memory 2048
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Java version
|
||||||
|
java -version
|
||||||
|
|
||||||
|
# Check Android SDK
|
||||||
|
$ANDROID_HOME/tools/bin/sdkmanager --list
|
||||||
|
|
||||||
|
# Invalidate Android Studio cache
|
||||||
|
rm -rf ~/.android/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables (Optional)
|
||||||
|
|
||||||
|
### Set ANDROID_HOME
|
||||||
|
|
||||||
|
**macOS/Linux:**
|
||||||
|
```bash
|
||||||
|
export ANDROID_HOME=$HOME/Library/Android/sdk # macOS
|
||||||
|
export ANDROID_HOME=$HOME/Android/Sdk # Linux
|
||||||
|
export PATH=$PATH:$ANDROID_HOME/tools
|
||||||
|
export PATH=$PATH:$ANDROID_HOME/platform-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```cmd
|
||||||
|
setx ANDROID_HOME "C:\Users\YourUsername\AppData\Local\Android\Sdk"
|
||||||
|
setx PATH "%PATH%;%ANDROID_HOME%\tools;%ANDROID_HOME%\platform-tools"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. ✅ Complete installation
|
||||||
|
2. ✅ Build and run the app
|
||||||
|
3. ✅ Test all features
|
||||||
|
4. ✅ Customize app (name, icon, colors)
|
||||||
|
5. ✅ Build release APK
|
||||||
|
6. ✅ Publish to Google Play Store (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support & Resources
|
||||||
|
|
||||||
|
- **Android Studio Help**: https://developer.android.com/studio/intro
|
||||||
|
- **Android Developer Docs**: https://developer.android.com/docs
|
||||||
|
- **WebView Guide**: https://developer.android.com/guide/webapps/webview
|
||||||
|
- **Gradle Documentation**: https://gradle.org/
|
||||||
|
- **Stack Overflow**: Tag with `android` and `android-studio`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to project
|
||||||
|
cd MathHomeworkHelper
|
||||||
|
|
||||||
|
# Build debug APK
|
||||||
|
./gradlew assembleDebug
|
||||||
|
|
||||||
|
# Build release APK
|
||||||
|
./gradlew assembleRelease
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
./gradlew test
|
||||||
|
|
||||||
|
# Clean build
|
||||||
|
./gradlew clean
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
./gradlew dependencies
|
||||||
|
|
||||||
|
# Update Gradle
|
||||||
|
./gradlew wrapper --gradle-version=latest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Installation Complete!** 🎉
|
||||||
|
|
||||||
|
Your Android project is now ready to build and deploy. Start with the Quick Start Guide (QUICKSTART.md) for next steps.
|
||||||
267
MathHomeworkHelper/PROJECT_SUMMARY.md
Normal file
267
MathHomeworkHelper/PROJECT_SUMMARY.md
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
# Android Project Summary - Math Homework Helper
|
||||||
|
|
||||||
|
## ✅ Project Successfully Created!
|
||||||
|
|
||||||
|
Your Android project has been successfully created and is ready to build and deploy. This is a native Android WebView wrapper that displays your Math Homework Helper HTML application.
|
||||||
|
|
||||||
|
## 📁 Complete Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
MathHomeworkHelper/
|
||||||
|
├── app/
|
||||||
|
│ ├── src/main/
|
||||||
|
│ │ ├── java/com/example/mathhomeworkhelper/
|
||||||
|
│ │ │ └── MainActivity.java ✅ Main activity with WebView
|
||||||
|
│ │ ├── res/
|
||||||
|
│ │ │ ├── layout/
|
||||||
|
│ │ │ │ └── activity_main.xml ✅ WebView layout
|
||||||
|
│ │ │ ├── values/
|
||||||
|
│ │ │ │ ├── strings.xml ✅ App strings
|
||||||
|
│ │ │ │ └── styles.xml ✅ App theme & colors
|
||||||
|
│ │ │ └── mipmap/ 📁 (for app icons)
|
||||||
|
│ │ ├── assets/
|
||||||
|
│ │ │ ├── index.html ✅ Main HTML file
|
||||||
|
│ │ │ ├── styles.css ✅ Stylesheets
|
||||||
|
│ │ │ ├── script.js ✅ JavaScript logic
|
||||||
|
│ │ │ ├── translations.js ✅ Translation logic
|
||||||
|
│ │ │ └── translations/
|
||||||
|
│ │ │ ├── en.json ✅ English translations
|
||||||
|
│ │ │ ├── es.json ✅ Spanish translations
|
||||||
|
│ │ │ ├── sv.json ✅ Swedish translations
|
||||||
|
│ │ │ └── el.json ✅ Greek translations
|
||||||
|
│ │ └── AndroidManifest.xml ✅ App manifest
|
||||||
|
│ ├── build.gradle ✅ App build config
|
||||||
|
│ └── proguard-rules.pro ✅ ProGuard rules
|
||||||
|
├── build.gradle ✅ Project build config
|
||||||
|
├── settings.gradle ✅ Gradle settings
|
||||||
|
├── gradle.properties ✅ Gradle properties
|
||||||
|
├── .gitignore ✅ Git ignore rules
|
||||||
|
├── README.md ✅ Full documentation
|
||||||
|
├── QUICKSTART.md ✅ Quick start guide
|
||||||
|
└── PROJECT_SUMMARY.md ✅ This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 What Was Created
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
1. **MainActivity.java**
|
||||||
|
- Initializes WebView with proper settings
|
||||||
|
- Enables JavaScript for interactive features
|
||||||
|
- Configures DOM storage and local storage
|
||||||
|
- Handles back button navigation
|
||||||
|
- Loads HTML from assets folder
|
||||||
|
|
||||||
|
2. **Activity Layout (activity_main.xml)**
|
||||||
|
- Simple LinearLayout with WebView
|
||||||
|
- WebView fills entire screen
|
||||||
|
|
||||||
|
3. **Android Manifest**
|
||||||
|
- Declares MainActivity as launcher activity
|
||||||
|
- Requests INTERNET permission
|
||||||
|
- Configures app theme and properties
|
||||||
|
|
||||||
|
4. **Build Configuration**
|
||||||
|
- Gradle build files for project and app
|
||||||
|
- Targets Android SDK 21-34
|
||||||
|
- Includes necessary dependencies
|
||||||
|
|
||||||
|
5. **Resources**
|
||||||
|
- App strings and theme colors
|
||||||
|
- ProGuard rules for code obfuscation
|
||||||
|
|
||||||
|
6. **Web Assets**
|
||||||
|
- All HTML, CSS, and JavaScript files copied
|
||||||
|
- All translation files included
|
||||||
|
- Ready to display in WebView
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Option 1: Using Android Studio (Recommended)
|
||||||
|
```bash
|
||||||
|
1. Open Android Studio
|
||||||
|
2. File → Open → Select MathHomeworkHelper folder
|
||||||
|
3. Wait for Gradle sync
|
||||||
|
4. Click Run (green play button)
|
||||||
|
5. Select emulator or device
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Using Command Line
|
||||||
|
```bash
|
||||||
|
cd MathHomeworkHelper
|
||||||
|
./gradlew build # Build the project
|
||||||
|
./gradlew assembleDebug # Create debug APK
|
||||||
|
./gradlew assembleRelease # Create release APK
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Key Features Implemented
|
||||||
|
|
||||||
|
✅ **WebView Integration**
|
||||||
|
- Loads local HTML files from assets
|
||||||
|
- JavaScript enabled for interactivity
|
||||||
|
- DOM storage for data persistence
|
||||||
|
- Zoom controls available
|
||||||
|
|
||||||
|
✅ **File Access**
|
||||||
|
- All HTML/CSS/JS files in assets folder
|
||||||
|
- Translation files included
|
||||||
|
- Proper file permissions configured
|
||||||
|
|
||||||
|
✅ **User Experience**
|
||||||
|
- Back button navigation support
|
||||||
|
- Responsive layout
|
||||||
|
- Theme colors configured
|
||||||
|
- Multi-language support preserved
|
||||||
|
|
||||||
|
✅ **Build Configuration**
|
||||||
|
- Modern Gradle setup
|
||||||
|
- AndroidX support enabled
|
||||||
|
- Proper SDK versions configured
|
||||||
|
- ProGuard rules for release builds
|
||||||
|
|
||||||
|
## 🔧 Configuration Details
|
||||||
|
|
||||||
|
### Android Versions
|
||||||
|
- **Minimum SDK**: 21 (Android 5.0)
|
||||||
|
- **Target SDK**: 34 (Android 14)
|
||||||
|
- **Compile SDK**: 34
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
- `INTERNET` - Required for WebView
|
||||||
|
- `ACCESS_NETWORK_STATE` - Network status checking
|
||||||
|
|
||||||
|
### WebView Settings
|
||||||
|
- JavaScript: ✅ Enabled
|
||||||
|
- DOM Storage: ✅ Enabled
|
||||||
|
- Database: ✅ Enabled
|
||||||
|
- File Access: ✅ Enabled
|
||||||
|
- Zoom Controls: ✅ Enabled
|
||||||
|
- Mixed Content: ✅ Allowed (Android 5.0+)
|
||||||
|
|
||||||
|
## 📦 Dependencies
|
||||||
|
|
||||||
|
The project uses minimal dependencies:
|
||||||
|
- `androidx.appcompat:appcompat:1.6.1`
|
||||||
|
- `com.google.android.material:material:1.10.0`
|
||||||
|
- `androidx.constraintlayout:constraintlayout:2.1.4`
|
||||||
|
|
||||||
|
## 🎨 Customization Options
|
||||||
|
|
||||||
|
### Change App Name
|
||||||
|
Edit: `app/src/main/res/values/strings.xml`
|
||||||
|
```xml
|
||||||
|
<string name="app_name">Your App Name</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change App Icon
|
||||||
|
Replace files in: `app/src/main/res/mipmap/`
|
||||||
|
- `ic_launcher.png` (108x108 dp)
|
||||||
|
- `ic_launcher_round.png` (108x108 dp)
|
||||||
|
|
||||||
|
### Change Theme Colors
|
||||||
|
Edit: `app/src/main/res/values/styles.xml`
|
||||||
|
```xml
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<!-- Change these color values -->
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modify WebView Behavior
|
||||||
|
Edit: `app/src/main/java/com/example/mathhomeworkhelper/MainActivity.java`
|
||||||
|
|
||||||
|
## 📱 Building for Distribution
|
||||||
|
|
||||||
|
### Debug APK (Testing)
|
||||||
|
```bash
|
||||||
|
./gradlew assembleDebug
|
||||||
|
# Output: app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release APK (Distribution)
|
||||||
|
```bash
|
||||||
|
./gradlew assembleRelease
|
||||||
|
# Output: app/build/outputs/apk/release/app-release.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signed Release APK
|
||||||
|
1. In Android Studio: Build → Generate Signed Bundle/APK
|
||||||
|
2. Select APK
|
||||||
|
3. Create or select keystore
|
||||||
|
4. Fill signing information
|
||||||
|
5. Select release build type
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Issue: Gradle Sync Fails
|
||||||
|
**Solution**:
|
||||||
|
- File → Sync Now
|
||||||
|
- File → Invalidate Caches → Invalidate and Restart
|
||||||
|
|
||||||
|
### Issue: WebView Shows Blank Screen
|
||||||
|
**Solution**:
|
||||||
|
- Verify files in `app/src/main/assets/`
|
||||||
|
- Check MainActivity.java loadUrl path
|
||||||
|
- Enable JavaScript in WebView settings
|
||||||
|
|
||||||
|
### Issue: App Crashes on Launch
|
||||||
|
**Solution**:
|
||||||
|
- Check Logcat for error messages
|
||||||
|
- Verify AndroidManifest.xml syntax
|
||||||
|
- Ensure all dependencies are installed
|
||||||
|
|
||||||
|
### Issue: JavaScript Not Working
|
||||||
|
**Solution**:
|
||||||
|
- Confirm `setJavaScriptEnabled(true)` in MainActivity
|
||||||
|
- Check browser console for JS errors
|
||||||
|
- Verify file paths in HTML
|
||||||
|
|
||||||
|
## 📚 Documentation Files
|
||||||
|
|
||||||
|
- **README.md** - Complete project documentation
|
||||||
|
- **QUICKSTART.md** - Step-by-step getting started guide
|
||||||
|
- **PROJECT_SUMMARY.md** - This file
|
||||||
|
|
||||||
|
## 🔗 Useful Resources
|
||||||
|
|
||||||
|
- [Android Developer Documentation](https://developer.android.com/docs)
|
||||||
|
- [WebView Guide](https://developer.android.com/guide/webapps/webview)
|
||||||
|
- [Gradle Documentation](https://gradle.org/)
|
||||||
|
- [Android Studio Help](https://developer.android.com/studio/intro)
|
||||||
|
|
||||||
|
## ✨ Next Steps
|
||||||
|
|
||||||
|
1. **Open in Android Studio**
|
||||||
|
- File → Open → Select MathHomeworkHelper folder
|
||||||
|
|
||||||
|
2. **Set Up Emulator or Device**
|
||||||
|
- Create Android Virtual Device (AVD)
|
||||||
|
- Or connect physical device with USB debugging
|
||||||
|
|
||||||
|
3. **Build and Run**
|
||||||
|
- Click Run button or press Shift+F10
|
||||||
|
- Select target device
|
||||||
|
|
||||||
|
4. **Test the App**
|
||||||
|
- Verify HTML content displays
|
||||||
|
- Test all interactive features
|
||||||
|
- Check language switching
|
||||||
|
|
||||||
|
5. **Customize (Optional)**
|
||||||
|
- Change app name and icon
|
||||||
|
- Modify theme colors
|
||||||
|
- Add additional features
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
1. Check the troubleshooting section above
|
||||||
|
2. Review the README.md and QUICKSTART.md files
|
||||||
|
3. Check Android Studio Logcat for error messages
|
||||||
|
4. Consult Android Developer Documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Project Status**: ✅ Ready to Build and Deploy
|
||||||
|
|
||||||
|
Your Android project is complete and ready to be opened in Android Studio. All necessary files have been created and configured. Simply open the project in Android Studio and click Run to get started!
|
||||||
|
|
||||||
|
Happy coding! 🚀
|
||||||
114
MathHomeworkHelper/QUICKSTART.md
Normal file
114
MathHomeworkHelper/QUICKSTART.md
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
# Quick Start Guide
|
||||||
|
|
||||||
|
## Getting Started with Math Homework Helper Android App
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Android Studio installed (download from https://developer.android.com/studio)
|
||||||
|
- Android SDK 21+ installed
|
||||||
|
- Java 11+ installed
|
||||||
|
|
||||||
|
### Step 1: Open the Project
|
||||||
|
1. Launch Android Studio
|
||||||
|
2. Select "Open an existing Android Studio project"
|
||||||
|
3. Navigate to and select the `MathHomeworkHelper` folder
|
||||||
|
4. Wait for Gradle to sync (this may take a few minutes on first load)
|
||||||
|
|
||||||
|
### Step 2: Set Up an Emulator (or use a physical device)
|
||||||
|
|
||||||
|
**Option A: Using Android Emulator**
|
||||||
|
1. In Android Studio, go to Tools → Device Manager
|
||||||
|
2. Click "Create Device"
|
||||||
|
3. Select a device (e.g., Pixel 4)
|
||||||
|
4. Select an API level (API 30 or higher recommended)
|
||||||
|
5. Click "Finish"
|
||||||
|
6. Click the play button to start the emulator
|
||||||
|
|
||||||
|
**Option B: Using Physical Device**
|
||||||
|
1. Enable Developer Mode on your Android device:
|
||||||
|
- Go to Settings → About Phone
|
||||||
|
- Tap "Build Number" 7 times
|
||||||
|
- Go back to Settings → Developer Options
|
||||||
|
- Enable "USB Debugging"
|
||||||
|
2. Connect your device via USB cable
|
||||||
|
3. Android Studio will detect it automatically
|
||||||
|
|
||||||
|
### Step 3: Build and Run
|
||||||
|
1. Click the green "Run" button (or press Shift+F10)
|
||||||
|
2. Select your emulator or device
|
||||||
|
3. Click "OK"
|
||||||
|
4. Wait for the app to build and install (first build takes longer)
|
||||||
|
|
||||||
|
### Step 4: Test the App
|
||||||
|
Once the app launches:
|
||||||
|
- You should see the Math Homework Helper interface
|
||||||
|
- Try selecting a difficulty level
|
||||||
|
- Solve a multiplication problem
|
||||||
|
- Click "Check Answer" to verify
|
||||||
|
- Use the language selector to change languages
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Gradle Sync Issues
|
||||||
|
- Click "File" → "Sync Now"
|
||||||
|
- If that doesn't work, try "File" → "Invalidate Caches" → "Invalidate and Restart"
|
||||||
|
|
||||||
|
### Emulator Won't Start
|
||||||
|
- Make sure you have enough disk space (at least 5GB)
|
||||||
|
- Try creating a new AVD with a different API level
|
||||||
|
- Check that virtualization is enabled in BIOS (for Windows/Linux)
|
||||||
|
|
||||||
|
### App Crashes on Launch
|
||||||
|
- Check the Logcat (View → Tool Windows → Logcat)
|
||||||
|
- Look for red error messages
|
||||||
|
- Common issues:
|
||||||
|
- Missing assets (check app/src/main/assets/)
|
||||||
|
- JavaScript errors (check browser console in WebView)
|
||||||
|
|
||||||
|
### WebView Not Displaying Content
|
||||||
|
- Ensure all HTML files are in `app/src/main/assets/`
|
||||||
|
- Check that the file path in MainActivity matches: `file:///android_asset/index.html`
|
||||||
|
- Verify JavaScript is enabled in WebView settings
|
||||||
|
|
||||||
|
## Project Structure Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
MathHomeworkHelper/
|
||||||
|
├── app/ # Main app module
|
||||||
|
│ ├── src/main/
|
||||||
|
│ │ ├── java/ # Java source code
|
||||||
|
│ │ ├── res/ # Android resources (layouts, strings, styles)
|
||||||
|
│ │ ├── assets/ # Web app files (HTML, CSS, JS)
|
||||||
|
│ │ └── AndroidManifest.xml # App configuration
|
||||||
|
│ └── build.gradle # App build configuration
|
||||||
|
├── build.gradle # Project build configuration
|
||||||
|
└── settings.gradle # Gradle settings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building for Distribution
|
||||||
|
|
||||||
|
### Debug APK (for testing)
|
||||||
|
```bash
|
||||||
|
./gradlew assembleDebug
|
||||||
|
# Output: app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release APK (for distribution)
|
||||||
|
```bash
|
||||||
|
./gradlew assembleRelease
|
||||||
|
# Output: app/build/outputs/apk/release/app-release.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Customize the app icon (replace files in `app/src/main/res/mipmap/`)
|
||||||
|
- Modify the app name in `app/src/main/res/values/strings.xml`
|
||||||
|
- Update the theme colors in `app/src/main/res/values/styles.xml`
|
||||||
|
- Add more features by modifying `MainActivity.java`
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
- Android Documentation: https://developer.android.com/docs
|
||||||
|
- WebView Guide: https://developer.android.com/guide/webapps/webview
|
||||||
|
- Gradle Documentation: https://gradle.org/
|
||||||
|
|
||||||
|
Happy coding! 🚀
|
||||||
143
MathHomeworkHelper/README.md
Normal file
143
MathHomeworkHelper/README.md
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
# Math Homework Helper - Android App
|
||||||
|
|
||||||
|
This is an Android wrapper application that displays the Math Homework Helper web application using a WebView component.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
MathHomeworkHelper/
|
||||||
|
├── app/
|
||||||
|
│ ├── src/main/
|
||||||
|
│ │ ├── java/com/example/mathhomeworkhelper/
|
||||||
|
│ │ │ └── MainActivity.java # Main activity with WebView
|
||||||
|
│ │ ├── res/
|
||||||
|
│ │ │ ├── layout/
|
||||||
|
│ │ │ │ └── activity_main.xml # Main layout with WebView
|
||||||
|
│ │ │ ├── values/
|
||||||
|
│ │ │ │ ├── strings.xml # String resources
|
||||||
|
│ │ │ │ └── styles.xml # App theme and colors
|
||||||
|
│ │ │ └── mipmap/ # App icons
|
||||||
|
│ │ ├── assets/
|
||||||
|
│ │ │ ├── index.html # Main HTML file
|
||||||
|
│ │ │ ├── styles.css # Stylesheets
|
||||||
|
│ │ │ ├── script.js # JavaScript logic
|
||||||
|
│ │ │ ├── translations.js # Translation logic
|
||||||
|
│ │ │ └── translations/ # Translation JSON files
|
||||||
|
│ │ └── AndroidManifest.xml # App manifest
|
||||||
|
│ ├── build.gradle # App-level build configuration
|
||||||
|
│ └── proguard-rules.pro # ProGuard rules
|
||||||
|
├── build.gradle # Project-level build configuration
|
||||||
|
├── settings.gradle # Gradle settings
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Android Studio (latest version recommended)
|
||||||
|
- Android SDK 21 or higher (minSdk)
|
||||||
|
- Target SDK 34
|
||||||
|
- Java 11 or higher
|
||||||
|
|
||||||
|
## Building the Project
|
||||||
|
|
||||||
|
### Using Android Studio
|
||||||
|
|
||||||
|
1. Open Android Studio
|
||||||
|
2. Click "Open an existing Android Studio project"
|
||||||
|
3. Navigate to the `MathHomeworkHelper` directory and select it
|
||||||
|
4. Wait for Gradle to sync
|
||||||
|
5. Click "Build" → "Build Bundle(s) / APK(s)" → "Build APK(s)"
|
||||||
|
|
||||||
|
### Using Command Line
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd MathHomeworkHelper
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
|
||||||
|
To build a debug APK:
|
||||||
|
```bash
|
||||||
|
./gradlew assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
To build a release APK:
|
||||||
|
```bash
|
||||||
|
./gradlew assembleRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the App
|
||||||
|
|
||||||
|
### On Android Studio Emulator
|
||||||
|
|
||||||
|
1. Create or select an Android Virtual Device (AVD)
|
||||||
|
2. Click the "Run" button (green play icon)
|
||||||
|
3. Select your emulator and click OK
|
||||||
|
|
||||||
|
### On Physical Device
|
||||||
|
|
||||||
|
1. Enable Developer Mode on your Android device
|
||||||
|
2. Connect your device via USB
|
||||||
|
3. Click the "Run" button in Android Studio
|
||||||
|
4. Select your device from the list
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ Displays HTML content in a WebView
|
||||||
|
- ✅ JavaScript enabled for interactive features
|
||||||
|
- ✅ Local storage and DOM storage support
|
||||||
|
- ✅ Zoom controls enabled
|
||||||
|
- ✅ Back button navigation support
|
||||||
|
- ✅ Multi-language support (English, Spanish, Swedish, Greek)
|
||||||
|
|
||||||
|
## WebView Configuration
|
||||||
|
|
||||||
|
The MainActivity.java configures the WebView with the following settings:
|
||||||
|
|
||||||
|
- **JavaScript**: Enabled for interactive functionality
|
||||||
|
- **DOM Storage**: Enabled for local data persistence
|
||||||
|
- **Database**: Enabled for local storage
|
||||||
|
- **Zoom Controls**: Built-in zoom controls available
|
||||||
|
- **File Access**: Enabled to access local assets
|
||||||
|
- **Mixed Content**: Allowed for Android 5.0+
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
The app requires the following permissions (defined in AndroidManifest.xml):
|
||||||
|
|
||||||
|
- `INTERNET`: For WebView functionality
|
||||||
|
- `ACCESS_NETWORK_STATE`: To check network connectivity
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### WebView not loading content
|
||||||
|
- Ensure all HTML files are in the `app/src/main/assets/` directory
|
||||||
|
- Check that JavaScript is enabled in WebView settings
|
||||||
|
- Verify the file path in `webView.loadUrl()`
|
||||||
|
|
||||||
|
### JavaScript not working
|
||||||
|
- Confirm `setJavaScriptEnabled(true)` is set in MainActivity
|
||||||
|
- Check browser console for JavaScript errors
|
||||||
|
|
||||||
|
### App crashes on startup
|
||||||
|
- Check the Logcat for error messages
|
||||||
|
- Ensure all required dependencies are in build.gradle
|
||||||
|
- Verify AndroidManifest.xml is properly configured
|
||||||
|
|
||||||
|
## Building for Release
|
||||||
|
|
||||||
|
To create a signed release APK:
|
||||||
|
|
||||||
|
1. Go to "Build" → "Generate Signed Bundle / APK"
|
||||||
|
2. Select "APK"
|
||||||
|
3. Create or select a keystore
|
||||||
|
4. Fill in the signing information
|
||||||
|
5. Select "release" build type
|
||||||
|
6. Click "Finish"
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project wraps the Math Homework Helper web application for Android.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions, please refer to the original web application documentation.
|
||||||
39
MathHomeworkHelper/app/build.gradle
Normal file
39
MathHomeworkHelper/app/build.gradle
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'com.example.mathhomeworkhelper'
|
||||||
|
compileSdk 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.example.mathhomeworkhelper"
|
||||||
|
minSdk 21
|
||||||
|
targetSdk 34
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_11
|
||||||
|
targetCompatibility JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation 'com.google.android.material:material:1.10.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
testImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
|
}
|
||||||
18
MathHomeworkHelper/app/proguard-rules.pro
vendored
Normal file
18
MathHomeworkHelper/app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# This is a configuration file for ProGuard.
|
||||||
|
# http://proguard.sourceforge.net/index.html#manual/usage.html
|
||||||
|
|
||||||
|
-dontusemixedcaseclassnames
|
||||||
|
# For using GSON @Expose annotation
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
|
# Gson specific classes
|
||||||
|
-dontwarn sun.misc.**
|
||||||
|
-dontwarn com.google.gson.**
|
||||||
|
-keep class com.google.gson.** { *; }
|
||||||
|
|
||||||
|
# Application classes that will be serialized/deserialized over Gson
|
||||||
|
-keep class com.example.mathhomeworkhelper.** { *; }
|
||||||
|
|
||||||
|
# Preserve line numbers for debugging stack traces
|
||||||
|
-keepattributes SourceFile,LineNumberTable
|
||||||
|
-renamesourcefileattribute SourceFile
|
||||||
28
MathHomeworkHelper/app/src/main/AndroidManifest.xml
Normal file
28
MathHomeworkHelper/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.example.mathhomeworkhelper">
|
||||||
|
|
||||||
|
<!-- Internet permission for WebView -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.MathHomeworkHelper">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
86
MathHomeworkHelper/app/src/main/assets/index.html
Normal file
86
MathHomeworkHelper/app/src/main/assets/index.html
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Math Homework Helper - Multiplication Practice</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Language Selector -->
|
||||||
|
<div class="language-selector">
|
||||||
|
<div class="language-flags" id="language-flags"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1 id="title">🎓 Math Homework Helper</h1>
|
||||||
|
<p class="subtitle" id="subtitle">Multiplication Practice</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- Difficulty Slider Section -->
|
||||||
|
<section class="difficulty-section">
|
||||||
|
<label for="difficulty-slider" class="difficulty-label">
|
||||||
|
Difficulty Level: <span id="difficulty-display">1</span>
|
||||||
|
</label>
|
||||||
|
<div class="slider-container">
|
||||||
|
<span class="difficulty-text">Easy</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="difficulty-slider"
|
||||||
|
min="1"
|
||||||
|
max="5"
|
||||||
|
value="1"
|
||||||
|
class="slider"
|
||||||
|
>
|
||||||
|
<span class="difficulty-text">Hard</span>
|
||||||
|
</div>
|
||||||
|
<div class="difficulty-info">
|
||||||
|
<p id="difficulty-description">1 digit × 1 digit (e.g., 5 × 7)</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Problem Display Section -->
|
||||||
|
<section class="problem-section">
|
||||||
|
<div class="problem-display" id="problem-display">
|
||||||
|
<!-- Problem will be rendered here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Check Answer Button -->
|
||||||
|
<section class="answer-section">
|
||||||
|
<button id="submit-btn" class="submit-btn" style="width: 100%;">Check Answer</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Feedback Section -->
|
||||||
|
<section class="feedback-section">
|
||||||
|
<div id="feedback" class="feedback"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Score Section -->
|
||||||
|
<section class="score-section">
|
||||||
|
<div class="score-box">
|
||||||
|
<p class="score-label">Points:</p>
|
||||||
|
<p class="score-value" id="points-score">0/20</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- New Problem Button -->
|
||||||
|
<button id="new-problem-btn" class="new-problem-btn">New Problem</button>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Victory Modal -->
|
||||||
|
<div id="victory-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2>🎉 Congratulations! 🎉</h2>
|
||||||
|
<p>You've reached 20 points!</p>
|
||||||
|
<button id="play-again-btn" class="modal-btn">Play Again</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="translations.js"></script>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
651
MathHomeworkHelper/app/src/main/assets/script.js
Normal file
651
MathHomeworkHelper/app/src/main/assets/script.js
Normal file
|
|
@ -0,0 +1,651 @@
|
||||||
|
// Game State
|
||||||
|
let gameState = {
|
||||||
|
difficulty: 1,
|
||||||
|
currentProblem: null,
|
||||||
|
points: 0,
|
||||||
|
totalAnswers: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Difficulty configurations (will be updated with translations)
|
||||||
|
let difficultyConfig = {
|
||||||
|
1: {
|
||||||
|
num1Min: 1, num1Max: 9,
|
||||||
|
num2Min: 1, num2Max: 9,
|
||||||
|
descriptionKey: 'difficulty1'
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
num1Min: 10, num1Max: 99,
|
||||||
|
num2Min: 1, num2Max: 9,
|
||||||
|
descriptionKey: 'difficulty2'
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
num1Min: 10, num1Max: 99,
|
||||||
|
num2Min: 10, num2Max: 99,
|
||||||
|
descriptionKey: 'difficulty3'
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
num1Min: 100, num1Max: 999,
|
||||||
|
num2Min: 10, num2Max: 99,
|
||||||
|
descriptionKey: 'difficulty4'
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
num1Min: 10000, num1Max: 99999,
|
||||||
|
num2Min: 100, num2Max: 999,
|
||||||
|
descriptionKey: 'difficulty5'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
const difficultySlider = document.getElementById('difficulty-slider');
|
||||||
|
const difficultyDisplay = document.getElementById('difficulty-display');
|
||||||
|
const difficultyDescription = document.getElementById('difficulty-description');
|
||||||
|
const problemDisplay = document.getElementById('problem-display');
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
const feedbackDiv = document.getElementById('feedback');
|
||||||
|
const pointsScoreDisplay = document.getElementById('points-score');
|
||||||
|
const newProblemBtn = document.getElementById('new-problem-btn');
|
||||||
|
const victoryModal = document.getElementById('victory-modal');
|
||||||
|
const playAgainBtn = document.getElementById('play-again-btn');
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
difficultySlider.addEventListener('change', handleDifficultyChange);
|
||||||
|
submitBtn.addEventListener('click', handleSubmitAnswer);
|
||||||
|
newProblemBtn.addEventListener('click', generateNewProblem);
|
||||||
|
playAgainBtn.addEventListener('click', resetGame);
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
window.addEventListener('load', async () => {
|
||||||
|
await i18n.initialize();
|
||||||
|
initializeLanguageSelector();
|
||||||
|
updateUIText();
|
||||||
|
generateNewProblem();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle difficulty slider change
|
||||||
|
*/
|
||||||
|
function handleDifficultyChange() {
|
||||||
|
gameState.difficulty = parseInt(difficultySlider.value);
|
||||||
|
difficultyDisplay.textContent = gameState.difficulty;
|
||||||
|
difficultyDescription.textContent = difficultyConfig[gameState.difficulty].description;
|
||||||
|
generateNewProblem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random number between min and max (inclusive)
|
||||||
|
*/
|
||||||
|
function getRandomNumber(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new multiplication problem
|
||||||
|
*/
|
||||||
|
function generateNewProblem() {
|
||||||
|
const config = difficultyConfig[gameState.difficulty];
|
||||||
|
const num1 = getRandomNumber(config.num1Min, config.num1Max);
|
||||||
|
const num2 = getRandomNumber(config.num2Min, config.num2Max);
|
||||||
|
const answer = num1 * num2;
|
||||||
|
|
||||||
|
gameState.currentProblem = {
|
||||||
|
num1,
|
||||||
|
num2,
|
||||||
|
answer
|
||||||
|
};
|
||||||
|
|
||||||
|
displayProblem();
|
||||||
|
clearFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate intermediate steps for vertical multiplication
|
||||||
|
*/
|
||||||
|
function calculateIntermediateSteps(num1, num2) {
|
||||||
|
const num2Str = num2.toString();
|
||||||
|
const steps = [];
|
||||||
|
|
||||||
|
// Calculate partial products for each digit of num2
|
||||||
|
for (let i = num2Str.length - 1; i >= 0; i--) {
|
||||||
|
const digit = parseInt(num2Str[i]);
|
||||||
|
const partialProduct = num1 * digit;
|
||||||
|
const shiftAmount = num2Str.length - 1 - i;
|
||||||
|
steps.push({
|
||||||
|
digit,
|
||||||
|
product: partialProduct,
|
||||||
|
shift: shiftAmount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the multiplication problem
|
||||||
|
*/
|
||||||
|
function displayProblem() {
|
||||||
|
const { num1, num2 } = gameState.currentProblem;
|
||||||
|
const difficulty = gameState.difficulty;
|
||||||
|
|
||||||
|
if (difficulty === 1) {
|
||||||
|
// Horizontal layout for 1x1 with result
|
||||||
|
const answer = gameState.currentProblem.answer.toString();
|
||||||
|
let resultHTML = '<div class="line" style="justify-content: flex-end; gap: 2px;">';
|
||||||
|
for (let i = 0; i < answer.length; i++) {
|
||||||
|
resultHTML += `<input type="text" class="digit-input result-input" maxlength="1" data-pos="${i}" />`;
|
||||||
|
}
|
||||||
|
resultHTML += '</div>';
|
||||||
|
|
||||||
|
problemDisplay.innerHTML = `
|
||||||
|
<div class="multiplication-horizontal">
|
||||||
|
<span class="number">${num1}</span>
|
||||||
|
<span class="operator">×</span>
|
||||||
|
<span class="number">${num2}</span>
|
||||||
|
<span class="operator">=</span>
|
||||||
|
${resultHTML}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add event listeners to result digit inputs
|
||||||
|
setTimeout(() => {
|
||||||
|
addResultDigitInputListeners();
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
// Vertical layout for 2+ digits
|
||||||
|
const num1Str = num1.toString();
|
||||||
|
const num2Str = num2.toString();
|
||||||
|
|
||||||
|
// Calculate intermediate steps for reference
|
||||||
|
const steps = calculateIntermediateSteps(num1, num2);
|
||||||
|
gameState.currentProblem.steps = steps;
|
||||||
|
|
||||||
|
// Create input fields for intermediate steps only if num2 has more than 1 digit
|
||||||
|
let intermediateHTML = '';
|
||||||
|
if (num2 > 9) {
|
||||||
|
// Calculate the maximum width needed for intermediate steps
|
||||||
|
const maxResultWidth = gameState.currentProblem.answer.toString().length;
|
||||||
|
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
const stepProduct = steps[i].product.toString();
|
||||||
|
const totalWidth = stepProduct.length + steps[i].shift;
|
||||||
|
intermediateHTML += '<div class="line" style="justify-content: flex-start; gap: 5px;">';
|
||||||
|
|
||||||
|
// Each row is offset one position to the left
|
||||||
|
// Add offset spaces at the beginning (left side)
|
||||||
|
for (let offset = 0; offset < i; offset++) {
|
||||||
|
intermediateHTML += '<span style="width: 90px;"></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add empty spaces for shift
|
||||||
|
for (let s = 0; s < steps[i].shift; s++) {
|
||||||
|
intermediateHTML += '<span style="width: 90px;"></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add input fields for each digit
|
||||||
|
for (let j = 0; j < stepProduct.length; j++) {
|
||||||
|
intermediateHTML += `<input type="text" class="digit-input intermediate-input" maxlength="1" data-step="${i}" data-pos="${j}" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add trailing empty spaces on the right
|
||||||
|
const trailingSpaces = maxResultWidth - totalWidth - i;
|
||||||
|
for (let e = 0; e < trailingSpaces; e++) {
|
||||||
|
intermediateHTML += '<span style="width: 90px;"></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
intermediateHTML += '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create input fields for final result
|
||||||
|
const answer = gameState.currentProblem.answer.toString();
|
||||||
|
let resultHTML = '<div class="line" style="justify-content: flex-end; gap: 2px;">';
|
||||||
|
for (let i = 0; i < answer.length; i++) {
|
||||||
|
resultHTML += `<input type="text" class="digit-input result-input" maxlength="1" data-pos="${i}" />`;
|
||||||
|
}
|
||||||
|
resultHTML += '</div>';
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="multiplication-vertical">
|
||||||
|
<div class="line">
|
||||||
|
<span>${num1Str}</span>
|
||||||
|
</div>
|
||||||
|
<div class="line">
|
||||||
|
<span class="operator">×</span>
|
||||||
|
<span>${num2Str}</span>
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (num2 > 9) {
|
||||||
|
html += `
|
||||||
|
<div id="intermediate-steps">
|
||||||
|
${intermediateHTML}
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div id="result-steps">
|
||||||
|
${resultHTML}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
problemDisplay.innerHTML = html;
|
||||||
|
|
||||||
|
// Add event listeners to digit inputs
|
||||||
|
setTimeout(() => {
|
||||||
|
addDigitInputListeners();
|
||||||
|
addResultDigitInputListeners();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add event listeners to digit input fields
|
||||||
|
*/
|
||||||
|
function addDigitInputListeners() {
|
||||||
|
const digitInputs = document.querySelectorAll('.digit-input:not(.result-input)');
|
||||||
|
|
||||||
|
digitInputs.forEach((input, index) => {
|
||||||
|
input.addEventListener('input', (e) => {
|
||||||
|
// Only allow digits
|
||||||
|
if (!/^\d?$/.test(e.target.value)) {
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to next input if digit entered
|
||||||
|
if (e.target.value && index < digitInputs.length - 1) {
|
||||||
|
digitInputs[index + 1].focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Backspace' && !e.target.value && index > 0) {
|
||||||
|
digitInputs[index - 1].focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add event listeners to result digit input fields
|
||||||
|
* Navigation: left after filling, wrap to rightmost of next line
|
||||||
|
*/
|
||||||
|
function addResultDigitInputListeners() {
|
||||||
|
const resultInputs = document.querySelectorAll('.result-input');
|
||||||
|
|
||||||
|
resultInputs.forEach((input, index) => {
|
||||||
|
input.addEventListener('input', (e) => {
|
||||||
|
// Only allow digits
|
||||||
|
if (!/^\d?$/.test(e.target.value)) {
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to LEFT if digit entered
|
||||||
|
if (e.target.value) {
|
||||||
|
if (index > 0) {
|
||||||
|
// Move to previous input (left)
|
||||||
|
resultInputs[index - 1].focus();
|
||||||
|
} else if (index === 0) {
|
||||||
|
// At the leftmost position, wrap to rightmost of previous line
|
||||||
|
const allLines = document.querySelectorAll('#result-steps .line, #intermediate-steps .line');
|
||||||
|
if (allLines.length > 1) {
|
||||||
|
const prevLineInputs = allLines[allLines.length - 2].querySelectorAll('input');
|
||||||
|
if (prevLineInputs.length > 0) {
|
||||||
|
prevLineInputs[prevLineInputs.length - 1].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Backspace' && !e.target.value && index < resultInputs.length - 1) {
|
||||||
|
resultInputs[index + 1].focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find which digits are incorrect in the answer
|
||||||
|
*/
|
||||||
|
function findIncorrectDigits(userAnswer, correctAnswer) {
|
||||||
|
const userStr = userAnswer.toString();
|
||||||
|
const correctStr = correctAnswer.toString();
|
||||||
|
const incorrectPositions = [];
|
||||||
|
|
||||||
|
// Pad the shorter string with leading zeros for comparison
|
||||||
|
const maxLen = Math.max(userStr.length, correctStr.length);
|
||||||
|
const userPadded = userStr.padStart(maxLen, '0');
|
||||||
|
const correctPadded = correctStr.padStart(maxLen, '0');
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
if (userPadded[i] !== correctPadded[i]) {
|
||||||
|
incorrectPositions.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return incorrectPositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display answer with highlighted incorrect digits
|
||||||
|
*/
|
||||||
|
function displayAnswerWithErrors(userAnswer, correctAnswer) {
|
||||||
|
const incorrectPositions = findIncorrectDigits(userAnswer, correctAnswer);
|
||||||
|
const userStr = userAnswer.toString();
|
||||||
|
const correctStr = correctAnswer.toString();
|
||||||
|
const maxLen = Math.max(userStr.length, correctStr.length);
|
||||||
|
const userPadded = userStr.padStart(maxLen, '0');
|
||||||
|
|
||||||
|
let html = '<div style="font-size: 1.5em; margin-top: 15px;"><strong>Your answer:</strong> ';
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
const digit = userPadded[i];
|
||||||
|
if (incorrectPositions.includes(i)) {
|
||||||
|
html += `<span style="border: 3px solid #dc2626; padding: 5px 8px; margin: 0 2px; display: inline-block; background-color: #fee2e2;">${digit}</span>`;
|
||||||
|
} else {
|
||||||
|
html += digit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `<br><strong>Correct answer:</strong> ${correctStr}</div>`;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user answer from input boxes
|
||||||
|
*/
|
||||||
|
function getUserAnswer() {
|
||||||
|
const resultInputs = document.querySelectorAll('.result-input');
|
||||||
|
let answer = '';
|
||||||
|
resultInputs.forEach(input => {
|
||||||
|
answer += input.value;
|
||||||
|
});
|
||||||
|
return answer === '' ? null : parseInt(answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user intermediate steps from input boxes
|
||||||
|
*/
|
||||||
|
function getUserIntermediateSteps() {
|
||||||
|
const steps = [];
|
||||||
|
const intermediateLines = document.querySelectorAll('#intermediate-steps .line');
|
||||||
|
|
||||||
|
intermediateLines.forEach((line, lineIndex) => {
|
||||||
|
const inputs = line.querySelectorAll('input');
|
||||||
|
let stepValue = '';
|
||||||
|
inputs.forEach(input => {
|
||||||
|
stepValue += input.value;
|
||||||
|
});
|
||||||
|
if (stepValue) {
|
||||||
|
steps.push(parseInt(stepValue));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle answer submission
|
||||||
|
*/
|
||||||
|
function handleSubmitAnswer() {
|
||||||
|
const userAnswer = getUserAnswer();
|
||||||
|
const correctAnswer = gameState.currentProblem.answer;
|
||||||
|
|
||||||
|
if (userAnswer === null) {
|
||||||
|
showFeedback('Please enter an answer!', 'incorrect');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameState.totalAnswers++;
|
||||||
|
|
||||||
|
// Check intermediate steps if they exist
|
||||||
|
const intermediateStepsDiv = document.getElementById('intermediate-steps');
|
||||||
|
let intermediateStepsCorrect = true;
|
||||||
|
let intermediateErrorMessage = '';
|
||||||
|
|
||||||
|
if (intermediateStepsDiv) {
|
||||||
|
const steps = gameState.currentProblem.steps;
|
||||||
|
const intermediateLines = document.querySelectorAll('#intermediate-steps .line');
|
||||||
|
|
||||||
|
intermediateLines.forEach((line, lineIndex) => {
|
||||||
|
const inputs = line.querySelectorAll('input');
|
||||||
|
let userStepValue = '';
|
||||||
|
inputs.forEach(input => {
|
||||||
|
userStepValue += input.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userStepValue) {
|
||||||
|
const userStepNum = parseInt(userStepValue);
|
||||||
|
const correctStepNum = steps[lineIndex].product;
|
||||||
|
|
||||||
|
if (userStepNum !== correctStepNum) {
|
||||||
|
intermediateStepsCorrect = false;
|
||||||
|
intermediateErrorMessage += `<br>Step ${lineIndex + 1}: You wrote ${userStepNum}, correct is ${correctStepNum}`;
|
||||||
|
|
||||||
|
// Highlight incorrect digits in this step
|
||||||
|
const correctStr = correctStepNum.toString();
|
||||||
|
const userStr = userStepValue;
|
||||||
|
inputs.forEach((input, digitIndex) => {
|
||||||
|
if (digitIndex < userStr.length && userStr[digitIndex] !== correctStr[correctStr.length - userStr.length + digitIndex]) {
|
||||||
|
input.classList.add('error');
|
||||||
|
} else if (digitIndex < userStr.length) {
|
||||||
|
input.classList.add('correct');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userAnswer === correctAnswer && intermediateStepsCorrect) {
|
||||||
|
gameState.points += 1;
|
||||||
|
showFeedback('🎉 Correct! +1 point', 'correct');
|
||||||
|
updateScore();
|
||||||
|
|
||||||
|
// Check if reached 20 points
|
||||||
|
if (gameState.points >= 20) {
|
||||||
|
showVictoryModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
generateNewProblem();
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
gameState.points = Math.max(0, gameState.points - 2);
|
||||||
|
|
||||||
|
// Mark incorrect result boxes with red color
|
||||||
|
if (userAnswer !== correctAnswer) {
|
||||||
|
const resultInputs = document.querySelectorAll('.result-input');
|
||||||
|
const userStr = userAnswer.toString();
|
||||||
|
const correctStr = correctAnswer.toString();
|
||||||
|
const maxLen = Math.max(userStr.length, correctStr.length);
|
||||||
|
const userPadded = userStr.padStart(maxLen, '0');
|
||||||
|
const correctPadded = correctStr.padStart(maxLen, '0');
|
||||||
|
|
||||||
|
resultInputs.forEach((input, index) => {
|
||||||
|
if (userPadded[index] !== correctPadded[index]) {
|
||||||
|
input.classList.add('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
feedbackDiv.textContent = '❌ Your answer is wrong, check the red boxes';
|
||||||
|
feedbackDiv.className = 'feedback incorrect';
|
||||||
|
updateScore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show feedback message
|
||||||
|
*/
|
||||||
|
function showFeedback(message, type) {
|
||||||
|
feedbackDiv.textContent = message;
|
||||||
|
feedbackDiv.className = `feedback ${type}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear feedback message
|
||||||
|
*/
|
||||||
|
function clearFeedback() {
|
||||||
|
feedbackDiv.textContent = '';
|
||||||
|
feedbackDiv.className = 'feedback empty';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update score display
|
||||||
|
*/
|
||||||
|
function updateScore() {
|
||||||
|
pointsScoreDisplay.textContent = `${gameState.points}/20`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show victory modal and create confetti
|
||||||
|
*/
|
||||||
|
function showVictoryModal() {
|
||||||
|
victoryModal.classList.add('show');
|
||||||
|
createConfetti();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create confetti animation
|
||||||
|
*/
|
||||||
|
function createConfetti() {
|
||||||
|
const colors = ['#2563eb', '#7c3aed', '#dc2626', '#16a34a', '#ea580c', '#f59e0b'];
|
||||||
|
const confettiCount = 50;
|
||||||
|
|
||||||
|
for (let i = 0; i < confettiCount; i++) {
|
||||||
|
const confetti = document.createElement('div');
|
||||||
|
confetti.className = 'confetti';
|
||||||
|
confetti.style.left = Math.random() * 100 + '%';
|
||||||
|
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
confetti.style.delay = Math.random() * 0.5 + 's';
|
||||||
|
confetti.style.animationDuration = (Math.random() * 2 + 2.5) + 's';
|
||||||
|
document.body.appendChild(confetti);
|
||||||
|
|
||||||
|
// Remove confetti element after animation
|
||||||
|
setTimeout(() => {
|
||||||
|
confetti.remove();
|
||||||
|
}, 3500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset game for new round
|
||||||
|
*/
|
||||||
|
function resetGame() {
|
||||||
|
gameState.points = 0;
|
||||||
|
gameState.totalAnswers = 0;
|
||||||
|
victoryModal.classList.remove('show');
|
||||||
|
updateScore();
|
||||||
|
generateNewProblem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize language selector
|
||||||
|
*/
|
||||||
|
function initializeLanguageSelector() {
|
||||||
|
const languageFlagsContainer = document.getElementById('language-flags');
|
||||||
|
const languages = i18n.getSupportedLanguages();
|
||||||
|
|
||||||
|
languageFlagsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
Object.entries(languages).forEach(([langCode, langInfo]) => {
|
||||||
|
const flagButton = document.createElement('button');
|
||||||
|
flagButton.className = 'language-flag';
|
||||||
|
flagButton.textContent = langInfo.flag;
|
||||||
|
flagButton.title = langInfo.name;
|
||||||
|
flagButton.setAttribute('data-tooltip', langInfo.name);
|
||||||
|
|
||||||
|
if (langCode === i18n.getCurrentLanguage()) {
|
||||||
|
flagButton.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
flagButton.addEventListener('click', () => {
|
||||||
|
changeLanguage(langCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
languageFlagsContainer.appendChild(flagButton);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change language and update UI
|
||||||
|
*/
|
||||||
|
function changeLanguage(langCode) {
|
||||||
|
i18n.setLanguage(langCode);
|
||||||
|
|
||||||
|
// Update active flag
|
||||||
|
document.querySelectorAll('.language-flag').forEach(flag => {
|
||||||
|
flag.classList.remove('active');
|
||||||
|
});
|
||||||
|
const languages = i18n.getSupportedLanguages();
|
||||||
|
const flagButtons = document.querySelectorAll('.language-flag');
|
||||||
|
const langCodes = Object.keys(languages);
|
||||||
|
flagButtons.forEach((btn, index) => {
|
||||||
|
if (langCodes[index] === langCode) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateUIText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all UI text based on current language
|
||||||
|
*/
|
||||||
|
function updateUIText() {
|
||||||
|
// Update header
|
||||||
|
document.getElementById('title').textContent = '🎓 ' + i18n.t('title');
|
||||||
|
document.getElementById('subtitle').textContent = i18n.t('subtitle');
|
||||||
|
|
||||||
|
// Update difficulty label
|
||||||
|
const difficultyLabel = document.querySelector('.difficulty-label');
|
||||||
|
if (difficultyLabel) {
|
||||||
|
difficultyLabel.innerHTML = i18n.t('difficultyLevel') + ' <span id="difficulty-display">' + gameState.difficulty + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update difficulty text labels
|
||||||
|
const difficultyTexts = document.querySelectorAll('.difficulty-text');
|
||||||
|
if (difficultyTexts.length >= 2) {
|
||||||
|
difficultyTexts[0].textContent = i18n.t('easy');
|
||||||
|
difficultyTexts[1].textContent = i18n.t('hard');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update difficulty description
|
||||||
|
const config = difficultyConfig[gameState.difficulty];
|
||||||
|
if (config && config.descriptionKey) {
|
||||||
|
document.getElementById('difficulty-description').textContent = i18n.t(config.descriptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update buttons
|
||||||
|
document.getElementById('submit-btn').textContent = i18n.t('checkAnswer');
|
||||||
|
document.getElementById('new-problem-btn').textContent = i18n.t('newProblem');
|
||||||
|
|
||||||
|
// Update score label
|
||||||
|
const scoreLabel = document.querySelector('.score-label');
|
||||||
|
if (scoreLabel) {
|
||||||
|
scoreLabel.textContent = i18n.t('points');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update modal
|
||||||
|
const modalTitle = document.querySelector('.modal-content h2');
|
||||||
|
const modalText = document.querySelector('.modal-content p');
|
||||||
|
const playAgainButton = document.getElementById('play-again-btn');
|
||||||
|
|
||||||
|
if (modalTitle) {
|
||||||
|
modalTitle.textContent = i18n.t('congratulations');
|
||||||
|
}
|
||||||
|
if (modalText) {
|
||||||
|
modalText.textContent = i18n.t('youReached20Points');
|
||||||
|
}
|
||||||
|
if (playAgainButton) {
|
||||||
|
playAgainButton.textContent = i18n.t('playAgain');
|
||||||
|
}
|
||||||
|
}
|
||||||
591
MathHomeworkHelper/app/src/main/assets/styles.css
Normal file
591
MathHomeworkHelper/app/src/main/assets/styles.css
Normal file
|
|
@ -0,0 +1,591 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||||
|
background: #87CEEB;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Language Selector */
|
||||||
|
.language-selector {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-flags {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
background: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-flag {
|
||||||
|
font-size: 1.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-flag:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-flag.active {
|
||||||
|
background: #87CEEB;
|
||||||
|
border-color: #2563eb;
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-flag-tooltip {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-flag-tooltip:hover::after {
|
||||||
|
content: attr(data-tooltip);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
z-index: 101;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 30px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
max-width: 1000px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 40px;
|
||||||
|
animation: slideIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 3em;
|
||||||
|
color: #2563eb;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: #7c3aed;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Difficulty Section */
|
||||||
|
.difficulty-section {
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #ea580c 100%);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 10px 25px rgba(220, 38, 38, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#difficulty-display {
|
||||||
|
background: white;
|
||||||
|
color: #dc2626;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-text {
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.95em;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
flex: 1;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-thumb:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-info {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#difficulty-description {
|
||||||
|
color: white;
|
||||||
|
font-size: 0.95em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Problem Section */
|
||||||
|
.problem-section {
|
||||||
|
background: linear-gradient(135deg, #dbeafe 0%, #e0e7ff 100%);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
min-height: 150px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 10px 25px rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-display {
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal multiplication (1x1) */
|
||||||
|
.multiplication-horizontal {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplication-horizontal .number {
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplication-horizontal .operator {
|
||||||
|
font-size: 2.5em;
|
||||||
|
color: #f5576c;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical multiplication (2+ digits) */
|
||||||
|
.multiplication-vertical {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplication-vertical .line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 5px;
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplication-vertical .operator {
|
||||||
|
color: #f5576c;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplication-vertical .separator {
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
background: #333;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit-input {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
font-size: 1.4em;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #2563eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 8px rgba(124, 58, 237, 0.4);
|
||||||
|
background-color: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit-input.error {
|
||||||
|
border-color: #dc2626;
|
||||||
|
background-color: #fee2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit-input.correct {
|
||||||
|
border-color: #16a34a;
|
||||||
|
background-color: #dcfce7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Answer Section */
|
||||||
|
.answer-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 18px;
|
||||||
|
font-size: 1.3em;
|
||||||
|
border: 3px solid #2563eb;
|
||||||
|
border-radius: 15px;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-input:focus {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 15px rgba(124, 58, 237, 0.3);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-input::placeholder {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
padding: 18px 30px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 5px 15px rgba(37, 99, 235, 0.4);
|
||||||
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feedback Section */
|
||||||
|
.feedback-section {
|
||||||
|
min-height: 80px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
min-width: 100%;
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback.correct {
|
||||||
|
background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
|
||||||
|
color: #2d5016;
|
||||||
|
box-shadow: 0 5px 15px rgba(132, 250, 176, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback.incorrect {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
color: #8b0000;
|
||||||
|
box-shadow: 0 5px 15px rgba(250, 112, 154, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback.empty {
|
||||||
|
background: transparent;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Score Section */
|
||||||
|
.score-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-box {
|
||||||
|
background: linear-gradient(135deg, #16a34a 0%, #059669 100%);
|
||||||
|
padding: 20px 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 5px 15px rgba(22, 163, 74, 0.3);
|
||||||
|
min-width: 120px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-label {
|
||||||
|
font-size: 0.95em;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
font-size: 2.5em;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New Problem Button */
|
||||||
|
.new-problem-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 18px;
|
||||||
|
font-size: 1.3em;
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #ea580c 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 5px 15px rgba(220, 38, 38, 0.4);
|
||||||
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-problem-btn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 20px rgba(220, 38, 38, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-problem-btn:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Confetti Animation */
|
||||||
|
@keyframes confetti-fall {
|
||||||
|
to {
|
||||||
|
transform: translateY(100vh) rotateZ(360deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confetti {
|
||||||
|
position: fixed;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: confetti-fall 3s ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Popup */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: modalBounce 0.5s ease-out;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalBounce {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
color: #16a34a;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content p {
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn {
|
||||||
|
padding: 15px 40px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
background: linear-gradient(135deg, #16a34a 0%, #059669 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 5px 15px rgba(22, 163, 74, 0.4);
|
||||||
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 20px rgba(22, 163, 74, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.container {
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-section {
|
||||||
|
padding: 25px;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-display {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-input,
|
||||||
|
.submit-btn {
|
||||||
|
font-size: 1.1em;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-box {
|
||||||
|
min-width: 100px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
184
MathHomeworkHelper/app/src/main/assets/translations.js
Normal file
184
MathHomeworkHelper/app/src/main/assets/translations.js
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
// Translation Manager
|
||||||
|
class TranslationManager {
|
||||||
|
constructor() {
|
||||||
|
this.currentLanguage = 'en'; // Will be set during initialization
|
||||||
|
this.translations = {};
|
||||||
|
this.supportedLanguages = {
|
||||||
|
'en': { name: 'English', flag: '🇬🇧' },
|
||||||
|
'es': { name: 'Español', flag: '🇪🇸' },
|
||||||
|
'sv': { name: 'Svenska', flag: '🇸🇪' },
|
||||||
|
'el': { name: 'Ελληνικά', flag: '🇬🇷' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Country to language mapping
|
||||||
|
this.countryToLanguage = {
|
||||||
|
// Spanish-speaking countries
|
||||||
|
'ES': 'es', 'MX': 'es', 'AR': 'es', 'CO': 'es', 'PE': 'es', 'VE': 'es',
|
||||||
|
'CL': 'es', 'EC': 'es', 'BO': 'es', 'PY': 'es', 'UY': 'es', 'CU': 'es',
|
||||||
|
'DO': 'es', 'GT': 'es', 'HN': 'es', 'SV': 'es', 'NI': 'es', 'CR': 'es',
|
||||||
|
'PA': 'es', 'BZ': 'es', 'EQ': 'es',
|
||||||
|
// Swedish-speaking countries
|
||||||
|
'SE': 'sv', 'FI': 'sv', 'AX': 'sv',
|
||||||
|
// Greek-speaking countries
|
||||||
|
'GR': 'el', 'CY': 'el',
|
||||||
|
// Default to English for all others
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect language with geolocation fallback
|
||||||
|
async detectLanguageWithGeolocation() {
|
||||||
|
// Step 1: Check localStorage for saved preference
|
||||||
|
const savedLanguage = localStorage.getItem('preferredLanguage');
|
||||||
|
if (savedLanguage && this.supportedLanguages[savedLanguage]) {
|
||||||
|
console.log('Using saved language preference:', savedLanguage);
|
||||||
|
return savedLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Try browser language detection
|
||||||
|
const browserLang = this.detectBrowserLanguage();
|
||||||
|
if (browserLang !== 'en') {
|
||||||
|
console.log('Using browser language:', browserLang);
|
||||||
|
return browserLang;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Try geolocation API to get country and map to language
|
||||||
|
try {
|
||||||
|
const countryCode = await this.getCountryFromGeolocation();
|
||||||
|
if (countryCode) {
|
||||||
|
const language = this.countryToLanguage[countryCode] || 'en';
|
||||||
|
console.log('Using geolocation-based language:', language, 'from country:', countryCode);
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Geolocation failed, continuing with fallback:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Fallback to English
|
||||||
|
console.log('Falling back to English');
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect browser language
|
||||||
|
detectBrowserLanguage() {
|
||||||
|
const browserLang = navigator.language || navigator.userLanguage;
|
||||||
|
const langCode = browserLang.split('-')[0];
|
||||||
|
|
||||||
|
// Check if the detected language is supported
|
||||||
|
if (this.supportedLanguages[langCode]) {
|
||||||
|
return langCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return 'en' if not supported (will trigger geolocation)
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get country code from geolocation API
|
||||||
|
async getCountryFromGeolocation() {
|
||||||
|
const timeout = 5000; // 5 second timeout
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
|
// Try ip-api.com (free, no auth required)
|
||||||
|
const response = await fetch('https://ipapi.co/json/', {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: { 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API returned status ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const countryCode = data.country_code;
|
||||||
|
|
||||||
|
if (countryCode && typeof countryCode === 'string') {
|
||||||
|
console.log('Geolocation API returned country:', countryCode);
|
||||||
|
return countryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No country code in response');
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
console.warn('Geolocation API request timed out');
|
||||||
|
} else {
|
||||||
|
console.warn('Geolocation API error:', error.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load translation file
|
||||||
|
async loadTranslation(language) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`translations/${language}.json`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load ${language} translation`);
|
||||||
|
}
|
||||||
|
this.translations[language] = await response.json();
|
||||||
|
this.currentLanguage = language;
|
||||||
|
localStorage.setItem('preferredLanguage', language);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading translation:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get translated string
|
||||||
|
t(key) {
|
||||||
|
if (this.translations[this.currentLanguage] && this.translations[this.currentLanguage][key]) {
|
||||||
|
return this.translations[this.currentLanguage][key];
|
||||||
|
}
|
||||||
|
// Fallback to English if key not found
|
||||||
|
if (this.translations['en'] && this.translations['en'][key]) {
|
||||||
|
return this.translations['en'][key];
|
||||||
|
}
|
||||||
|
// Return key if no translation found
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current language
|
||||||
|
getCurrentLanguage() {
|
||||||
|
return this.currentLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all supported languages
|
||||||
|
getSupportedLanguages() {
|
||||||
|
return this.supportedLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set language
|
||||||
|
setLanguage(language) {
|
||||||
|
if (this.supportedLanguages[language]) {
|
||||||
|
this.currentLanguage = language;
|
||||||
|
localStorage.setItem('preferredLanguage', language);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize translations
|
||||||
|
async initialize() {
|
||||||
|
// Detect language using geolocation fallback chain
|
||||||
|
this.currentLanguage = await this.detectLanguageWithGeolocation();
|
||||||
|
|
||||||
|
// Load all supported language files
|
||||||
|
const loadPromises = Object.keys(this.supportedLanguages).map(lang =>
|
||||||
|
this.loadTranslation(lang)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(loadPromises);
|
||||||
|
|
||||||
|
// Ensure current language is set correctly
|
||||||
|
if (!this.translations[this.currentLanguage]) {
|
||||||
|
this.currentLanguage = 'en';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create global instance
|
||||||
|
const i18n = new TranslationManager();
|
||||||
20
MathHomeworkHelper/app/src/main/assets/translations/el.json
Normal file
20
MathHomeworkHelper/app/src/main/assets/translations/el.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"title": "Βοηθός Μαθηματικών Εργασιών",
|
||||||
|
"subtitle": "Εξάσκηση Πολλαπλασιασμού",
|
||||||
|
"difficultyLevel": "Επίπεδο Δυσκολίας:",
|
||||||
|
"easy": "Εύκολο",
|
||||||
|
"hard": "Δύσκολο",
|
||||||
|
"difficulty1": "1 ψηφίο × 1 ψηφίο (π.χ. 5 × 7)",
|
||||||
|
"difficulty2": "1 ψηφίο × 2 ψηφία (π.χ. 5 × 23)",
|
||||||
|
"difficulty3": "2 ψηφία × 2 ψηφία (π.χ. 23 × 45)",
|
||||||
|
"difficulty4": "2 ψηφία × 3 ψηφία (π.χ. 23 × 456)",
|
||||||
|
"difficulty5": "3 ψηφία × 3 ψηφία (π.χ. 234 × 567)",
|
||||||
|
"checkAnswer": "Έλεγχος Απάντησης",
|
||||||
|
"points": "Πόντοι:",
|
||||||
|
"newProblem": "Νέο Πρόβλημα",
|
||||||
|
"congratulations": "🎉 Συγχαρητήρια! 🎉",
|
||||||
|
"youReached20Points": "Έφτασες τους 20 πόντους!",
|
||||||
|
"playAgain": "Παίξε Ξανά",
|
||||||
|
"correct": "Σωστό! Εξαιρετική δουλειά! 🎉",
|
||||||
|
"incorrect": "Λάθος. Προσπάθησε ξανά! 💪"
|
||||||
|
}
|
||||||
20
MathHomeworkHelper/app/src/main/assets/translations/en.json
Normal file
20
MathHomeworkHelper/app/src/main/assets/translations/en.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"title": "Math Homework Helper",
|
||||||
|
"subtitle": "Multiplication Practice",
|
||||||
|
"difficultyLevel": "Difficulty Level:",
|
||||||
|
"easy": "Easy",
|
||||||
|
"hard": "Hard",
|
||||||
|
"difficulty1": "1 digit × 1 digit (e.g., 5 × 7)",
|
||||||
|
"difficulty2": "1 digit × 2 digits (e.g., 5 × 23)",
|
||||||
|
"difficulty3": "2 digits × 2 digits (e.g., 23 × 45)",
|
||||||
|
"difficulty4": "2 digits × 3 digits (e.g., 23 × 456)",
|
||||||
|
"difficulty5": "3 digits × 3 digits (e.g., 234 × 567)",
|
||||||
|
"checkAnswer": "Check Answer",
|
||||||
|
"points": "Points:",
|
||||||
|
"newProblem": "New Problem",
|
||||||
|
"congratulations": "🎉 Congratulations! 🎉",
|
||||||
|
"youReached20Points": "You've reached 20 points!",
|
||||||
|
"playAgain": "Play Again",
|
||||||
|
"correct": "Correct! Great job! 🎉",
|
||||||
|
"incorrect": "Incorrect. Try again! 💪"
|
||||||
|
}
|
||||||
20
MathHomeworkHelper/app/src/main/assets/translations/es.json
Normal file
20
MathHomeworkHelper/app/src/main/assets/translations/es.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"title": "Ayudante de Tareas de Matemáticas",
|
||||||
|
"subtitle": "Práctica de Multiplicación",
|
||||||
|
"difficultyLevel": "Nivel de Dificultad:",
|
||||||
|
"easy": "Fácil",
|
||||||
|
"hard": "Difícil",
|
||||||
|
"difficulty1": "1 dígito × 1 dígito (p. ej., 5 × 7)",
|
||||||
|
"difficulty2": "1 dígito × 2 dígitos (p. ej., 5 × 23)",
|
||||||
|
"difficulty3": "2 dígitos × 2 dígitos (p. ej., 23 × 45)",
|
||||||
|
"difficulty4": "2 dígitos × 3 dígitos (p. ej., 23 × 456)",
|
||||||
|
"difficulty5": "3 dígitos × 3 dígitos (p. ej., 234 × 567)",
|
||||||
|
"checkAnswer": "Verificar Respuesta",
|
||||||
|
"points": "Puntos:",
|
||||||
|
"newProblem": "Nuevo Problema",
|
||||||
|
"congratulations": "🎉 ¡Felicitaciones! 🎉",
|
||||||
|
"youReached20Points": "¡Has alcanzado 20 puntos!",
|
||||||
|
"playAgain": "Jugar de Nuevo",
|
||||||
|
"correct": "¡Correcto! ¡Excelente trabajo! 🎉",
|
||||||
|
"incorrect": "Incorrecto. ¡Intenta de nuevo! 💪"
|
||||||
|
}
|
||||||
20
MathHomeworkHelper/app/src/main/assets/translations/sv.json
Normal file
20
MathHomeworkHelper/app/src/main/assets/translations/sv.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"title": "Matematikläxhjälp",
|
||||||
|
"subtitle": "Multiplikationspraktik",
|
||||||
|
"difficultyLevel": "Svårighetsnivå:",
|
||||||
|
"easy": "Lätt",
|
||||||
|
"hard": "Svårt",
|
||||||
|
"difficulty1": "1 siffra × 1 siffra (t.ex. 5 × 7)",
|
||||||
|
"difficulty2": "1 siffra × 2 siffror (t.ex. 5 × 23)",
|
||||||
|
"difficulty3": "2 siffror × 2 siffror (t.ex. 23 × 45)",
|
||||||
|
"difficulty4": "2 siffror × 3 siffror (t.ex. 23 × 456)",
|
||||||
|
"difficulty5": "3 siffror × 3 siffror (t.ex. 234 × 567)",
|
||||||
|
"checkAnswer": "Kontrollera Svar",
|
||||||
|
"points": "Poäng:",
|
||||||
|
"newProblem": "Nytt Problem",
|
||||||
|
"congratulations": "🎉 Grattis! 🎉",
|
||||||
|
"youReached20Points": "Du har nått 20 poäng!",
|
||||||
|
"playAgain": "Spela Igen",
|
||||||
|
"correct": "Rätt! Bra jobbat! 🎉",
|
||||||
|
"incorrect": "Fel. Försök igen! 💪"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package com.example.mathhomeworkhelper;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.webkit.WebSettings;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private WebView webView;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
|
webView = findViewById(R.id.webview);
|
||||||
|
|
||||||
|
// Configure WebView settings
|
||||||
|
WebSettings webSettings = webView.getSettings();
|
||||||
|
|
||||||
|
// Enable JavaScript (required for your app)
|
||||||
|
webSettings.setJavaScriptEnabled(true);
|
||||||
|
|
||||||
|
// Enable DOM storage
|
||||||
|
webSettings.setDomStorageEnabled(true);
|
||||||
|
|
||||||
|
// Enable local storage
|
||||||
|
webSettings.setDatabaseEnabled(true);
|
||||||
|
|
||||||
|
// Set user agent
|
||||||
|
webSettings.setUserAgentString(webSettings.getUserAgentString());
|
||||||
|
|
||||||
|
// Enable zoom controls
|
||||||
|
webSettings.setBuiltInZoomControls(true);
|
||||||
|
webSettings.setDisplayZoomControls(false);
|
||||||
|
|
||||||
|
// Set default zoom level
|
||||||
|
webSettings.setDefaultZoom(WebSettings.ZoomDensity.MEDIUM);
|
||||||
|
|
||||||
|
// Allow file access
|
||||||
|
webSettings.setAllowFileAccess(true);
|
||||||
|
webSettings.setAllowContentAccess(true);
|
||||||
|
|
||||||
|
// For Android 5.0 and above, allow mixed content
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the local HTML file from assets
|
||||||
|
webView.loadUrl("file:///android_asset/index.html");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
// Allow back navigation within the WebView
|
||||||
|
if (webView.canGoBack()) {
|
||||||
|
webView.goBack();
|
||||||
|
} else {
|
||||||
|
super.onBackPressed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
MathHomeworkHelper/app/src/main/res/layout/activity_main.xml
Normal file
12
MathHomeworkHelper/app/src/main/res/layout/activity_main.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:id="@+id/webview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
5
MathHomeworkHelper/app/src/main/res/values/strings.xml
Normal file
5
MathHomeworkHelper/app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Math Homework Helper</string>
|
||||||
|
<string name="app_title">Math Homework Helper</string>
|
||||||
|
</resources>
|
||||||
24
MathHomeworkHelper/app/src/main/res/values/styles.xml
Normal file
24
MathHomeworkHelper/app/src/main/res/values/styles.xml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.MathHomeworkHelper" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<item name="colorPrimary">@color/purple_500</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||||
|
<item name="colorError">@color/red_500</item>
|
||||||
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<item name="colorOnError">@color/white</item>
|
||||||
|
<item name="colorOnBackground">@color/black</item>
|
||||||
|
<item name="colorOnSurface">@color/black</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="red_500">#FFFF0000</color>
|
||||||
|
</resources>
|
||||||
5
MathHomeworkHelper/build.gradle
Normal file
5
MathHomeworkHelper/build.gradle
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
plugins {
|
||||||
|
id 'com.android.application' version '8.1.0' apply false
|
||||||
|
id 'com.android.library' version '8.1.0' apply false
|
||||||
|
}
|
||||||
21
MathHomeworkHelper/gradle.properties
Normal file
21
MathHomeworkHelper/gradle.properties
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE are ignored when building from the
|
||||||
|
# command line. Check Gradle settings in the IDE if you notice discrepancies
|
||||||
|
# between the IDE and command line builds.
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
|
||||||
|
# AndroidX package structure to make it easier to discover and migrate existing, old-style packages
|
||||||
|
android.useAndroidX=true
|
||||||
|
|
||||||
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
# resources declared in the library itself and does not include resources from the
|
||||||
|
# dependencies, thereby reducing the size of the R class in the final APK.
|
||||||
|
android.nonTransitiveRClass=true
|
||||||
|
|
||||||
|
# Gradle memory settings
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m
|
||||||
16
MathHomeworkHelper/settings.gradle
Normal file
16
MathHomeworkHelper/settings.gradle
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootProject.name = "Math Homework Helper"
|
||||||
|
include ':app'
|
||||||
86
index.html
Normal file
86
index.html
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Math Homework Helper - Multiplication Practice</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Language Selector -->
|
||||||
|
<div class="language-selector">
|
||||||
|
<div class="language-flags" id="language-flags"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1 id="title">🎓 Math Homework Helper</h1>
|
||||||
|
<p class="subtitle" id="subtitle">Multiplication Practice</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- Difficulty Slider Section -->
|
||||||
|
<section class="difficulty-section">
|
||||||
|
<label for="difficulty-slider" class="difficulty-label">
|
||||||
|
Difficulty Level: <span id="difficulty-display">1</span>
|
||||||
|
</label>
|
||||||
|
<div class="slider-container">
|
||||||
|
<span class="difficulty-text">Easy</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="difficulty-slider"
|
||||||
|
min="1"
|
||||||
|
max="5"
|
||||||
|
value="1"
|
||||||
|
class="slider"
|
||||||
|
>
|
||||||
|
<span class="difficulty-text">Hard</span>
|
||||||
|
</div>
|
||||||
|
<div class="difficulty-info">
|
||||||
|
<p id="difficulty-description">1 digit × 1 digit (e.g., 5 × 7)</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Problem Display Section -->
|
||||||
|
<section class="problem-section">
|
||||||
|
<div class="problem-display" id="problem-display">
|
||||||
|
<!-- Problem will be rendered here -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Check Answer Button -->
|
||||||
|
<section class="answer-section">
|
||||||
|
<button id="submit-btn" class="submit-btn" style="width: 100%;">Check Answer</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Feedback Section -->
|
||||||
|
<section class="feedback-section">
|
||||||
|
<div id="feedback" class="feedback"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Score Section -->
|
||||||
|
<section class="score-section">
|
||||||
|
<div class="score-box">
|
||||||
|
<p class="score-label">Points:</p>
|
||||||
|
<p class="score-value" id="points-score">0/20</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- New Problem Button -->
|
||||||
|
<button id="new-problem-btn" class="new-problem-btn">New Problem</button>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Victory Modal -->
|
||||||
|
<div id="victory-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2>🎉 Congratulations! 🎉</h2>
|
||||||
|
<p>You've reached 20 points!</p>
|
||||||
|
<button id="play-again-btn" class="modal-btn">Play Again</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="translations.js"></script>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
651
script.js
Normal file
651
script.js
Normal file
|
|
@ -0,0 +1,651 @@
|
||||||
|
// Game State
|
||||||
|
let gameState = {
|
||||||
|
difficulty: 1,
|
||||||
|
currentProblem: null,
|
||||||
|
points: 0,
|
||||||
|
totalAnswers: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Difficulty configurations (will be updated with translations)
|
||||||
|
let difficultyConfig = {
|
||||||
|
1: {
|
||||||
|
num1Min: 1, num1Max: 9,
|
||||||
|
num2Min: 1, num2Max: 9,
|
||||||
|
descriptionKey: 'difficulty1'
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
num1Min: 10, num1Max: 99,
|
||||||
|
num2Min: 1, num2Max: 9,
|
||||||
|
descriptionKey: 'difficulty2'
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
num1Min: 10, num1Max: 99,
|
||||||
|
num2Min: 10, num2Max: 99,
|
||||||
|
descriptionKey: 'difficulty3'
|
||||||
|
},
|
||||||
|
4: {
|
||||||
|
num1Min: 100, num1Max: 999,
|
||||||
|
num2Min: 10, num2Max: 99,
|
||||||
|
descriptionKey: 'difficulty4'
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
num1Min: 10000, num1Max: 99999,
|
||||||
|
num2Min: 100, num2Max: 999,
|
||||||
|
descriptionKey: 'difficulty5'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
const difficultySlider = document.getElementById('difficulty-slider');
|
||||||
|
const difficultyDisplay = document.getElementById('difficulty-display');
|
||||||
|
const difficultyDescription = document.getElementById('difficulty-description');
|
||||||
|
const problemDisplay = document.getElementById('problem-display');
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
const feedbackDiv = document.getElementById('feedback');
|
||||||
|
const pointsScoreDisplay = document.getElementById('points-score');
|
||||||
|
const newProblemBtn = document.getElementById('new-problem-btn');
|
||||||
|
const victoryModal = document.getElementById('victory-modal');
|
||||||
|
const playAgainBtn = document.getElementById('play-again-btn');
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
difficultySlider.addEventListener('change', handleDifficultyChange);
|
||||||
|
submitBtn.addEventListener('click', handleSubmitAnswer);
|
||||||
|
newProblemBtn.addEventListener('click', generateNewProblem);
|
||||||
|
playAgainBtn.addEventListener('click', resetGame);
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
window.addEventListener('load', async () => {
|
||||||
|
await i18n.initialize();
|
||||||
|
initializeLanguageSelector();
|
||||||
|
updateUIText();
|
||||||
|
generateNewProblem();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle difficulty slider change
|
||||||
|
*/
|
||||||
|
function handleDifficultyChange() {
|
||||||
|
gameState.difficulty = parseInt(difficultySlider.value);
|
||||||
|
difficultyDisplay.textContent = gameState.difficulty;
|
||||||
|
difficultyDescription.textContent = difficultyConfig[gameState.difficulty].description;
|
||||||
|
generateNewProblem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random number between min and max (inclusive)
|
||||||
|
*/
|
||||||
|
function getRandomNumber(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new multiplication problem
|
||||||
|
*/
|
||||||
|
function generateNewProblem() {
|
||||||
|
const config = difficultyConfig[gameState.difficulty];
|
||||||
|
const num1 = getRandomNumber(config.num1Min, config.num1Max);
|
||||||
|
const num2 = getRandomNumber(config.num2Min, config.num2Max);
|
||||||
|
const answer = num1 * num2;
|
||||||
|
|
||||||
|
gameState.currentProblem = {
|
||||||
|
num1,
|
||||||
|
num2,
|
||||||
|
answer
|
||||||
|
};
|
||||||
|
|
||||||
|
displayProblem();
|
||||||
|
clearFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate intermediate steps for vertical multiplication
|
||||||
|
*/
|
||||||
|
function calculateIntermediateSteps(num1, num2) {
|
||||||
|
const num2Str = num2.toString();
|
||||||
|
const steps = [];
|
||||||
|
|
||||||
|
// Calculate partial products for each digit of num2
|
||||||
|
for (let i = num2Str.length - 1; i >= 0; i--) {
|
||||||
|
const digit = parseInt(num2Str[i]);
|
||||||
|
const partialProduct = num1 * digit;
|
||||||
|
const shiftAmount = num2Str.length - 1 - i;
|
||||||
|
steps.push({
|
||||||
|
digit,
|
||||||
|
product: partialProduct,
|
||||||
|
shift: shiftAmount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the multiplication problem
|
||||||
|
*/
|
||||||
|
function displayProblem() {
|
||||||
|
const { num1, num2 } = gameState.currentProblem;
|
||||||
|
const difficulty = gameState.difficulty;
|
||||||
|
|
||||||
|
if (difficulty === 1) {
|
||||||
|
// Horizontal layout for 1x1 with result
|
||||||
|
const answer = gameState.currentProblem.answer.toString();
|
||||||
|
let resultHTML = '<div class="line" style="justify-content: flex-end; gap: 2px;">';
|
||||||
|
for (let i = 0; i < answer.length; i++) {
|
||||||
|
resultHTML += `<input type="text" class="digit-input result-input" maxlength="1" data-pos="${i}" />`;
|
||||||
|
}
|
||||||
|
resultHTML += '</div>';
|
||||||
|
|
||||||
|
problemDisplay.innerHTML = `
|
||||||
|
<div class="multiplication-horizontal">
|
||||||
|
<span class="number">${num1}</span>
|
||||||
|
<span class="operator">×</span>
|
||||||
|
<span class="number">${num2}</span>
|
||||||
|
<span class="operator">=</span>
|
||||||
|
${resultHTML}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add event listeners to result digit inputs
|
||||||
|
setTimeout(() => {
|
||||||
|
addResultDigitInputListeners();
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
// Vertical layout for 2+ digits
|
||||||
|
const num1Str = num1.toString();
|
||||||
|
const num2Str = num2.toString();
|
||||||
|
|
||||||
|
// Calculate intermediate steps for reference
|
||||||
|
const steps = calculateIntermediateSteps(num1, num2);
|
||||||
|
gameState.currentProblem.steps = steps;
|
||||||
|
|
||||||
|
// Create input fields for intermediate steps only if num2 has more than 1 digit
|
||||||
|
let intermediateHTML = '';
|
||||||
|
if (num2 > 9) {
|
||||||
|
// Calculate the maximum width needed for intermediate steps
|
||||||
|
const maxResultWidth = gameState.currentProblem.answer.toString().length;
|
||||||
|
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
const stepProduct = steps[i].product.toString();
|
||||||
|
const totalWidth = stepProduct.length + steps[i].shift;
|
||||||
|
intermediateHTML += '<div class="line" style="justify-content: flex-start; gap: 5px;">';
|
||||||
|
|
||||||
|
// Each row is offset one position to the left
|
||||||
|
// Add offset spaces at the beginning (left side)
|
||||||
|
for (let offset = 0; offset < i; offset++) {
|
||||||
|
intermediateHTML += '<span style="width: 90px;"></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add empty spaces for shift
|
||||||
|
for (let s = 0; s < steps[i].shift; s++) {
|
||||||
|
intermediateHTML += '<span style="width: 90px;"></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add input fields for each digit
|
||||||
|
for (let j = 0; j < stepProduct.length; j++) {
|
||||||
|
intermediateHTML += `<input type="text" class="digit-input intermediate-input" maxlength="1" data-step="${i}" data-pos="${j}" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add trailing empty spaces on the right
|
||||||
|
const trailingSpaces = maxResultWidth - totalWidth - i;
|
||||||
|
for (let e = 0; e < trailingSpaces; e++) {
|
||||||
|
intermediateHTML += '<span style="width: 90px;"></span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
intermediateHTML += '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create input fields for final result
|
||||||
|
const answer = gameState.currentProblem.answer.toString();
|
||||||
|
let resultHTML = '<div class="line" style="justify-content: flex-end; gap: 2px;">';
|
||||||
|
for (let i = 0; i < answer.length; i++) {
|
||||||
|
resultHTML += `<input type="text" class="digit-input result-input" maxlength="1" data-pos="${i}" />`;
|
||||||
|
}
|
||||||
|
resultHTML += '</div>';
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="multiplication-vertical">
|
||||||
|
<div class="line">
|
||||||
|
<span>${num1Str}</span>
|
||||||
|
</div>
|
||||||
|
<div class="line">
|
||||||
|
<span class="operator">×</span>
|
||||||
|
<span>${num2Str}</span>
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (num2 > 9) {
|
||||||
|
html += `
|
||||||
|
<div id="intermediate-steps">
|
||||||
|
${intermediateHTML}
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div id="result-steps">
|
||||||
|
${resultHTML}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
problemDisplay.innerHTML = html;
|
||||||
|
|
||||||
|
// Add event listeners to digit inputs
|
||||||
|
setTimeout(() => {
|
||||||
|
addDigitInputListeners();
|
||||||
|
addResultDigitInputListeners();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add event listeners to digit input fields
|
||||||
|
*/
|
||||||
|
function addDigitInputListeners() {
|
||||||
|
const digitInputs = document.querySelectorAll('.digit-input:not(.result-input)');
|
||||||
|
|
||||||
|
digitInputs.forEach((input, index) => {
|
||||||
|
input.addEventListener('input', (e) => {
|
||||||
|
// Only allow digits
|
||||||
|
if (!/^\d?$/.test(e.target.value)) {
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to next input if digit entered
|
||||||
|
if (e.target.value && index < digitInputs.length - 1) {
|
||||||
|
digitInputs[index + 1].focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Backspace' && !e.target.value && index > 0) {
|
||||||
|
digitInputs[index - 1].focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add event listeners to result digit input fields
|
||||||
|
* Navigation: left after filling, wrap to rightmost of next line
|
||||||
|
*/
|
||||||
|
function addResultDigitInputListeners() {
|
||||||
|
const resultInputs = document.querySelectorAll('.result-input');
|
||||||
|
|
||||||
|
resultInputs.forEach((input, index) => {
|
||||||
|
input.addEventListener('input', (e) => {
|
||||||
|
// Only allow digits
|
||||||
|
if (!/^\d?$/.test(e.target.value)) {
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to LEFT if digit entered
|
||||||
|
if (e.target.value) {
|
||||||
|
if (index > 0) {
|
||||||
|
// Move to previous input (left)
|
||||||
|
resultInputs[index - 1].focus();
|
||||||
|
} else if (index === 0) {
|
||||||
|
// At the leftmost position, wrap to rightmost of previous line
|
||||||
|
const allLines = document.querySelectorAll('#result-steps .line, #intermediate-steps .line');
|
||||||
|
if (allLines.length > 1) {
|
||||||
|
const prevLineInputs = allLines[allLines.length - 2].querySelectorAll('input');
|
||||||
|
if (prevLineInputs.length > 0) {
|
||||||
|
prevLineInputs[prevLineInputs.length - 1].focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Backspace' && !e.target.value && index < resultInputs.length - 1) {
|
||||||
|
resultInputs[index + 1].focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find which digits are incorrect in the answer
|
||||||
|
*/
|
||||||
|
function findIncorrectDigits(userAnswer, correctAnswer) {
|
||||||
|
const userStr = userAnswer.toString();
|
||||||
|
const correctStr = correctAnswer.toString();
|
||||||
|
const incorrectPositions = [];
|
||||||
|
|
||||||
|
// Pad the shorter string with leading zeros for comparison
|
||||||
|
const maxLen = Math.max(userStr.length, correctStr.length);
|
||||||
|
const userPadded = userStr.padStart(maxLen, '0');
|
||||||
|
const correctPadded = correctStr.padStart(maxLen, '0');
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
if (userPadded[i] !== correctPadded[i]) {
|
||||||
|
incorrectPositions.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return incorrectPositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display answer with highlighted incorrect digits
|
||||||
|
*/
|
||||||
|
function displayAnswerWithErrors(userAnswer, correctAnswer) {
|
||||||
|
const incorrectPositions = findIncorrectDigits(userAnswer, correctAnswer);
|
||||||
|
const userStr = userAnswer.toString();
|
||||||
|
const correctStr = correctAnswer.toString();
|
||||||
|
const maxLen = Math.max(userStr.length, correctStr.length);
|
||||||
|
const userPadded = userStr.padStart(maxLen, '0');
|
||||||
|
|
||||||
|
let html = '<div style="font-size: 1.5em; margin-top: 15px;"><strong>Your answer:</strong> ';
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
const digit = userPadded[i];
|
||||||
|
if (incorrectPositions.includes(i)) {
|
||||||
|
html += `<span style="border: 3px solid #dc2626; padding: 5px 8px; margin: 0 2px; display: inline-block; background-color: #fee2e2;">${digit}</span>`;
|
||||||
|
} else {
|
||||||
|
html += digit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `<br><strong>Correct answer:</strong> ${correctStr}</div>`;
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user answer from input boxes
|
||||||
|
*/
|
||||||
|
function getUserAnswer() {
|
||||||
|
const resultInputs = document.querySelectorAll('.result-input');
|
||||||
|
let answer = '';
|
||||||
|
resultInputs.forEach(input => {
|
||||||
|
answer += input.value;
|
||||||
|
});
|
||||||
|
return answer === '' ? null : parseInt(answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user intermediate steps from input boxes
|
||||||
|
*/
|
||||||
|
function getUserIntermediateSteps() {
|
||||||
|
const steps = [];
|
||||||
|
const intermediateLines = document.querySelectorAll('#intermediate-steps .line');
|
||||||
|
|
||||||
|
intermediateLines.forEach((line, lineIndex) => {
|
||||||
|
const inputs = line.querySelectorAll('input');
|
||||||
|
let stepValue = '';
|
||||||
|
inputs.forEach(input => {
|
||||||
|
stepValue += input.value;
|
||||||
|
});
|
||||||
|
if (stepValue) {
|
||||||
|
steps.push(parseInt(stepValue));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle answer submission
|
||||||
|
*/
|
||||||
|
function handleSubmitAnswer() {
|
||||||
|
const userAnswer = getUserAnswer();
|
||||||
|
const correctAnswer = gameState.currentProblem.answer;
|
||||||
|
|
||||||
|
if (userAnswer === null) {
|
||||||
|
showFeedback('Please enter an answer!', 'incorrect');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameState.totalAnswers++;
|
||||||
|
|
||||||
|
// Check intermediate steps if they exist
|
||||||
|
const intermediateStepsDiv = document.getElementById('intermediate-steps');
|
||||||
|
let intermediateStepsCorrect = true;
|
||||||
|
let intermediateErrorMessage = '';
|
||||||
|
|
||||||
|
if (intermediateStepsDiv) {
|
||||||
|
const steps = gameState.currentProblem.steps;
|
||||||
|
const intermediateLines = document.querySelectorAll('#intermediate-steps .line');
|
||||||
|
|
||||||
|
intermediateLines.forEach((line, lineIndex) => {
|
||||||
|
const inputs = line.querySelectorAll('input');
|
||||||
|
let userStepValue = '';
|
||||||
|
inputs.forEach(input => {
|
||||||
|
userStepValue += input.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userStepValue) {
|
||||||
|
const userStepNum = parseInt(userStepValue);
|
||||||
|
const correctStepNum = steps[lineIndex].product;
|
||||||
|
|
||||||
|
if (userStepNum !== correctStepNum) {
|
||||||
|
intermediateStepsCorrect = false;
|
||||||
|
intermediateErrorMessage += `<br>Step ${lineIndex + 1}: You wrote ${userStepNum}, correct is ${correctStepNum}`;
|
||||||
|
|
||||||
|
// Highlight incorrect digits in this step
|
||||||
|
const correctStr = correctStepNum.toString();
|
||||||
|
const userStr = userStepValue;
|
||||||
|
inputs.forEach((input, digitIndex) => {
|
||||||
|
if (digitIndex < userStr.length && userStr[digitIndex] !== correctStr[correctStr.length - userStr.length + digitIndex]) {
|
||||||
|
input.classList.add('error');
|
||||||
|
} else if (digitIndex < userStr.length) {
|
||||||
|
input.classList.add('correct');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userAnswer === correctAnswer && intermediateStepsCorrect) {
|
||||||
|
gameState.points += 1;
|
||||||
|
showFeedback('🎉 Correct! +1 point', 'correct');
|
||||||
|
updateScore();
|
||||||
|
|
||||||
|
// Check if reached 20 points
|
||||||
|
if (gameState.points >= 20) {
|
||||||
|
showVictoryModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
generateNewProblem();
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
gameState.points = Math.max(0, gameState.points - 2);
|
||||||
|
|
||||||
|
// Mark incorrect result boxes with red color
|
||||||
|
if (userAnswer !== correctAnswer) {
|
||||||
|
const resultInputs = document.querySelectorAll('.result-input');
|
||||||
|
const userStr = userAnswer.toString();
|
||||||
|
const correctStr = correctAnswer.toString();
|
||||||
|
const maxLen = Math.max(userStr.length, correctStr.length);
|
||||||
|
const userPadded = userStr.padStart(maxLen, '0');
|
||||||
|
const correctPadded = correctStr.padStart(maxLen, '0');
|
||||||
|
|
||||||
|
resultInputs.forEach((input, index) => {
|
||||||
|
if (userPadded[index] !== correctPadded[index]) {
|
||||||
|
input.classList.add('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
feedbackDiv.textContent = '❌ Your answer is wrong, check the red boxes';
|
||||||
|
feedbackDiv.className = 'feedback incorrect';
|
||||||
|
updateScore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show feedback message
|
||||||
|
*/
|
||||||
|
function showFeedback(message, type) {
|
||||||
|
feedbackDiv.textContent = message;
|
||||||
|
feedbackDiv.className = `feedback ${type}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear feedback message
|
||||||
|
*/
|
||||||
|
function clearFeedback() {
|
||||||
|
feedbackDiv.textContent = '';
|
||||||
|
feedbackDiv.className = 'feedback empty';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update score display
|
||||||
|
*/
|
||||||
|
function updateScore() {
|
||||||
|
pointsScoreDisplay.textContent = `${gameState.points}/20`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show victory modal and create confetti
|
||||||
|
*/
|
||||||
|
function showVictoryModal() {
|
||||||
|
victoryModal.classList.add('show');
|
||||||
|
createConfetti();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create confetti animation
|
||||||
|
*/
|
||||||
|
function createConfetti() {
|
||||||
|
const colors = ['#2563eb', '#7c3aed', '#dc2626', '#16a34a', '#ea580c', '#f59e0b'];
|
||||||
|
const confettiCount = 50;
|
||||||
|
|
||||||
|
for (let i = 0; i < confettiCount; i++) {
|
||||||
|
const confetti = document.createElement('div');
|
||||||
|
confetti.className = 'confetti';
|
||||||
|
confetti.style.left = Math.random() * 100 + '%';
|
||||||
|
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
confetti.style.delay = Math.random() * 0.5 + 's';
|
||||||
|
confetti.style.animationDuration = (Math.random() * 2 + 2.5) + 's';
|
||||||
|
document.body.appendChild(confetti);
|
||||||
|
|
||||||
|
// Remove confetti element after animation
|
||||||
|
setTimeout(() => {
|
||||||
|
confetti.remove();
|
||||||
|
}, 3500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset game for new round
|
||||||
|
*/
|
||||||
|
function resetGame() {
|
||||||
|
gameState.points = 0;
|
||||||
|
gameState.totalAnswers = 0;
|
||||||
|
victoryModal.classList.remove('show');
|
||||||
|
updateScore();
|
||||||
|
generateNewProblem();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize language selector
|
||||||
|
*/
|
||||||
|
function initializeLanguageSelector() {
|
||||||
|
const languageFlagsContainer = document.getElementById('language-flags');
|
||||||
|
const languages = i18n.getSupportedLanguages();
|
||||||
|
|
||||||
|
languageFlagsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
Object.entries(languages).forEach(([langCode, langInfo]) => {
|
||||||
|
const flagButton = document.createElement('button');
|
||||||
|
flagButton.className = 'language-flag';
|
||||||
|
flagButton.textContent = langInfo.flag;
|
||||||
|
flagButton.title = langInfo.name;
|
||||||
|
flagButton.setAttribute('data-tooltip', langInfo.name);
|
||||||
|
|
||||||
|
if (langCode === i18n.getCurrentLanguage()) {
|
||||||
|
flagButton.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
flagButton.addEventListener('click', () => {
|
||||||
|
changeLanguage(langCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
languageFlagsContainer.appendChild(flagButton);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change language and update UI
|
||||||
|
*/
|
||||||
|
function changeLanguage(langCode) {
|
||||||
|
i18n.setLanguage(langCode);
|
||||||
|
|
||||||
|
// Update active flag
|
||||||
|
document.querySelectorAll('.language-flag').forEach(flag => {
|
||||||
|
flag.classList.remove('active');
|
||||||
|
});
|
||||||
|
const languages = i18n.getSupportedLanguages();
|
||||||
|
const flagButtons = document.querySelectorAll('.language-flag');
|
||||||
|
const langCodes = Object.keys(languages);
|
||||||
|
flagButtons.forEach((btn, index) => {
|
||||||
|
if (langCodes[index] === langCode) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateUIText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update all UI text based on current language
|
||||||
|
*/
|
||||||
|
function updateUIText() {
|
||||||
|
// Update header
|
||||||
|
document.getElementById('title').textContent = '🎓 ' + i18n.t('title');
|
||||||
|
document.getElementById('subtitle').textContent = i18n.t('subtitle');
|
||||||
|
|
||||||
|
// Update difficulty label
|
||||||
|
const difficultyLabel = document.querySelector('.difficulty-label');
|
||||||
|
if (difficultyLabel) {
|
||||||
|
difficultyLabel.innerHTML = i18n.t('difficultyLevel') + ' <span id="difficulty-display">' + gameState.difficulty + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update difficulty text labels
|
||||||
|
const difficultyTexts = document.querySelectorAll('.difficulty-text');
|
||||||
|
if (difficultyTexts.length >= 2) {
|
||||||
|
difficultyTexts[0].textContent = i18n.t('easy');
|
||||||
|
difficultyTexts[1].textContent = i18n.t('hard');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update difficulty description
|
||||||
|
const config = difficultyConfig[gameState.difficulty];
|
||||||
|
if (config && config.descriptionKey) {
|
||||||
|
document.getElementById('difficulty-description').textContent = i18n.t(config.descriptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update buttons
|
||||||
|
document.getElementById('submit-btn').textContent = i18n.t('checkAnswer');
|
||||||
|
document.getElementById('new-problem-btn').textContent = i18n.t('newProblem');
|
||||||
|
|
||||||
|
// Update score label
|
||||||
|
const scoreLabel = document.querySelector('.score-label');
|
||||||
|
if (scoreLabel) {
|
||||||
|
scoreLabel.textContent = i18n.t('points');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update modal
|
||||||
|
const modalTitle = document.querySelector('.modal-content h2');
|
||||||
|
const modalText = document.querySelector('.modal-content p');
|
||||||
|
const playAgainButton = document.getElementById('play-again-btn');
|
||||||
|
|
||||||
|
if (modalTitle) {
|
||||||
|
modalTitle.textContent = i18n.t('congratulations');
|
||||||
|
}
|
||||||
|
if (modalText) {
|
||||||
|
modalText.textContent = i18n.t('youReached20Points');
|
||||||
|
}
|
||||||
|
if (playAgainButton) {
|
||||||
|
playAgainButton.textContent = i18n.t('playAgain');
|
||||||
|
}
|
||||||
|
}
|
||||||
591
styles.css
Normal file
591
styles.css
Normal file
|
|
@ -0,0 +1,591 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||||
|
background: #87CEEB;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Language Selector */
|
||||||
|
.language-selector {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-flags {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
background: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-flag {
|
||||||
|
font-size: 1.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-flag:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-flag.active {
|
||||||
|
background: #87CEEB;
|
||||||
|
border-color: #2563eb;
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-flag-tooltip {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-flag-tooltip:hover::after {
|
||||||
|
content: attr(data-tooltip);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
z-index: 101;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 30px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
max-width: 1000px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 40px;
|
||||||
|
animation: slideIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 3em;
|
||||||
|
color: #2563eb;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: #7c3aed;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Difficulty Section */
|
||||||
|
.difficulty-section {
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #ea580c 100%);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 10px 25px rgba(220, 38, 38, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#difficulty-display {
|
||||||
|
background: white;
|
||||||
|
color: #dc2626;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-text {
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.95em;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
flex: 1;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-thumb:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-info {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#difficulty-description {
|
||||||
|
color: white;
|
||||||
|
font-size: 0.95em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Problem Section */
|
||||||
|
.problem-section {
|
||||||
|
background: linear-gradient(135deg, #dbeafe 0%, #e0e7ff 100%);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
min-height: 150px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 10px 25px rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-display {
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal multiplication (1x1) */
|
||||||
|
.multiplication-horizontal {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplication-horizontal .number {
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplication-horizontal .operator {
|
||||||
|
font-size: 2.5em;
|
||||||
|
color: #f5576c;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical multiplication (2+ digits) */
|
||||||
|
.multiplication-vertical {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplication-vertical .line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 5px;
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplication-vertical .operator {
|
||||||
|
color: #f5576c;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiplication-vertical .separator {
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
background: #333;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit-input {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
font-size: 1.4em;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #2563eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 8px rgba(124, 58, 237, 0.4);
|
||||||
|
background-color: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit-input.error {
|
||||||
|
border-color: #dc2626;
|
||||||
|
background-color: #fee2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.digit-input.correct {
|
||||||
|
border-color: #16a34a;
|
||||||
|
background-color: #dcfce7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Answer Section */
|
||||||
|
.answer-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 18px;
|
||||||
|
font-size: 1.3em;
|
||||||
|
border: 3px solid #2563eb;
|
||||||
|
border-radius: 15px;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-input:focus {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 15px rgba(124, 58, 237, 0.3);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-input::placeholder {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
padding: 18px 30px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 5px 15px rgba(37, 99, 235, 0.4);
|
||||||
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feedback Section */
|
||||||
|
.feedback-section {
|
||||||
|
min-height: 80px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 15px;
|
||||||
|
min-width: 100%;
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback.correct {
|
||||||
|
background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
|
||||||
|
color: #2d5016;
|
||||||
|
box-shadow: 0 5px 15px rgba(132, 250, 176, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback.incorrect {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
color: #8b0000;
|
||||||
|
box-shadow: 0 5px 15px rgba(250, 112, 154, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback.empty {
|
||||||
|
background: transparent;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Score Section */
|
||||||
|
.score-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-box {
|
||||||
|
background: linear-gradient(135deg, #16a34a 0%, #059669 100%);
|
||||||
|
padding: 20px 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 5px 15px rgba(22, 163, 74, 0.3);
|
||||||
|
min-width: 120px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-label {
|
||||||
|
font-size: 0.95em;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
font-size: 2.5em;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New Problem Button */
|
||||||
|
.new-problem-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 18px;
|
||||||
|
font-size: 1.3em;
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #ea580c 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 5px 15px rgba(220, 38, 38, 0.4);
|
||||||
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-problem-btn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 20px rgba(220, 38, 38, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-problem-btn:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Confetti Animation */
|
||||||
|
@keyframes confetti-fall {
|
||||||
|
to {
|
||||||
|
transform: translateY(100vh) rotateZ(360deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confetti {
|
||||||
|
position: fixed;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: confetti-fall 3s ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Popup */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: modalBounce 0.5s ease-out;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalBounce {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.5);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
color: #16a34a;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content p {
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn {
|
||||||
|
padding: 15px 40px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
background: linear-gradient(135deg, #16a34a 0%, #059669 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 5px 15px rgba(22, 163, 74, 0.4);
|
||||||
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 20px rgba(22, 163, 74, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-btn:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.container {
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-section {
|
||||||
|
padding: 25px;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-display {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-input,
|
||||||
|
.submit-btn {
|
||||||
|
font-size: 1.1em;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-box {
|
||||||
|
min-width: 100px;
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
97
test.html
Normal file
97
test.html
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Test Intermediate Steps</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>🎓 Math Homework Helper</h1>
|
||||||
|
<p class="subtitle">Test: Vertical Multiplication</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- Problem Display Section -->
|
||||||
|
<section class="problem-section">
|
||||||
|
<div class="problem-display" id="problem-display">
|
||||||
|
<div class="multiplication-vertical">
|
||||||
|
<div class="line">
|
||||||
|
<span>23</span>
|
||||||
|
</div>
|
||||||
|
<div class="line">
|
||||||
|
<span class="operator">×</span>
|
||||||
|
<span>7</span>
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
<div id="intermediate-steps">
|
||||||
|
<div class="line" style="justify-content: flex-end; gap: 2px;">
|
||||||
|
<input type="text" class="digit-input" maxlength="1" />
|
||||||
|
<input type="text" class="digit-input" maxlength="1" />
|
||||||
|
<input type="text" class="digit-input" maxlength="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
<div class="line" style="min-height: 2em; color: #2563eb;">
|
||||||
|
<span>?</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Answer Input Section -->
|
||||||
|
<section class="answer-section">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="answer-input"
|
||||||
|
class="answer-input"
|
||||||
|
placeholder="Enter your answer"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<button id="submit-btn" class="submit-btn">Check Answer</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Score Section -->
|
||||||
|
<section class="score-section">
|
||||||
|
<div class="score-box">
|
||||||
|
<p class="score-label">Points:</p>
|
||||||
|
<p class="score-value" id="points-score">0/20</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Test the digit input functionality
|
||||||
|
const digitInputs = document.querySelectorAll('.digit-input');
|
||||||
|
|
||||||
|
digitInputs.forEach((input, index) => {
|
||||||
|
input.addEventListener('input', (e) => {
|
||||||
|
// Only allow digits
|
||||||
|
if (!/^\d?$/.test(e.target.value)) {
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to next input if digit entered
|
||||||
|
if (e.target.value && index < digitInputs.length - 1) {
|
||||||
|
digitInputs[index + 1].focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Backspace' && !e.target.value && index > 0) {
|
||||||
|
digitInputs[index - 1].focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus first input on load
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
digitInputs[0].focus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
test2.html
Normal file
79
test2.html
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Test Result Input</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>🎓 Math Homework Helper</h1>
|
||||||
|
<p class="subtitle">Test: Result Input (Left-to-Right)</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
<!-- Problem Display Section -->
|
||||||
|
<section class="problem-section">
|
||||||
|
<div class="problem-display" id="problem-display">
|
||||||
|
<div class="multiplication-horizontal">
|
||||||
|
<span class="number">8</span>
|
||||||
|
<span class="operator">×</span>
|
||||||
|
<span class="number">5</span>
|
||||||
|
<span class="operator">=</span>
|
||||||
|
<div class="line" style="justify-content: flex-end; gap: 2px;">
|
||||||
|
<input type="text" class="digit-input result-input" maxlength="1" data-pos="0" />
|
||||||
|
<input type="text" class="digit-input result-input" maxlength="1" data-pos="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Score Section -->
|
||||||
|
<section class="score-section">
|
||||||
|
<div class="score-box">
|
||||||
|
<p class="score-label">Points:</p>
|
||||||
|
<p class="score-value" id="points-score">0/20</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Test the result digit input functionality
|
||||||
|
const resultInputs = document.querySelectorAll('.result-input');
|
||||||
|
|
||||||
|
resultInputs.forEach((input, index) => {
|
||||||
|
input.addEventListener('input', (e) => {
|
||||||
|
// Only allow digits
|
||||||
|
if (!/^\d?$/.test(e.target.value)) {
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to LEFT if digit entered
|
||||||
|
if (e.target.value) {
|
||||||
|
if (index > 0) {
|
||||||
|
// Move to previous input (left)
|
||||||
|
resultInputs[index - 1].focus();
|
||||||
|
} else if (index === 0) {
|
||||||
|
console.log('At leftmost position');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Backspace' && !e.target.value && index < resultInputs.length - 1) {
|
||||||
|
resultInputs[index + 1].focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus rightmost input on load
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
resultInputs[resultInputs.length - 1].focus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
342
tests.html
Normal file
342
tests.html
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Math Homework Helper - Unit Tests</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.test-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.test-title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
.test-case {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
border-left: 4px solid #ddd;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
.test-case.pass {
|
||||||
|
border-left-color: #16a34a;
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
.test-case.fail {
|
||||||
|
border-left-color: #dc2626;
|
||||||
|
background: #fef2f2;
|
||||||
|
}
|
||||||
|
.test-name {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.test-result {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.pass-text {
|
||||||
|
color: #16a34a;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.fail-text {
|
||||||
|
color: #dc2626;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.summary {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.summary.all-pass {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
.summary.some-fail {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🧪 Math Homework Helper - Unit Tests</h1>
|
||||||
|
<div id="test-results"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Test Framework
|
||||||
|
class TestRunner {
|
||||||
|
constructor() {
|
||||||
|
this.tests = [];
|
||||||
|
this.passed = 0;
|
||||||
|
this.failed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
this.tests.push({ name, status: 'pass', error: null });
|
||||||
|
this.passed++;
|
||||||
|
} catch (error) {
|
||||||
|
this.tests.push({ name, status: 'fail', error: error.message });
|
||||||
|
this.failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(condition, message) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(message || 'Assertion failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEqual(actual, expected, message) {
|
||||||
|
if (actual !== expected) {
|
||||||
|
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const resultsDiv = document.getElementById('test-results');
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Test cases
|
||||||
|
this.tests.forEach(test => {
|
||||||
|
const className = test.status === 'pass' ? 'pass' : 'fail';
|
||||||
|
const statusText = test.status === 'pass'
|
||||||
|
? '<span class="pass-text">✓ PASS</span>'
|
||||||
|
: '<span class="fail-text">✗ FAIL</span>';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="test-case ${className}">
|
||||||
|
<div class="test-name">${test.name}</div>
|
||||||
|
<div class="test-result">${statusText}${test.error ? ': ' + test.error : ''}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
const totalTests = this.passed + this.failed;
|
||||||
|
const summaryClass = this.failed === 0 ? 'all-pass' : 'some-fail';
|
||||||
|
const summaryText = this.failed === 0
|
||||||
|
? `✓ All ${totalTests} tests passed!`
|
||||||
|
: `✗ ${this.failed} of ${totalTests} tests failed`;
|
||||||
|
|
||||||
|
html += `<div class="summary ${summaryClass}">${summaryText}</div>`;
|
||||||
|
|
||||||
|
resultsDiv.innerHTML = `
|
||||||
|
<div class="test-container">
|
||||||
|
<div class="test-title">Test Results</div>
|
||||||
|
${html}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize test runner
|
||||||
|
const runner = new TestRunner();
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// UNIT TESTS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Test 1: getRandomNumber function
|
||||||
|
runner.test('getRandomNumber generates number within range', () => {
|
||||||
|
// Mock function since we need to test the logic
|
||||||
|
const getRandomNumber = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const num = getRandomNumber(1, 9);
|
||||||
|
runner.assert(num >= 1 && num <= 9, `Random number ${num} out of range [1, 9]`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 2: calculateIntermediateSteps for 23 × 45
|
||||||
|
runner.test('calculateIntermediateSteps calculates correct partial products', () => {
|
||||||
|
const calculateIntermediateSteps = (num1, num2) => {
|
||||||
|
const num2Str = num2.toString();
|
||||||
|
const steps = [];
|
||||||
|
for (let i = num2Str.length - 1; i >= 0; i--) {
|
||||||
|
const digit = parseInt(num2Str[i]);
|
||||||
|
const partialProduct = num1 * digit;
|
||||||
|
const shiftAmount = num2Str.length - 1 - i;
|
||||||
|
steps.push({ digit, product: partialProduct, shift: shiftAmount });
|
||||||
|
}
|
||||||
|
return steps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const steps = calculateIntermediateSteps(23, 45);
|
||||||
|
runner.assertEqual(steps.length, 2, 'Should have 2 steps for 2-digit multiplicand');
|
||||||
|
runner.assertEqual(steps[0].product, 115, 'First step: 23 × 5 = 115');
|
||||||
|
runner.assertEqual(steps[0].shift, 0, 'First step shift should be 0');
|
||||||
|
runner.assertEqual(steps[1].product, 92, 'Second step: 23 × 4 = 92');
|
||||||
|
runner.assertEqual(steps[1].shift, 1, 'Second step shift should be 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: findIncorrectDigits function
|
||||||
|
runner.test('findIncorrectDigits identifies wrong digits correctly', () => {
|
||||||
|
const findIncorrectDigits = (userAnswer, correctAnswer) => {
|
||||||
|
const userStr = userAnswer.toString();
|
||||||
|
const correctStr = correctAnswer.toString();
|
||||||
|
const incorrectPositions = [];
|
||||||
|
const maxLen = Math.max(userStr.length, correctStr.length);
|
||||||
|
const userPadded = userStr.padStart(maxLen, '0');
|
||||||
|
const correctPadded = correctStr.padStart(maxLen, '0');
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
if (userPadded[i] !== correctPadded[i]) {
|
||||||
|
incorrectPositions.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return incorrectPositions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const incorrect = findIncorrectDigits(123, 125);
|
||||||
|
runner.assertEqual(incorrect.length, 1, 'Should find 1 incorrect digit');
|
||||||
|
runner.assertEqual(incorrect[0], 2, 'Incorrect digit at position 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: Point calculation - correct answer
|
||||||
|
runner.test('Correct answer adds 1 point', () => {
|
||||||
|
let points = 0;
|
||||||
|
const userAnswer = 72;
|
||||||
|
const correctAnswer = 72;
|
||||||
|
|
||||||
|
if (userAnswer === correctAnswer) {
|
||||||
|
points += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
runner.assertEqual(points, 1, 'Should add 1 point for correct answer');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 5: Point calculation - incorrect answer
|
||||||
|
runner.test('Incorrect answer subtracts 2 points (minimum 0)', () => {
|
||||||
|
let points = 5;
|
||||||
|
const userAnswer = 70;
|
||||||
|
const correctAnswer = 72;
|
||||||
|
|
||||||
|
if (userAnswer !== correctAnswer) {
|
||||||
|
points = Math.max(0, points - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
runner.assertEqual(points, 3, 'Should subtract 2 points for incorrect answer');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 6: Point calculation - incorrect answer with 0 points
|
||||||
|
runner.test('Points cannot go below 0', () => {
|
||||||
|
let points = 1;
|
||||||
|
const userAnswer = 70;
|
||||||
|
const correctAnswer = 72;
|
||||||
|
|
||||||
|
if (userAnswer !== correctAnswer) {
|
||||||
|
points = Math.max(0, points - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
runner.assertEqual(points, 0, 'Points should not go below 0');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 7: Victory condition
|
||||||
|
runner.test('Victory triggered at 20 points', () => {
|
||||||
|
let points = 19;
|
||||||
|
let victory = false;
|
||||||
|
|
||||||
|
points += 1;
|
||||||
|
if (points >= 20) {
|
||||||
|
victory = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
runner.assert(victory, 'Victory should be triggered at 20 points');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 8: Difficulty configuration
|
||||||
|
runner.test('Difficulty levels have correct configurations', () => {
|
||||||
|
const difficultyConfig = {
|
||||||
|
1: { num1Min: 1, num1Max: 9, num2Min: 1, num2Max: 9 },
|
||||||
|
2: { num1Min: 10, num1Max: 99, num2Min: 1, num2Max: 9 },
|
||||||
|
3: { num1Min: 10, num1Max: 99, num2Min: 10, num2Max: 99 },
|
||||||
|
4: { num1Min: 100, num1Max: 999, num2Min: 10, num2Max: 99 },
|
||||||
|
5: { num1Min: 10000, num1Max: 99999, num2Min: 100, num2Max: 999 }
|
||||||
|
};
|
||||||
|
|
||||||
|
runner.assertEqual(difficultyConfig[1].num1Max, 9, 'Level 1: num1Max should be 9');
|
||||||
|
runner.assertEqual(difficultyConfig[2].num2Max, 9, 'Level 2: num2Max should be 9');
|
||||||
|
runner.assertEqual(difficultyConfig[3].num2Min, 10, 'Level 3: num2Min should be 10');
|
||||||
|
runner.assertEqual(difficultyConfig[5].num1Max, 99999, 'Level 5: num1Max should be 99999');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 9: Intermediate steps only for 2+ digit multiplicand
|
||||||
|
runner.test('Intermediate steps skipped for 1-digit multiplicand', () => {
|
||||||
|
const num2 = 7; // 1-digit
|
||||||
|
const shouldShowIntermediateSteps = num2 > 9;
|
||||||
|
|
||||||
|
runner.assert(!shouldShowIntermediateSteps, 'Should not show intermediate steps for 1-digit multiplicand');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 10: Intermediate steps shown for 2+ digit multiplicand
|
||||||
|
runner.test('Intermediate steps shown for 2+ digit multiplicand', () => {
|
||||||
|
const num2 = 45; // 2-digit
|
||||||
|
const shouldShowIntermediateSteps = num2 > 9;
|
||||||
|
|
||||||
|
runner.assert(shouldShowIntermediateSteps, 'Should show intermediate steps for 2+ digit multiplicand');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 11: Multiplication result calculation
|
||||||
|
runner.test('Multiplication result calculated correctly', () => {
|
||||||
|
const num1 = 23;
|
||||||
|
const num2 = 45;
|
||||||
|
const result = num1 * num2;
|
||||||
|
|
||||||
|
runner.assertEqual(result, 1035, '23 × 45 should equal 1035');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 12: Intermediate step verification
|
||||||
|
runner.test('Intermediate step verification detects errors', () => {
|
||||||
|
const userStepValue = '115';
|
||||||
|
const correctStepNum = 115;
|
||||||
|
const isCorrect = parseInt(userStepValue) === correctStepNum;
|
||||||
|
|
||||||
|
runner.assert(isCorrect, 'Intermediate step should be verified correctly');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 13: Intermediate step verification - incorrect
|
||||||
|
runner.test('Intermediate step verification detects incorrect values', () => {
|
||||||
|
const userStepValue = '116';
|
||||||
|
const correctStepNum = 115;
|
||||||
|
const isCorrect = parseInt(userStepValue) === correctStepNum;
|
||||||
|
|
||||||
|
runner.assert(!isCorrect, 'Incorrect intermediate step should be detected');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 14: Answer validation - empty answer
|
||||||
|
runner.test('Empty answer is rejected', () => {
|
||||||
|
const answer = '';
|
||||||
|
const isValid = answer !== '';
|
||||||
|
|
||||||
|
runner.assert(!isValid, 'Empty answer should be invalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 15: Answer validation - valid answer
|
||||||
|
runner.test('Valid numeric answer is accepted', () => {
|
||||||
|
const answer = '72';
|
||||||
|
const isValid = answer !== '' && !isNaN(parseInt(answer));
|
||||||
|
|
||||||
|
runner.assert(isValid, 'Valid numeric answer should be accepted');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render results
|
||||||
|
runner.render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
184
translations.js
Normal file
184
translations.js
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
// Translation Manager
|
||||||
|
class TranslationManager {
|
||||||
|
constructor() {
|
||||||
|
this.currentLanguage = 'en'; // Will be set during initialization
|
||||||
|
this.translations = {};
|
||||||
|
this.supportedLanguages = {
|
||||||
|
'en': { name: 'English', flag: '🇬🇧' },
|
||||||
|
'es': { name: 'Español', flag: '🇪🇸' },
|
||||||
|
'sv': { name: 'Svenska', flag: '🇸🇪' },
|
||||||
|
'el': { name: 'Ελληνικά', flag: '🇬🇷' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Country to language mapping
|
||||||
|
this.countryToLanguage = {
|
||||||
|
// Spanish-speaking countries
|
||||||
|
'ES': 'es', 'MX': 'es', 'AR': 'es', 'CO': 'es', 'PE': 'es', 'VE': 'es',
|
||||||
|
'CL': 'es', 'EC': 'es', 'BO': 'es', 'PY': 'es', 'UY': 'es', 'CU': 'es',
|
||||||
|
'DO': 'es', 'GT': 'es', 'HN': 'es', 'SV': 'es', 'NI': 'es', 'CR': 'es',
|
||||||
|
'PA': 'es', 'BZ': 'es', 'EQ': 'es',
|
||||||
|
// Swedish-speaking countries
|
||||||
|
'SE': 'sv', 'FI': 'sv', 'AX': 'sv',
|
||||||
|
// Greek-speaking countries
|
||||||
|
'GR': 'el', 'CY': 'el',
|
||||||
|
// Default to English for all others
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect language with geolocation fallback
|
||||||
|
async detectLanguageWithGeolocation() {
|
||||||
|
// Step 1: Check localStorage for saved preference
|
||||||
|
const savedLanguage = localStorage.getItem('preferredLanguage');
|
||||||
|
if (savedLanguage && this.supportedLanguages[savedLanguage]) {
|
||||||
|
console.log('Using saved language preference:', savedLanguage);
|
||||||
|
return savedLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Try browser language detection
|
||||||
|
const browserLang = this.detectBrowserLanguage();
|
||||||
|
if (browserLang !== 'en') {
|
||||||
|
console.log('Using browser language:', browserLang);
|
||||||
|
return browserLang;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Try geolocation API to get country and map to language
|
||||||
|
try {
|
||||||
|
const countryCode = await this.getCountryFromGeolocation();
|
||||||
|
if (countryCode) {
|
||||||
|
const language = this.countryToLanguage[countryCode] || 'en';
|
||||||
|
console.log('Using geolocation-based language:', language, 'from country:', countryCode);
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Geolocation failed, continuing with fallback:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Fallback to English
|
||||||
|
console.log('Falling back to English');
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect browser language
|
||||||
|
detectBrowserLanguage() {
|
||||||
|
const browserLang = navigator.language || navigator.userLanguage;
|
||||||
|
const langCode = browserLang.split('-')[0];
|
||||||
|
|
||||||
|
// Check if the detected language is supported
|
||||||
|
if (this.supportedLanguages[langCode]) {
|
||||||
|
return langCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return 'en' if not supported (will trigger geolocation)
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get country code from geolocation API
|
||||||
|
async getCountryFromGeolocation() {
|
||||||
|
const timeout = 5000; // 5 second timeout
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
|
// Try ip-api.com (free, no auth required)
|
||||||
|
const response = await fetch('https://ipapi.co/json/', {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: { 'Accept': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API returned status ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const countryCode = data.country_code;
|
||||||
|
|
||||||
|
if (countryCode && typeof countryCode === 'string') {
|
||||||
|
console.log('Geolocation API returned country:', countryCode);
|
||||||
|
return countryCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No country code in response');
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
console.warn('Geolocation API request timed out');
|
||||||
|
} else {
|
||||||
|
console.warn('Geolocation API error:', error.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load translation file
|
||||||
|
async loadTranslation(language) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`translations/${language}.json`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load ${language} translation`);
|
||||||
|
}
|
||||||
|
this.translations[language] = await response.json();
|
||||||
|
this.currentLanguage = language;
|
||||||
|
localStorage.setItem('preferredLanguage', language);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading translation:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get translated string
|
||||||
|
t(key) {
|
||||||
|
if (this.translations[this.currentLanguage] && this.translations[this.currentLanguage][key]) {
|
||||||
|
return this.translations[this.currentLanguage][key];
|
||||||
|
}
|
||||||
|
// Fallback to English if key not found
|
||||||
|
if (this.translations['en'] && this.translations['en'][key]) {
|
||||||
|
return this.translations['en'][key];
|
||||||
|
}
|
||||||
|
// Return key if no translation found
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current language
|
||||||
|
getCurrentLanguage() {
|
||||||
|
return this.currentLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all supported languages
|
||||||
|
getSupportedLanguages() {
|
||||||
|
return this.supportedLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set language
|
||||||
|
setLanguage(language) {
|
||||||
|
if (this.supportedLanguages[language]) {
|
||||||
|
this.currentLanguage = language;
|
||||||
|
localStorage.setItem('preferredLanguage', language);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize translations
|
||||||
|
async initialize() {
|
||||||
|
// Detect language using geolocation fallback chain
|
||||||
|
this.currentLanguage = await this.detectLanguageWithGeolocation();
|
||||||
|
|
||||||
|
// Load all supported language files
|
||||||
|
const loadPromises = Object.keys(this.supportedLanguages).map(lang =>
|
||||||
|
this.loadTranslation(lang)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(loadPromises);
|
||||||
|
|
||||||
|
// Ensure current language is set correctly
|
||||||
|
if (!this.translations[this.currentLanguage]) {
|
||||||
|
this.currentLanguage = 'en';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create global instance
|
||||||
|
const i18n = new TranslationManager();
|
||||||
20
translations/el.json
Normal file
20
translations/el.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"title": "Βοηθός Μαθηματικών Εργασιών",
|
||||||
|
"subtitle": "Εξάσκηση Πολλαπλασιασμού",
|
||||||
|
"difficultyLevel": "Επίπεδο Δυσκολίας:",
|
||||||
|
"easy": "Εύκολο",
|
||||||
|
"hard": "Δύσκολο",
|
||||||
|
"difficulty1": "1 ψηφίο × 1 ψηφίο (π.χ. 5 × 7)",
|
||||||
|
"difficulty2": "1 ψηφίο × 2 ψηφία (π.χ. 5 × 23)",
|
||||||
|
"difficulty3": "2 ψηφία × 2 ψηφία (π.χ. 23 × 45)",
|
||||||
|
"difficulty4": "2 ψηφία × 3 ψηφία (π.χ. 23 × 456)",
|
||||||
|
"difficulty5": "3 ψηφία × 3 ψηφία (π.χ. 234 × 567)",
|
||||||
|
"checkAnswer": "Έλεγχος Απάντησης",
|
||||||
|
"points": "Πόντοι:",
|
||||||
|
"newProblem": "Νέο Πρόβλημα",
|
||||||
|
"congratulations": "🎉 Συγχαρητήρια! 🎉",
|
||||||
|
"youReached20Points": "Έφτασες τους 20 πόντους!",
|
||||||
|
"playAgain": "Παίξε Ξανά",
|
||||||
|
"correct": "Σωστό! Εξαιρετική δουλειά! 🎉",
|
||||||
|
"incorrect": "Λάθος. Προσπάθησε ξανά! 💪"
|
||||||
|
}
|
||||||
20
translations/en.json
Normal file
20
translations/en.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"title": "Math Homework Helper",
|
||||||
|
"subtitle": "Multiplication Practice",
|
||||||
|
"difficultyLevel": "Difficulty Level:",
|
||||||
|
"easy": "Easy",
|
||||||
|
"hard": "Hard",
|
||||||
|
"difficulty1": "1 digit × 1 digit (e.g., 5 × 7)",
|
||||||
|
"difficulty2": "1 digit × 2 digits (e.g., 5 × 23)",
|
||||||
|
"difficulty3": "2 digits × 2 digits (e.g., 23 × 45)",
|
||||||
|
"difficulty4": "2 digits × 3 digits (e.g., 23 × 456)",
|
||||||
|
"difficulty5": "3 digits × 3 digits (e.g., 234 × 567)",
|
||||||
|
"checkAnswer": "Check Answer",
|
||||||
|
"points": "Points:",
|
||||||
|
"newProblem": "New Problem",
|
||||||
|
"congratulations": "🎉 Congratulations! 🎉",
|
||||||
|
"youReached20Points": "You've reached 20 points!",
|
||||||
|
"playAgain": "Play Again",
|
||||||
|
"correct": "Correct! Great job! 🎉",
|
||||||
|
"incorrect": "Incorrect. Try again! 💪"
|
||||||
|
}
|
||||||
20
translations/es.json
Normal file
20
translations/es.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"title": "Ayudante de Tareas de Matemáticas",
|
||||||
|
"subtitle": "Práctica de Multiplicación",
|
||||||
|
"difficultyLevel": "Nivel de Dificultad:",
|
||||||
|
"easy": "Fácil",
|
||||||
|
"hard": "Difícil",
|
||||||
|
"difficulty1": "1 dígito × 1 dígito (p. ej., 5 × 7)",
|
||||||
|
"difficulty2": "1 dígito × 2 dígitos (p. ej., 5 × 23)",
|
||||||
|
"difficulty3": "2 dígitos × 2 dígitos (p. ej., 23 × 45)",
|
||||||
|
"difficulty4": "2 dígitos × 3 dígitos (p. ej., 23 × 456)",
|
||||||
|
"difficulty5": "3 dígitos × 3 dígitos (p. ej., 234 × 567)",
|
||||||
|
"checkAnswer": "Verificar Respuesta",
|
||||||
|
"points": "Puntos:",
|
||||||
|
"newProblem": "Nuevo Problema",
|
||||||
|
"congratulations": "🎉 ¡Felicitaciones! 🎉",
|
||||||
|
"youReached20Points": "¡Has alcanzado 20 puntos!",
|
||||||
|
"playAgain": "Jugar de Nuevo",
|
||||||
|
"correct": "¡Correcto! ¡Excelente trabajo! 🎉",
|
||||||
|
"incorrect": "Incorrecto. ¡Intenta de nuevo! 💪"
|
||||||
|
}
|
||||||
20
translations/sv.json
Normal file
20
translations/sv.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"title": "Matematikläxhjälp",
|
||||||
|
"subtitle": "Multiplikationspraktik",
|
||||||
|
"difficultyLevel": "Svårighetsnivå:",
|
||||||
|
"easy": "Lätt",
|
||||||
|
"hard": "Svårt",
|
||||||
|
"difficulty1": "1 siffra × 1 siffra (t.ex. 5 × 7)",
|
||||||
|
"difficulty2": "1 siffra × 2 siffror (t.ex. 5 × 23)",
|
||||||
|
"difficulty3": "2 siffror × 2 siffror (t.ex. 23 × 45)",
|
||||||
|
"difficulty4": "2 siffror × 3 siffror (t.ex. 23 × 456)",
|
||||||
|
"difficulty5": "3 siffror × 3 siffror (t.ex. 234 × 567)",
|
||||||
|
"checkAnswer": "Kontrollera Svar",
|
||||||
|
"points": "Poäng:",
|
||||||
|
"newProblem": "Nytt Problem",
|
||||||
|
"congratulations": "🎉 Grattis! 🎉",
|
||||||
|
"youReached20Points": "Du har nått 20 poäng!",
|
||||||
|
"playAgain": "Spela Igen",
|
||||||
|
"correct": "Rätt! Bra jobbat! 🎉",
|
||||||
|
"incorrect": "Fel. Försök igen! 💪"
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue