Spaces:
Sleeping
Sleeping
equal step 2 game
Browse files- Dockerfile +2 -2
- README.md +163 -157
- index.html +1 -1
- package-lock.json +4 -4
- package.json +2 -2
- src/App.css +224 -90
- src/App.tsx +227 -252
Dockerfile
CHANGED
|
@@ -23,5 +23,5 @@ RUN npm install -g serve
|
|
| 23 |
COPY --from=build /app/dist ./dist
|
| 24 |
|
| 25 |
# Expose port and start server
|
| 26 |
-
EXPOSE
|
| 27 |
-
CMD ["serve", "-s", "dist", "-l", "
|
|
|
|
| 23 |
COPY --from=build /app/dist ./dist
|
| 24 |
|
| 25 |
# Expose port and start server
|
| 26 |
+
EXPOSE 7860
|
| 27 |
+
CMD ["serve", "-s", "dist", "-l", "7860"]
|
README.md
CHANGED
|
@@ -1,185 +1,191 @@
|
|
| 1 |
---
|
| 2 |
-
title: Equal
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
-
app_port:
|
| 9 |
---
|
| 10 |
-
# Colorful Semantics - To Whom? Game
|
| 11 |
|
| 12 |
-
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
-
|
| 17 |
-
- Visual and animated feedback for correct answers
|
| 18 |
-
- Support for both images and videos
|
| 19 |
-
- Customizable questions and answers
|
| 20 |
|
| 21 |
-
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
| 27 |
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
-
|
| 34 |
-
{
|
| 35 |
-
"file": "media/your_image.jpg",
|
| 36 |
-
"who": "The boy",
|
| 37 |
-
"doing": "is giving",
|
| 38 |
-
"what": "a book",
|
| 39 |
-
"to_whom": "to the teacher",
|
| 40 |
-
"distractors": ["to Mom", "to the dog", "to Grandma"]
|
| 41 |
-
}
|
| 42 |
-
```
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
-
- `who`: The subject of the sentence
|
| 48 |
-
- `doing`: The verb phrase
|
| 49 |
-
- `what`: The object
|
| 50 |
-
- `to_whom`: The correct answer (recipient)
|
| 51 |
-
- `distractors`: Array of incorrect options
|
| 52 |
-
4. Restart the application to see your new questions
|
| 53 |
|
| 54 |
-
|
| 55 |
|
| 56 |
```bash
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
# Install dependencies
|
| 58 |
npm install
|
| 59 |
|
| 60 |
# Start development server
|
| 61 |
npm run dev
|
|
|
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
# Build for production
|
| 64 |
npm run build
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
```
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
fly
|
| 136 |
-
eat
|
| 137 |
-
dry
|
| 138 |
-
play
|
| 139 |
-
climb
|
| 140 |
-
scratch
|
| 141 |
-
cut
|
| 142 |
-
brush
|
| 143 |
-
bounce
|
| 144 |
-
argue
|
| 145 |
-
work
|
| 146 |
-
fight
|
| 147 |
-
practice
|
| 148 |
-
throw
|
| 149 |
-
take
|
| 150 |
-
bring
|
| 151 |
-
take
|
| 152 |
-
wipe
|
| 153 |
-
drink
|
| 154 |
-
|
| 155 |
-
## what
|
| 156 |
-
|
| 157 |
-
flower
|
| 158 |
-
icecream
|
| 159 |
-
pen
|
| 160 |
-
pencil
|
| 161 |
-
socks
|
| 162 |
-
underwear
|
| 163 |
-
coat
|
| 164 |
-
wellies
|
| 165 |
-
shoes
|
| 166 |
-
spoon
|
| 167 |
-
fork
|
| 168 |
-
food
|
| 169 |
-
bread
|
| 170 |
-
butter
|
| 171 |
-
salt
|
| 172 |
-
chilly
|
| 173 |
-
bathroom
|
| 174 |
-
towel
|
| 175 |
-
coffee
|
| 176 |
-
tea
|
| 177 |
-
milk
|
| 178 |
-
almond
|
| 179 |
-
monkey nuts
|
| 180 |
-
upstairs
|
| 181 |
-
downstairs
|
| 182 |
-
|
| 183 |
-
## License
|
| 184 |
-
|
| 185 |
-
MIT
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Equal 2-Step Game
|
| 3 |
+
emoji: ⚖️
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
app_port: 7860
|
| 9 |
---
|
|
|
|
| 10 |
|
| 11 |
+
# Equal 2-Step Game ⚖️
|
| 12 |
|
| 13 |
+
An interactive educational game that teaches equality comparison and counting skills through engaging emoji-based questions. Perfect for children learning basic math concepts!
|
| 14 |
|
| 15 |
+
## 🎮 How to Play
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
The game consists of **two sequential steps** that help children develop comparison and counting skills:
|
| 18 |
|
| 19 |
+
### Step 1: Equality Check
|
| 20 |
+
- Look at the two sides with emojis
|
| 21 |
+
- Count the items on each side
|
| 22 |
+
- Answer the question: **"Are they equal?"**
|
| 23 |
+
- Choose between **"Equal"** or **"Not Equal"**
|
| 24 |
|
| 25 |
+
### Step 2: Comparison (Only if Not Equal)
|
| 26 |
+
- If you correctly answered "Not Equal" in Step 1, you proceed to Step 2
|
| 27 |
+
- The game asks either:
|
| 28 |
+
- **"Which side has more?"** or
|
| 29 |
+
- **"Which side has fewer?"**
|
| 30 |
+
- Click on the correct side to answer
|
| 31 |
|
| 32 |
+
### Game Flow
|
| 33 |
+
1. **Equal sides**: Step 1 → Correct answer → New question
|
| 34 |
+
2. **Unequal sides**: Step 1 → Step 2 → Correct answer → New question
|
| 35 |
|
| 36 |
+
## ✨ Features
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
- **Visual Learning**: Uses colorful emojis from different categories (animals, fruits, objects, hearts, stars)
|
| 39 |
+
- **Two-Step Logic**: Teaches both equality and comparison concepts
|
| 40 |
+
- **Immediate Feedback**: Shows "Correct! 🎉" or "Try again! 🤔" messages
|
| 41 |
+
- **Auto-Generated Questions**: Random emoji selection and counts (1-10 items)
|
| 42 |
+
- **Responsive Design**: Works on desktop, tablet, and mobile devices
|
| 43 |
+
- **Clean UI**: Simple, child-friendly interface with large buttons and clear visuals
|
| 44 |
|
| 45 |
+
## 🚀 How to Run
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
+
### Option 1: Development Mode
|
| 48 |
|
| 49 |
```bash
|
| 50 |
+
# Clone the repository
|
| 51 |
+
git clone <repository-url>
|
| 52 |
+
cd equal-2-step-game
|
| 53 |
+
|
| 54 |
# Install dependencies
|
| 55 |
npm install
|
| 56 |
|
| 57 |
# Start development server
|
| 58 |
npm run dev
|
| 59 |
+
```
|
| 60 |
|
| 61 |
+
The game will be available at `http://localhost:5173`
|
| 62 |
+
|
| 63 |
+
### Option 2: Production Build
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
# Build for production
|
| 67 |
npm run build
|
| 68 |
+
|
| 69 |
+
# Preview production build
|
| 70 |
+
npm run preview
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Option 3: Docker
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
# Build Docker image
|
| 77 |
+
docker build -t equal-2-step-game .
|
| 78 |
+
|
| 79 |
+
# Run container
|
| 80 |
+
docker run -p 3000:3000 equal-2-step-game
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
The game will be available at `http://localhost:3000`
|
| 84 |
+
|
| 85 |
+
## 🎯 Educational Benefits
|
| 86 |
+
|
| 87 |
+
- **Counting Skills**: Children practice counting objects up to 10
|
| 88 |
+
- **Equality Concepts**: Understanding when quantities are the same
|
| 89 |
+
- **Comparison Skills**: Learning "more than" and "fewer than" concepts
|
| 90 |
+
- **Visual Recognition**: Identifying and categorizing different emoji types
|
| 91 |
+
- **Logical Thinking**: Following two-step problem-solving processes
|
| 92 |
+
|
| 93 |
+
## 🎨 Game Categories
|
| 94 |
+
|
| 95 |
+
The game uses emojis from 5 different categories:
|
| 96 |
+
|
| 97 |
+
- **Animals**: 🐶 🐱 🐭 🐹 🐰 🦊 🐻 🐼 🐸 🐷
|
| 98 |
+
- **Fruits**: 🍎 🍊 🍋 🍌 🍉 🍇 🍓 🫐 🍒 🥝
|
| 99 |
+
- **Objects**: ⚽ 🏀 🎾 🏈 🎱 🎪 🎨 🎭 🎪 🎯
|
| 100 |
+
- **Hearts**: 💙 💚 💛 💜 🤍 🖤 🤎 💗 💖 💕
|
| 101 |
+
- **Stars**: ⭐ 🌟 💫 ✨ 🌠 ⚡ 🔥 ❄️ ☀️ 🌙
|
| 102 |
+
|
| 103 |
+
## 🛠️ Technical Details
|
| 104 |
+
|
| 105 |
+
### Technologies Used
|
| 106 |
+
- **React 19** with TypeScript
|
| 107 |
+
- **Vite** for build tooling
|
| 108 |
+
- **CSS3** with modern features (Grid, Flexbox, Animations)
|
| 109 |
+
- **Docker** for deployment
|
| 110 |
+
|
| 111 |
+
### Project Structure
|
| 112 |
+
```
|
| 113 |
+
src/
|
| 114 |
+
├── App.tsx # Main game component
|
| 115 |
+
├── App.css # Game styling
|
| 116 |
+
├── main.tsx # App entry point
|
| 117 |
+
└── index.css # Global styles
|
| 118 |
+
|
| 119 |
+
public/
|
| 120 |
+
├── favicon.svg # App icon
|
| 121 |
+
└── index.html # HTML template
|
| 122 |
```
|
| 123 |
|
| 124 |
+
### Game Logic
|
| 125 |
+
- **Question Generation**: Random emoji selection from categories
|
| 126 |
+
- **Count Generation**: Random numbers 1-10 for each side
|
| 127 |
+
- **Equality Control**: 50% chance for equal vs unequal questions
|
| 128 |
+
- **Step Control**: Step 2 only appears for unequal quantities
|
| 129 |
+
- **Safety Checks**: Prevents step 2 from showing with equal sides
|
| 130 |
+
|
| 131 |
+
## 🎨 Customization
|
| 132 |
+
|
| 133 |
+
You can easily customize the game by modifying the emoji categories in `src/App.tsx`:
|
| 134 |
+
|
| 135 |
+
```typescript
|
| 136 |
+
const emojiCategories = {
|
| 137 |
+
animals: ['🐶', '🐱', ...], // Add your animal emojis
|
| 138 |
+
fruits: ['🍎', '🍊', ...], // Add your fruit emojis
|
| 139 |
+
// Add more categories...
|
| 140 |
+
};
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
## 📱 Mobile Support
|
| 144 |
+
|
| 145 |
+
The game is fully responsive and optimized for:
|
| 146 |
+
- **Desktop**: Full-featured experience with hover effects
|
| 147 |
+
- **Tablet**: Touch-friendly interface with optimized sizing
|
| 148 |
+
- **Mobile**: Compact layout with large touch targets
|
| 149 |
+
|
| 150 |
+
## 🚀 Deployment
|
| 151 |
+
|
| 152 |
+
### Hugging Face Spaces
|
| 153 |
+
This project is configured for easy deployment to Hugging Face Spaces using Docker.
|
| 154 |
+
|
| 155 |
+
### Other Platforms
|
| 156 |
+
The built project can be deployed to any static hosting service:
|
| 157 |
+
- Vercel
|
| 158 |
+
- Netlify
|
| 159 |
+
- GitHub Pages
|
| 160 |
+
- AWS S3
|
| 161 |
+
- And more...
|
| 162 |
+
|
| 163 |
+
## 🤝 Contributing
|
| 164 |
+
|
| 165 |
+
1. Fork the repository
|
| 166 |
+
2. Create a feature branch: `git checkout -b feature-name`
|
| 167 |
+
3. Make your changes
|
| 168 |
+
4. Test thoroughly
|
| 169 |
+
5. Submit a pull request
|
| 170 |
+
|
| 171 |
+
## 📄 License
|
| 172 |
+
|
| 173 |
+
MIT License - feel free to use this project for educational purposes!
|
| 174 |
+
|
| 175 |
+
## 🎓 Age Recommendation
|
| 176 |
+
|
| 177 |
+
This game is designed for children ages **3-8** who are learning:
|
| 178 |
+
- Basic counting (1-10)
|
| 179 |
+
- Equality concepts
|
| 180 |
+
- Comparison skills
|
| 181 |
+
- Visual recognition
|
| 182 |
+
|
| 183 |
+
Perfect for:
|
| 184 |
+
- **Preschool** mathematics preparation
|
| 185 |
+
- **Kindergarten** counting practice
|
| 186 |
+
- **Early elementary** comparison skills
|
| 187 |
+
- **Special education** visual learning support
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
Made with ❤️ for young learners everywhere!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
<title>
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
<div id="root"></div>
|
|
|
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Equal 2-Step Game</title>
|
| 8 |
</head>
|
| 9 |
<body>
|
| 10 |
<div id="root"></div>
|
package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "
|
| 3 |
-
"version": "
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
-
"name": "
|
| 9 |
-
"version": "
|
| 10 |
"dependencies": {
|
| 11 |
"react": "^19.0.0",
|
| 12 |
"react-dom": "^19.0.0"
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "equal-2-step-game",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
"lockfileVersion": 3,
|
| 5 |
"requires": true,
|
| 6 |
"packages": {
|
| 7 |
"": {
|
| 8 |
+
"name": "equal-2-step-game",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
"react": "^19.0.0",
|
| 12 |
"react-dom": "^19.0.0"
|
package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "
|
| 3 |
"private": true,
|
| 4 |
"version": "1.0.0",
|
| 5 |
"type": "module",
|
| 6 |
-
"description": "
|
| 7 |
"scripts": {
|
| 8 |
"dev": "vite",
|
| 9 |
"build": "vite build",
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "equal-2-step-game",
|
| 3 |
"private": true,
|
| 4 |
"version": "1.0.0",
|
| 5 |
"type": "module",
|
| 6 |
+
"description": "A 2-step educational game for teaching equality comparison using emojis",
|
| 7 |
"scripts": {
|
| 8 |
"dev": "vite",
|
| 9 |
"build": "vite build",
|
src/App.css
CHANGED
|
@@ -1,15 +1,16 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
padding: 2rem;
|
| 5 |
-
text-align: center;
|
| 6 |
-
font-family: 'Arial', sans-serif;
|
| 7 |
display: flex;
|
| 8 |
flex-direction: column;
|
| 9 |
align-items: center;
|
| 10 |
-
justify-content:
|
|
|
|
|
|
|
|
|
|
| 11 |
}
|
| 12 |
|
|
|
|
| 13 |
.loading {
|
| 14 |
display: flex;
|
| 15 |
justify-content: center;
|
|
@@ -17,132 +18,265 @@
|
|
| 17 |
height: 100vh;
|
| 18 |
font-size: 1.5rem;
|
| 19 |
font-weight: bold;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
width: 100%;
|
| 25 |
-
max-width: 900px;
|
| 26 |
-
height: 55vh;
|
| 27 |
-
max-height: 500px;
|
| 28 |
-
margin: 0 auto 3rem;
|
| 29 |
display: flex;
|
|
|
|
| 30 |
justify-content: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
align-items: center;
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.25);
|
| 35 |
-
background-color: #f0f0f0;
|
| 36 |
}
|
| 37 |
|
| 38 |
-
.
|
| 39 |
-
|
| 40 |
-
max-height: 100%;
|
| 41 |
-
object-fit: contain;
|
| 42 |
}
|
| 43 |
|
| 44 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
display: flex;
|
| 46 |
-
|
| 47 |
justify-content: center;
|
| 48 |
-
|
| 49 |
-
margin-bottom: 2rem;
|
| 50 |
}
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
min-width: 140px;
|
| 56 |
-
padding: 1.4rem 1.6rem;
|
| 57 |
-
border-radius: 10px;
|
| 58 |
-
border: 3px solid #000;
|
| 59 |
-
font-size: 1.6rem;
|
| 60 |
font-weight: bold;
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
display: flex;
|
| 63 |
-
justify-content: center;
|
| 64 |
align-items: center;
|
| 65 |
-
|
| 66 |
-
text-
|
| 67 |
-
white-space: nowrap; /* prevent text from wrapping to a new line */
|
| 68 |
}
|
| 69 |
|
| 70 |
-
|
|
|
|
| 71 |
display: flex;
|
| 72 |
-
gap:
|
| 73 |
justify-content: center;
|
| 74 |
flex-wrap: wrap;
|
| 75 |
-
margin-top: 2rem;
|
| 76 |
}
|
| 77 |
|
| 78 |
-
.
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
cursor: pointer;
|
| 85 |
-
transition:
|
| 86 |
-
min-width:
|
| 87 |
-
box-shadow: 0
|
| 88 |
}
|
| 89 |
|
| 90 |
-
.
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
.flying-option {
|
| 96 |
-
padding: 0.8rem 1.2rem;
|
| 97 |
-
border: 2px solid #000;
|
| 98 |
-
border-radius: 6px;
|
| 99 |
-
font-size: 1.1rem;
|
| 100 |
-
pointer-events: none;
|
| 101 |
-
z-index: 100;
|
| 102 |
-
min-width: 120px;
|
| 103 |
-
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
| 104 |
-
white-space: nowrap;
|
| 105 |
-
display: flex;
|
| 106 |
-
justify-content: center;
|
| 107 |
-
align-items: center;
|
| 108 |
}
|
| 109 |
|
| 110 |
-
.
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
font-size: 1.2rem;
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
|
| 118 |
-
|
|
|
|
| 119 |
position: fixed;
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
| 123 |
font-weight: bold;
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
z-index:
|
|
|
|
| 128 |
}
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
@media (max-width: 768px) {
|
| 131 |
-
.
|
| 132 |
-
|
| 133 |
-
|
| 134 |
}
|
| 135 |
|
| 136 |
-
.
|
| 137 |
-
|
|
|
|
| 138 |
}
|
| 139 |
|
| 140 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
flex-direction: column;
|
| 142 |
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
}
|
| 144 |
|
| 145 |
-
.
|
| 146 |
-
|
| 147 |
}
|
| 148 |
}
|
|
|
|
| 1 |
+
/* Main container */
|
| 2 |
+
.equal-game {
|
| 3 |
+
min-height: 100vh;
|
|
|
|
|
|
|
|
|
|
| 4 |
display: flex;
|
| 5 |
flex-direction: column;
|
| 6 |
align-items: center;
|
| 7 |
+
justify-content: center;
|
| 8 |
+
padding: 2rem;
|
| 9 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 10 |
+
font-family: 'Arial', sans-serif;
|
| 11 |
}
|
| 12 |
|
| 13 |
+
/* Loading state */
|
| 14 |
.loading {
|
| 15 |
display: flex;
|
| 16 |
justify-content: center;
|
|
|
|
| 18 |
height: 100vh;
|
| 19 |
font-size: 1.5rem;
|
| 20 |
font-weight: bold;
|
| 21 |
+
color: white;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/* Question title */
|
| 25 |
+
.question-title {
|
| 26 |
+
font-size: 3rem;
|
| 27 |
+
font-weight: bold;
|
| 28 |
+
color: white;
|
| 29 |
+
text-align: center;
|
| 30 |
+
margin-bottom: 2rem;
|
| 31 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
| 32 |
}
|
| 33 |
|
| 34 |
+
/* Comparison container */
|
| 35 |
+
.comparison-container {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
display: flex;
|
| 37 |
+
align-items: center;
|
| 38 |
justify-content: center;
|
| 39 |
+
gap: 3rem;
|
| 40 |
+
margin-bottom: 3rem;
|
| 41 |
+
flex-wrap: wrap;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/* Side container */
|
| 45 |
+
.side-container {
|
| 46 |
+
background: white;
|
| 47 |
+
border-radius: 20px;
|
| 48 |
+
padding: 2rem;
|
| 49 |
+
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
| 50 |
+
min-width: 300px;
|
| 51 |
+
max-width: 400px;
|
| 52 |
+
display: flex;
|
| 53 |
+
flex-direction: column;
|
| 54 |
align-items: center;
|
| 55 |
+
gap: 1rem;
|
| 56 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
+
.side-container.clickable {
|
| 60 |
+
cursor: pointer;
|
|
|
|
|
|
|
| 61 |
}
|
| 62 |
|
| 63 |
+
.side-container.clickable:hover {
|
| 64 |
+
transform: scale(1.05);
|
| 65 |
+
box-shadow: 0 12px 35px rgba(0,0,0,0.2);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/* Emoji grid */
|
| 69 |
+
.emoji-grid {
|
| 70 |
+
display: grid;
|
| 71 |
+
grid-template-columns: repeat(auto-fit, minmax(40px, 1fr));
|
| 72 |
+
gap: 8px;
|
| 73 |
+
max-width: 300px;
|
| 74 |
+
justify-items: center;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.emoji-item {
|
| 78 |
+
font-size: 2rem;
|
| 79 |
+
display: block;
|
| 80 |
+
animation: popIn 0.3s ease-out;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
@keyframes popIn {
|
| 84 |
+
0% {
|
| 85 |
+
transform: scale(0);
|
| 86 |
+
}
|
| 87 |
+
80% {
|
| 88 |
+
transform: scale(1.1);
|
| 89 |
+
}
|
| 90 |
+
100% {
|
| 91 |
+
transform: scale(1);
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* Count display */
|
| 96 |
+
.count-display {
|
| 97 |
+
font-size: 2.5rem;
|
| 98 |
+
font-weight: bold;
|
| 99 |
+
color: #333;
|
| 100 |
+
background: #f0f0f0;
|
| 101 |
+
border-radius: 50%;
|
| 102 |
+
width: 80px;
|
| 103 |
+
height: 80px;
|
| 104 |
display: flex;
|
| 105 |
+
align-items: center;
|
| 106 |
justify-content: center;
|
| 107 |
+
border: 3px solid #ddd;
|
|
|
|
| 108 |
}
|
| 109 |
|
| 110 |
+
/* VS divider */
|
| 111 |
+
.vs-divider {
|
| 112 |
+
font-size: 2rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
font-weight: bold;
|
| 114 |
+
color: white;
|
| 115 |
+
background: rgba(255,255,255,0.2);
|
| 116 |
+
border-radius: 50%;
|
| 117 |
+
width: 80px;
|
| 118 |
+
height: 80px;
|
| 119 |
display: flex;
|
|
|
|
| 120 |
align-items: center;
|
| 121 |
+
justify-content: center;
|
| 122 |
+
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
|
|
|
|
| 123 |
}
|
| 124 |
|
| 125 |
+
/* Answer buttons for step 1 */
|
| 126 |
+
.answer-buttons {
|
| 127 |
display: flex;
|
| 128 |
+
gap: 2rem;
|
| 129 |
justify-content: center;
|
| 130 |
flex-wrap: wrap;
|
|
|
|
| 131 |
}
|
| 132 |
|
| 133 |
+
.answer-btn {
|
| 134 |
+
padding: 1.5rem 3rem;
|
| 135 |
+
font-size: 1.5rem;
|
| 136 |
+
font-weight: bold;
|
| 137 |
+
border: none;
|
| 138 |
+
border-radius: 15px;
|
| 139 |
cursor: pointer;
|
| 140 |
+
transition: all 0.3s ease;
|
| 141 |
+
min-width: 150px;
|
| 142 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
| 143 |
}
|
| 144 |
|
| 145 |
+
.equal-btn {
|
| 146 |
+
background: linear-gradient(45deg, #4CAF50, #45a049);
|
| 147 |
+
color: white;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
}
|
| 149 |
|
| 150 |
+
.equal-btn:hover {
|
| 151 |
+
background: linear-gradient(45deg, #45a049, #4CAF50);
|
| 152 |
+
transform: translateY(-2px);
|
| 153 |
+
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.not-equal-btn {
|
| 157 |
+
background: linear-gradient(45deg, #f44336, #d32f2f);
|
| 158 |
+
color: white;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.not-equal-btn:hover {
|
| 162 |
+
background: linear-gradient(45deg, #d32f2f, #f44336);
|
| 163 |
+
transform: translateY(-2px);
|
| 164 |
+
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/* Step 2 instruction */
|
| 168 |
+
.step2-instruction {
|
| 169 |
font-size: 1.2rem;
|
| 170 |
+
color: white;
|
| 171 |
+
text-align: center;
|
| 172 |
+
margin-top: 1rem;
|
| 173 |
+
background: rgba(255,255,255,0.1);
|
| 174 |
+
padding: 1rem 2rem;
|
| 175 |
+
border-radius: 10px;
|
| 176 |
+
backdrop-filter: blur(10px);
|
| 177 |
}
|
| 178 |
|
| 179 |
+
/* Feedback message */
|
| 180 |
+
.feedback-message {
|
| 181 |
position: fixed;
|
| 182 |
+
top: 50%;
|
| 183 |
+
left: 50%;
|
| 184 |
+
transform: translate(-50%, -50%);
|
| 185 |
+
background: white;
|
| 186 |
+
color: #333;
|
| 187 |
+
font-size: 2rem;
|
| 188 |
font-weight: bold;
|
| 189 |
+
padding: 2rem 3rem;
|
| 190 |
+
border-radius: 20px;
|
| 191 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
| 192 |
+
z-index: 1000;
|
| 193 |
+
animation: feedbackPop 0.5s ease-out;
|
| 194 |
}
|
| 195 |
|
| 196 |
+
@keyframes feedbackPop {
|
| 197 |
+
0% {
|
| 198 |
+
transform: translate(-50%, -50%) scale(0);
|
| 199 |
+
}
|
| 200 |
+
80% {
|
| 201 |
+
transform: translate(-50%, -50%) scale(1.1);
|
| 202 |
+
}
|
| 203 |
+
100% {
|
| 204 |
+
transform: translate(-50%, -50%) scale(1);
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
/* Responsive design */
|
| 209 |
@media (max-width: 768px) {
|
| 210 |
+
.question-title {
|
| 211 |
+
font-size: 2rem;
|
| 212 |
+
margin-bottom: 1.5rem;
|
| 213 |
}
|
| 214 |
|
| 215 |
+
.comparison-container {
|
| 216 |
+
gap: 2rem;
|
| 217 |
+
flex-direction: column;
|
| 218 |
}
|
| 219 |
|
| 220 |
+
.side-container {
|
| 221 |
+
min-width: 250px;
|
| 222 |
+
max-width: 300px;
|
| 223 |
+
padding: 1.5rem;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.vs-divider {
|
| 227 |
+
width: 60px;
|
| 228 |
+
height: 60px;
|
| 229 |
+
font-size: 1.5rem;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.emoji-item {
|
| 233 |
+
font-size: 1.5rem;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
.count-display {
|
| 237 |
+
width: 60px;
|
| 238 |
+
height: 60px;
|
| 239 |
+
font-size: 2rem;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.answer-buttons {
|
| 243 |
flex-direction: column;
|
| 244 |
align-items: center;
|
| 245 |
+
gap: 1rem;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.answer-btn {
|
| 249 |
+
padding: 1rem 2rem;
|
| 250 |
+
font-size: 1.2rem;
|
| 251 |
+
min-width: 200px;
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.feedback-message {
|
| 255 |
+
font-size: 1.5rem;
|
| 256 |
+
padding: 1.5rem 2rem;
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
@media (max-width: 480px) {
|
| 261 |
+
.equal-game {
|
| 262 |
+
padding: 1rem;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.question-title {
|
| 266 |
+
font-size: 1.5rem;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.side-container {
|
| 270 |
+
min-width: 200px;
|
| 271 |
+
max-width: 250px;
|
| 272 |
+
padding: 1rem;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.emoji-grid {
|
| 276 |
+
max-width: 200px;
|
| 277 |
}
|
| 278 |
|
| 279 |
+
.emoji-item {
|
| 280 |
+
font-size: 1.2rem;
|
| 281 |
}
|
| 282 |
}
|
src/App.tsx
CHANGED
|
@@ -1,286 +1,261 @@
|
|
| 1 |
-
import { useState, useEffect
|
| 2 |
import './App.css'
|
| 3 |
|
| 4 |
-
//
|
| 5 |
-
interface
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
to_whom: string;
|
| 11 |
-
distractors: string[];
|
| 12 |
}
|
| 13 |
|
| 14 |
-
//
|
| 15 |
-
|
| 16 |
-
frames: string[];
|
| 17 |
-
frameIndex: number;
|
| 18 |
-
who: string;
|
| 19 |
-
doing: string;
|
| 20 |
-
what: string;
|
| 21 |
-
answer: string;
|
| 22 |
-
choices: string[];
|
| 23 |
-
}
|
| 24 |
|
| 25 |
function App() {
|
| 26 |
-
const [
|
| 27 |
-
const [
|
| 28 |
-
const [
|
| 29 |
-
const [
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
const
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
const
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
// Load questions on mount
|
| 46 |
-
useEffect(() => {
|
| 47 |
-
const loadQuestions = async () => {
|
| 48 |
-
try {
|
| 49 |
-
const response = await fetch('/questions.json');
|
| 50 |
-
if (!response.ok) {
|
| 51 |
-
throw new Error('Failed to load questions');
|
| 52 |
-
}
|
| 53 |
-
const data: Question[] = await response.json();
|
| 54 |
-
// Shuffle questions
|
| 55 |
-
const shuffled = [...data].sort(() => Math.random() - 0.5);
|
| 56 |
-
setQuestions(shuffled);
|
| 57 |
-
} catch (error) {
|
| 58 |
-
console.error('Error loading questions:', error);
|
| 59 |
-
}
|
| 60 |
-
};
|
| 61 |
|
| 62 |
-
|
|
|
|
| 63 |
|
| 64 |
-
//
|
| 65 |
-
const
|
| 66 |
-
|
| 67 |
-
width: window.innerWidth,
|
| 68 |
-
height: window.innerHeight
|
| 69 |
-
});
|
| 70 |
-
};
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
// Prepare game state when questions load or change
|
| 77 |
-
useEffect(() => {
|
| 78 |
-
if (questions.length > 0) {
|
| 79 |
-
prepareRound(questions[currentQuestionIndex]);
|
| 80 |
}
|
| 81 |
-
}, [questions, currentQuestionIndex]);
|
| 82 |
-
|
| 83 |
-
// Handle video/image frame updates
|
| 84 |
-
useEffect(() => {
|
| 85 |
-
if (!gameState || !gameState.frames.length) return;
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
if (!prev) return prev;
|
| 91 |
-
const nextIndex = (prev.frameIndex + 1) % prev.frames.length;
|
| 92 |
-
return { ...prev, frameIndex: nextIndex };
|
| 93 |
-
});
|
| 94 |
-
}
|
| 95 |
-
}, 100); // Adjust frame rate as needed
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
useEffect(() => {
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
setIsFilled(false);
|
| 107 |
-
setFeedbackMsg('');
|
| 108 |
-
setAnimationElement(null);
|
| 109 |
-
}, 1000);
|
| 110 |
-
|
| 111 |
-
return () => clearTimeout(timer);
|
| 112 |
-
}
|
| 113 |
-
}, [isFilled, questions.length]);
|
| 114 |
-
|
| 115 |
-
// Animation effect
|
| 116 |
useEffect(() => {
|
| 117 |
-
if (
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
anim.style.transition = 'transform 500ms linear';
|
| 126 |
-
anim.style.transform = `translate(${targetPosition.x - animationElement.position.x}px, ${targetPosition.y - animationElement.position.y}px)`;
|
| 127 |
-
|
| 128 |
-
const handleAnimationEnd = () => {
|
| 129 |
-
setIsAnimating(false);
|
| 130 |
-
setIsFilled(true);
|
| 131 |
-
};
|
| 132 |
-
|
| 133 |
-
anim.addEventListener('transitionend', handleAnimationEnd);
|
| 134 |
-
return () => anim.removeEventListener('transitionend', handleAnimationEnd);
|
| 135 |
}
|
| 136 |
-
}, [
|
| 137 |
-
|
| 138 |
-
//
|
| 139 |
-
const
|
| 140 |
-
|
| 141 |
-
// Shuffle choices
|
| 142 |
-
const shuffledChoices = [...choices].sort(() => Math.random() - 0.5);
|
| 143 |
-
|
| 144 |
-
// Determine if media is image or video
|
| 145 |
-
const isVideo = question.file.endsWith('.mp4');
|
| 146 |
-
const frames = isVideo
|
| 147 |
-
? [question.file] // For simplicity, we'll just use the path for videos
|
| 148 |
-
: [question.file]; // Same for images
|
| 149 |
|
| 150 |
-
|
| 151 |
-
frames,
|
| 152 |
-
frameIndex: 0,
|
| 153 |
-
who: question.who,
|
| 154 |
-
doing: question.doing,
|
| 155 |
-
what: question.what,
|
| 156 |
-
answer: question.to_whom,
|
| 157 |
-
choices: shuffledChoices
|
| 158 |
-
});
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
};
|
| 165 |
-
|
| 166 |
-
// Handle
|
| 167 |
-
const
|
| 168 |
-
if (
|
| 169 |
|
| 170 |
-
const
|
| 171 |
-
const
|
| 172 |
-
x: rect.left + rect.width / 2,
|
| 173 |
-
y: rect.top + rect.height / 2
|
| 174 |
-
};
|
| 175 |
|
| 176 |
-
if
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
setIsAnimating(true);
|
| 183 |
-
setFeedbackMsg('');
|
| 184 |
-
} else {
|
| 185 |
-
// Wrong answer
|
| 186 |
-
setFeedbackMsg(wrongMessages[Math.floor(Math.random() * wrongMessages.length)]);
|
| 187 |
}
|
| 188 |
-
};
|
| 189 |
-
|
| 190 |
-
// Render current media (image or video)
|
| 191 |
-
const renderMedia = () => {
|
| 192 |
-
if (!gameState || !gameState.frames.length) return null;
|
| 193 |
|
| 194 |
-
|
| 195 |
-
const isVideo = currentFrame.endsWith('.mp4');
|
| 196 |
|
| 197 |
-
if (
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
} else {
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
);
|
| 215 |
}
|
| 216 |
};
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
}
|
| 221 |
-
|
| 222 |
return (
|
| 223 |
-
<div className="
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
<div className="semantic-box" style={{ backgroundColor: GREEN }}>
|
| 242 |
-
{gameState.what}
|
| 243 |
-
</div>
|
| 244 |
-
|
| 245 |
-
{/* To Whom - Pink */}
|
| 246 |
-
<div
|
| 247 |
-
className="semantic-box"
|
| 248 |
-
style={{ backgroundColor: PINK }}
|
| 249 |
-
ref={destinationRef}
|
| 250 |
-
>
|
| 251 |
-
{isFilled ? gameState.answer : "to whom?"}
|
| 252 |
-
</div>
|
| 253 |
-
</div>
|
| 254 |
-
|
| 255 |
-
<div className="options-container">
|
| 256 |
-
{!isFilled && gameState.choices.map((option, index) => (
|
| 257 |
-
<div
|
| 258 |
-
key={index}
|
| 259 |
-
className="option"
|
| 260 |
-
onClick={(e) => handleOptionClick(option, e)}
|
| 261 |
-
>
|
| 262 |
-
{option}
|
| 263 |
</div>
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
)}
|
| 270 |
|
| 271 |
-
{
|
| 272 |
-
<div
|
| 273 |
-
|
| 274 |
-
ref={animationRef}
|
| 275 |
-
style={{
|
| 276 |
-
backgroundColor: PINK,
|
| 277 |
-
position: 'fixed',
|
| 278 |
-
left: animationElement.position.x,
|
| 279 |
-
top: animationElement.position.y,
|
| 280 |
-
transform: 'translate(-50%, -50%)'
|
| 281 |
-
}}
|
| 282 |
-
>
|
| 283 |
-
{animationElement.content}
|
| 284 |
</div>
|
| 285 |
)}
|
| 286 |
</div>
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react'
|
| 2 |
import './App.css'
|
| 3 |
|
| 4 |
+
// Game question structure
|
| 5 |
+
interface ComparisonQuestion {
|
| 6 |
+
leftSide: string[];
|
| 7 |
+
rightSide: string[];
|
| 8 |
+
isEqual: boolean;
|
| 9 |
+
secondStepQuestion: 'more' | 'fewer';
|
|
|
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
+
// Game states
|
| 13 |
+
type GameStep = 'step1' | 'step2' | 'correct-step1' | 'correct-step2';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
function App() {
|
| 16 |
+
const [currentQuestion, setCurrentQuestion] = useState<ComparisonQuestion | null>(null);
|
| 17 |
+
const [gameStep, setGameStep] = useState<GameStep>('step1');
|
| 18 |
+
const [feedback, setFeedback] = useState('');
|
| 19 |
+
const [showFeedback, setShowFeedback] = useState(false);
|
| 20 |
+
|
| 21 |
+
// Emoji pools for different categories
|
| 22 |
+
const emojiCategories = {
|
| 23 |
+
animals: ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐸', '🐷'],
|
| 24 |
+
fruits: ['🍎', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🫐', '🍒', '🥝'],
|
| 25 |
+
objects: ['⚽', '🏀', '🎾', '🏈', '🎱', '🎪', '🎨', '🎭', '🎪', '🎯'],
|
| 26 |
+
hearts: ['💙', '💚', '💛', '💜', '🤍', '🖤', '🤎', '💗', '💖', '💕'],
|
| 27 |
+
stars: ['⭐', '🌟', '💫', '✨', '🌠', '⚡', '🔥', '❄️', '☀️', '🌙']
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
// Generate a random question
|
| 31 |
+
const generateQuestion = (): ComparisonQuestion => {
|
| 32 |
+
const categories = Object.keys(emojiCategories) as (keyof typeof emojiCategories)[];
|
| 33 |
+
const selectedCategory = categories[Math.floor(Math.random() * categories.length)];
|
| 34 |
+
const emojis = emojiCategories[selectedCategory];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
// Pick a random emoji for this question
|
| 37 |
+
const emoji = emojis[Math.floor(Math.random() * emojis.length)];
|
| 38 |
|
| 39 |
+
// Generate counts (1-10)
|
| 40 |
+
const leftCount = Math.floor(Math.random() * 10) + 1;
|
| 41 |
+
let rightCount = Math.floor(Math.random() * 10) + 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
+
// 50% chance of making them equal
|
| 44 |
+
const shouldBeEqual = Math.random() < 0.5;
|
| 45 |
+
if (shouldBeEqual) {
|
| 46 |
+
rightCount = leftCount;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
// Create arrays with repeated emoji
|
| 50 |
+
const leftSide = Array(leftCount).fill(emoji);
|
| 51 |
+
const rightSide = Array(rightCount).fill(emoji);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
+
// Determine second step question type
|
| 54 |
+
const secondStepQuestion: 'more' | 'fewer' = Math.random() < 0.5 ? 'more' : 'fewer';
|
| 55 |
+
|
| 56 |
+
return {
|
| 57 |
+
leftSide,
|
| 58 |
+
rightSide,
|
| 59 |
+
isEqual: leftCount === rightCount,
|
| 60 |
+
secondStepQuestion
|
| 61 |
+
};
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
// Initialize with first question
|
| 65 |
useEffect(() => {
|
| 66 |
+
setCurrentQuestion(generateQuestion());
|
| 67 |
+
}, []);
|
| 68 |
+
|
| 69 |
+
// Safety effect: if we're in step 2 but sides are equal, generate new question
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
useEffect(() => {
|
| 71 |
+
if (currentQuestion &&
|
| 72 |
+
(gameStep === 'step2' || gameStep === 'correct-step2') &&
|
| 73 |
+
currentQuestion.leftSide.length === currentQuestion.rightSide.length) {
|
| 74 |
+
// Generate new question and go back to step 1
|
| 75 |
+
setTimeout(() => {
|
| 76 |
+
setCurrentQuestion(generateQuestion());
|
| 77 |
+
setGameStep('step1');
|
| 78 |
+
}, 1000);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
+
}, [currentQuestion, gameStep]);
|
| 81 |
+
|
| 82 |
+
// Handle step 1 answer (equal/not equal)
|
| 83 |
+
const handleStep1Answer = (answer: 'equal' | 'not-equal') => {
|
| 84 |
+
if (!currentQuestion) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
const isCorrect = (answer === 'equal') === currentQuestion.isEqual;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
+
if (isCorrect) {
|
| 89 |
+
setFeedback('Correct! 🎉');
|
| 90 |
+
setShowFeedback(true);
|
| 91 |
+
setGameStep('correct-step1');
|
| 92 |
+
|
| 93 |
+
// If they correctly identified as "equal", generate new question
|
| 94 |
+
// If they correctly identified as "not equal", proceed to step 2
|
| 95 |
+
if (answer === 'equal') {
|
| 96 |
+
// Generate new question after delay
|
| 97 |
+
setTimeout(() => {
|
| 98 |
+
setCurrentQuestion(generateQuestion());
|
| 99 |
+
setGameStep('step1');
|
| 100 |
+
setShowFeedback(false);
|
| 101 |
+
setFeedback('');
|
| 102 |
+
}, 2000);
|
| 103 |
+
} else {
|
| 104 |
+
// Proceed to step 2 only if they are NOT equal
|
| 105 |
+
setTimeout(() => {
|
| 106 |
+
setGameStep('step2');
|
| 107 |
+
setShowFeedback(false);
|
| 108 |
+
setFeedback('');
|
| 109 |
+
}, 1500);
|
| 110 |
+
}
|
| 111 |
+
} else {
|
| 112 |
+
setFeedback('Try again! 🤔');
|
| 113 |
+
setShowFeedback(true);
|
| 114 |
+
setTimeout(() => {
|
| 115 |
+
setShowFeedback(false);
|
| 116 |
+
setFeedback('');
|
| 117 |
+
}, 1000);
|
| 118 |
+
}
|
| 119 |
};
|
| 120 |
+
|
| 121 |
+
// Handle step 2 answer (more/fewer)
|
| 122 |
+
const handleStep2Answer = (answer: 'left' | 'right') => {
|
| 123 |
+
if (!currentQuestion) return;
|
| 124 |
|
| 125 |
+
const leftCount = currentQuestion.leftSide.length;
|
| 126 |
+
const rightCount = currentQuestion.rightSide.length;
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
+
// Safety check: if sides are equal, this shouldn't happen in step 2
|
| 129 |
+
if (leftCount === rightCount) {
|
| 130 |
+
// Generate new question and go back to step 1
|
| 131 |
+
setCurrentQuestion(generateQuestion());
|
| 132 |
+
setGameStep('step1');
|
| 133 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
+
let isCorrect = false;
|
|
|
|
| 137 |
|
| 138 |
+
if (currentQuestion.secondStepQuestion === 'more') {
|
| 139 |
+
if (leftCount > rightCount && answer === 'left') isCorrect = true;
|
| 140 |
+
if (rightCount > leftCount && answer === 'right') isCorrect = true;
|
| 141 |
+
} else { // fewer
|
| 142 |
+
if (leftCount < rightCount && answer === 'left') isCorrect = true;
|
| 143 |
+
if (rightCount < leftCount && answer === 'right') isCorrect = true;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
if (isCorrect) {
|
| 147 |
+
setFeedback('Excellent! 🌟');
|
| 148 |
+
setShowFeedback(true);
|
| 149 |
+
setGameStep('correct-step2');
|
| 150 |
+
|
| 151 |
+
// Generate new question after delay
|
| 152 |
+
setTimeout(() => {
|
| 153 |
+
setCurrentQuestion(generateQuestion());
|
| 154 |
+
setGameStep('step1');
|
| 155 |
+
setShowFeedback(false);
|
| 156 |
+
setFeedback('');
|
| 157 |
+
}, 2000);
|
| 158 |
} else {
|
| 159 |
+
setFeedback('Try again! 🤔');
|
| 160 |
+
setShowFeedback(true);
|
| 161 |
+
setTimeout(() => {
|
| 162 |
+
setShowFeedback(false);
|
| 163 |
+
setFeedback('');
|
| 164 |
+
}, 1000);
|
|
|
|
| 165 |
}
|
| 166 |
};
|
| 167 |
+
|
| 168 |
+
// Render emoji grid
|
| 169 |
+
const renderEmojiSide = (emojis: string[], side: 'left' | 'right') => {
|
| 170 |
+
const gridClass = `emoji-grid ${side}`;
|
| 171 |
+
return (
|
| 172 |
+
<div className={gridClass}>
|
| 173 |
+
{emojis.map((emoji, index) => (
|
| 174 |
+
<span key={index} className="emoji-item">
|
| 175 |
+
{emoji}
|
| 176 |
+
</span>
|
| 177 |
+
))}
|
| 178 |
+
</div>
|
| 179 |
+
);
|
| 180 |
+
};
|
| 181 |
+
|
| 182 |
+
if (!currentQuestion) {
|
| 183 |
+
return <div className="loading">Loading...</div>;
|
| 184 |
}
|
| 185 |
+
|
| 186 |
return (
|
| 187 |
+
<div className="equal-game">
|
| 188 |
+
{gameStep === 'step1' || gameStep === 'correct-step1' ? (
|
| 189 |
+
// Step 1: Are they equal?
|
| 190 |
+
<>
|
| 191 |
+
<h1 className="question-title">Are they equal?</h1>
|
| 192 |
+
|
| 193 |
+
<div className="comparison-container">
|
| 194 |
+
<div className="side-container">
|
| 195 |
+
{renderEmojiSide(currentQuestion.leftSide, 'left')}
|
| 196 |
+
<div className="count-display">{currentQuestion.leftSide.length}</div>
|
| 197 |
+
</div>
|
| 198 |
+
|
| 199 |
+
<div className="vs-divider">VS</div>
|
| 200 |
+
|
| 201 |
+
<div className="side-container">
|
| 202 |
+
{renderEmojiSide(currentQuestion.rightSide, 'right')}
|
| 203 |
+
<div className="count-display">{currentQuestion.rightSide.length}</div>
|
| 204 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
</div>
|
| 206 |
+
|
| 207 |
+
{gameStep === 'step1' && (
|
| 208 |
+
<div className="answer-buttons">
|
| 209 |
+
<button
|
| 210 |
+
className="answer-btn equal-btn"
|
| 211 |
+
onClick={() => handleStep1Answer('equal')}
|
| 212 |
+
>
|
| 213 |
+
Equal
|
| 214 |
+
</button>
|
| 215 |
+
<button
|
| 216 |
+
className="answer-btn not-equal-btn"
|
| 217 |
+
onClick={() => handleStep1Answer('not-equal')}
|
| 218 |
+
>
|
| 219 |
+
Not Equal
|
| 220 |
+
</button>
|
| 221 |
+
</div>
|
| 222 |
+
)}
|
| 223 |
+
</>
|
| 224 |
+
) : (
|
| 225 |
+
// Step 2: Which side has more/fewer? (Only if sides are NOT equal)
|
| 226 |
+
currentQuestion.leftSide.length !== currentQuestion.rightSide.length ? (
|
| 227 |
+
<>
|
| 228 |
+
<h1 className="question-title">
|
| 229 |
+
Which side has {currentQuestion.secondStepQuestion}?
|
| 230 |
+
</h1>
|
| 231 |
+
|
| 232 |
+
<div className="comparison-container">
|
| 233 |
+
<div className="side-container clickable" onClick={() => handleStep2Answer('left')}>
|
| 234 |
+
{renderEmojiSide(currentQuestion.leftSide, 'left')}
|
| 235 |
+
<div className="count-display">{currentQuestion.leftSide.length}</div>
|
| 236 |
+
</div>
|
| 237 |
+
|
| 238 |
+
<div className="vs-divider">VS</div>
|
| 239 |
+
|
| 240 |
+
<div className="side-container clickable" onClick={() => handleStep2Answer('right')}>
|
| 241 |
+
{renderEmojiSide(currentQuestion.rightSide, 'right')}
|
| 242 |
+
<div className="count-display">{currentQuestion.rightSide.length}</div>
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
|
| 246 |
+
<div className="step2-instruction">
|
| 247 |
+
Click on the side with {currentQuestion.secondStepQuestion} items
|
| 248 |
+
</div>
|
| 249 |
+
</>
|
| 250 |
+
) : (
|
| 251 |
+
// If somehow we get to step 2 with equal sides, generate new question
|
| 252 |
+
<div className="loading">Generating new question...</div>
|
| 253 |
+
)
|
| 254 |
)}
|
| 255 |
|
| 256 |
+
{showFeedback && (
|
| 257 |
+
<div className="feedback-message">
|
| 258 |
+
{feedback}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
</div>
|
| 260 |
)}
|
| 261 |
</div>
|