Spaces:
Sleeping
Sleeping
updated game
Browse files- README.md +40 -60
- index.html +2 -2
- package.json +3 -2
- public/admin-guide.md +95 -0
- public/favicon.svg +14 -0
- public/media/grandma_cookies.png +3 -0
- public/media/readme.txt +11 -0
- public/questions.json +34 -0
- src/App.css +96 -94
- src/App.tsx +247 -98
README.md
CHANGED
@@ -1,36 +1,58 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
emoji: 🎮
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
app_port: 3000
|
9 |
---
|
10 |
|
11 |
-
#
|
12 |
|
13 |
-
A
|
14 |
|
15 |
-
|
16 |
|
17 |
-
|
18 |
-
-
|
19 |
-
-
|
20 |
-
-
|
21 |
-
# Memory Card Game
|
22 |
|
23 |
-
|
24 |
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
|
27 |
-
|
28 |
-
-
|
29 |
-
-
|
|
|
|
|
|
|
|
|
30 |
|
31 |
-
|
32 |
|
33 |
-
|
34 |
|
35 |
```bash
|
36 |
# Install dependencies
|
@@ -38,53 +60,11 @@ npm install
|
|
38 |
|
39 |
# Start development server
|
40 |
npm run dev
|
41 |
-
```
|
42 |
-
|
43 |
-
### Building for Production
|
44 |
|
45 |
-
```bash
|
46 |
# Build for production
|
47 |
npm run build
|
48 |
-
|
49 |
-
# Preview production build
|
50 |
-
npm run preview
|
51 |
```
|
52 |
|
53 |
-
## Docker Deployment
|
54 |
-
|
55 |
-
Build and run the Docker container:
|
56 |
-
|
57 |
-
```bash
|
58 |
-
# Build the Docker image
|
59 |
-
docker build -t memory-game .
|
60 |
-
|
61 |
-
# Run the container
|
62 |
-
docker run -p 3000:3000 memory-game
|
63 |
-
```
|
64 |
-
|
65 |
-
## Deployment to Hugging Face Spaces
|
66 |
-
|
67 |
-
1. Create a new Space on Hugging Face: https://huggingface.co/spaces
|
68 |
-
2. Choose Docker as the SDK
|
69 |
-
3. Clone your Space repository
|
70 |
-
4. Copy your project files to the repository
|
71 |
-
5. Push to Hugging Face:
|
72 |
-
|
73 |
-
```bash
|
74 |
-
git add .
|
75 |
-
git commit -m "Initial commit"
|
76 |
-
git push
|
77 |
-
```
|
78 |
-
|
79 |
-
6. Hugging Face will automatically build and deploy your Docker container
|
80 |
-
|
81 |
-
## How to Play
|
82 |
-
|
83 |
-
1. Click on cards to flip them over
|
84 |
-
2. Try to find matching pairs of emojis
|
85 |
-
3. The game is complete when all pairs have been matched
|
86 |
-
4. Click "New Game" to reset and play again
|
87 |
-
|
88 |
## License
|
89 |
|
90 |
MIT
|
|
|
1 |
---
|
2 |
+
title: Colorful Semantics Game
|
3 |
emoji: 🎮
|
4 |
+
colorFrom: orange
|
5 |
+
colorTo: pink
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
app_port: 3000
|
9 |
---
|
10 |
|
11 |
+
# Colorful Semantics - To Whom? Game
|
12 |
|
13 |
+
A learning game that teaches sentence structure through colorful semantic components.
|
14 |
|
15 |
+
## Features
|
16 |
|
17 |
+
- Interactive game for learning "to whom" sentence components
|
18 |
+
- Visual and animated feedback for correct answers
|
19 |
+
- Support for both images and videos
|
20 |
+
- Customizable questions and answers
|
|
|
21 |
|
22 |
+
## How to Play
|
23 |
|
24 |
+
1. Look at the image/video and read the colored sentence parts
|
25 |
+
2. Choose the correct "to whom" option from the choices below
|
26 |
+
3. The game will animate the correct answer to the pink box
|
27 |
+
4. Continue to the next question automatically
|
28 |
+
|
29 |
+
## Adding New Questions
|
30 |
+
|
31 |
+
1. Add your image or video to the `public/media` folder
|
32 |
+
2. Edit the `public/questions.json` file to add a new question:
|
33 |
+
|
34 |
+
```json
|
35 |
+
{
|
36 |
+
"file": "media/your_image.jpg",
|
37 |
+
"who": "The boy",
|
38 |
+
"doing": "is giving",
|
39 |
+
"what": "a book",
|
40 |
+
"to_whom": "to the teacher",
|
41 |
+
"distractors": ["to Mom", "to the dog", "to Grandma"]
|
42 |
+
}
|
43 |
+
```
|
44 |
|
45 |
+
3. Each question needs the following fields:
|
46 |
+
- `file`: Path to the media file (jpg, png, or mp4)
|
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 |
|
53 |
+
4. Restart the application to see your new questions
|
54 |
|
55 |
+
## Development
|
56 |
|
57 |
```bash
|
58 |
# Install dependencies
|
|
|
60 |
|
61 |
# Start development server
|
62 |
npm run dev
|
|
|
|
|
|
|
63 |
|
|
|
64 |
# Build for production
|
65 |
npm run build
|
|
|
|
|
|
|
66 |
```
|
67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
## License
|
69 |
|
70 |
MIT
|
index.html
CHANGED
@@ -2,9 +2,9 @@
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8" />
|
5 |
-
<link rel="icon" type="image/svg+xml" href="/
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
-
<title>
|
8 |
</head>
|
9 |
<body>
|
10 |
<div id="root"></div>
|
|
|
2 |
<html lang="en">
|
3 |
<head>
|
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>Colorful Semantics - To Whom?</title>
|
8 |
</head>
|
9 |
<body>
|
10 |
<div id="root"></div>
|
package.json
CHANGED
@@ -1,8 +1,9 @@
|
|
1 |
{
|
2 |
-
"name": "
|
3 |
"private": true,
|
4 |
-
"version": "
|
5 |
"type": "module",
|
|
|
6 |
"scripts": {
|
7 |
"dev": "vite",
|
8 |
"build": "vite build",
|
|
|
1 |
{
|
2 |
+
"name": "colorful-semantics-game",
|
3 |
"private": true,
|
4 |
+
"version": "1.0.0",
|
5 |
"type": "module",
|
6 |
+
"description": "An educational game for teaching sentence structure using colorful semantics",
|
7 |
"scripts": {
|
8 |
"dev": "vite",
|
9 |
"build": "vite build",
|
public/admin-guide.md
ADDED
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Colorful Semantics - "To Whom?" Game - Administrator Guide
|
2 |
+
|
3 |
+
This guide explains how to customize and manage the Colorful Semantics "To Whom?" educational game.
|
4 |
+
|
5 |
+
## Table of Contents
|
6 |
+
1. [Game Overview](#game-overview)
|
7 |
+
2. [Adding New Questions](#adding-new-questions)
|
8 |
+
3. [Media Requirements](#media-requirements)
|
9 |
+
4. [Question JSON Structure](#question-json-structure)
|
10 |
+
5. [Troubleshooting](#troubleshooting)
|
11 |
+
|
12 |
+
## Game Overview
|
13 |
+
|
14 |
+
The "To Whom?" game teaches sentence structure using four colorful semantic components:
|
15 |
+
- **Orange**: Who (the subject)
|
16 |
+
- **Yellow**: Doing (the verb phrase)
|
17 |
+
- **Green**: What (the object)
|
18 |
+
- **Pink**: To Whom (the recipient)
|
19 |
+
|
20 |
+
Players are presented with an image or video and must select the correct recipient ("to whom") from multiple choices.
|
21 |
+
|
22 |
+
## Adding New Questions
|
23 |
+
|
24 |
+
### Step 1: Prepare Media File
|
25 |
+
1. Create or select an image (JPG/PNG) or short video (MP4) that clearly shows someone giving something to someone else
|
26 |
+
2. Name your file without spaces (e.g., `teacher_book.jpg` or `dad_gift.mp4`)
|
27 |
+
3. Place the file in the `/public/media/` directory
|
28 |
+
|
29 |
+
### Step 2: Add Question to JSON
|
30 |
+
1. Open the file `/public/questions.json`
|
31 |
+
2. Add a new JSON object to the array, following this structure:
|
32 |
+
|
33 |
+
```json
|
34 |
+
{
|
35 |
+
"file": "/media/your_file_name.jpg",
|
36 |
+
"who": "The person giving",
|
37 |
+
"doing": "is passing/giving/handing",
|
38 |
+
"what": "the object being given",
|
39 |
+
"to_whom": "to the recipient",
|
40 |
+
"distractors": ["wrong option 1", "wrong option 2", "wrong option 3"]
|
41 |
+
}
|
42 |
+
```
|
43 |
+
|
44 |
+
3. Save the file
|
45 |
+
4. Refresh the application to see your new question appear in the rotation
|
46 |
+
|
47 |
+
## Media Requirements
|
48 |
+
|
49 |
+
### Images
|
50 |
+
- **Formats**: JPG, PNG
|
51 |
+
- **Recommended size**: 800-1200px wide
|
52 |
+
- **File size**: Keep under 500KB for optimal performance
|
53 |
+
|
54 |
+
### Videos
|
55 |
+
- **Format**: MP4
|
56 |
+
- **Duration**: Keep under 5 seconds to minimize load time
|
57 |
+
- **Resolution**: 720p or lower recommended
|
58 |
+
- **File size**: Keep under 2MB
|
59 |
+
|
60 |
+
## Question JSON Structure
|
61 |
+
|
62 |
+
Each question in the `questions.json` file must include these fields:
|
63 |
+
|
64 |
+
| Field | Description | Example |
|
65 |
+
|-------|-------------|---------|
|
66 |
+
| `file` | Path to media file, starting with "/media/" | "/media/teacher_book.jpg" |
|
67 |
+
| `who` | The subject (person giving) | "The teacher" |
|
68 |
+
| `doing` | The verb phrase | "is giving" |
|
69 |
+
| `what` | The object being given | "a book" |
|
70 |
+
| `to_whom` | The correct recipient | "to the student" |
|
71 |
+
| `distractors` | Array of incorrect options | ["to Mom", "to the principal"] |
|
72 |
+
|
73 |
+
Notes:
|
74 |
+
- You can include 1-5 distractors
|
75 |
+
- The "to_whom" value should start with "to "
|
76 |
+
- Keep text short so it fits in the colored boxes
|
77 |
+
|
78 |
+
## Troubleshooting
|
79 |
+
|
80 |
+
### Media Not Displaying
|
81 |
+
- Ensure the path in the JSON matches the actual file location
|
82 |
+
- Check that the file has no spaces in its name
|
83 |
+
- Verify the file format is supported (JPG, PNG, or MP4)
|
84 |
+
|
85 |
+
### Game Not Loading New Questions
|
86 |
+
- Check JSON file for syntax errors (missing commas, brackets, etc.)
|
87 |
+
- Ensure the JSON file is properly formatted with square brackets `[]` enclosing all questions
|
88 |
+
- Refresh the page completely (Ctrl+F5 or Cmd+Shift+R)
|
89 |
+
|
90 |
+
### Video Playback Issues
|
91 |
+
- Make sure the video is in MP4 format
|
92 |
+
- Try reducing video file size or resolution
|
93 |
+
- Check if the video codec is widely supported (H.264 recommended)
|
94 |
+
|
95 |
+
For additional support, please contact the development team.
|
public/favicon.svg
ADDED
|
public/media/grandma_cookies.png
ADDED
![]() |
Git LFS Details
|
public/media/readme.txt
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Place your media files (images and videos) in this directory.
|
2 |
+
|
3 |
+
Recommended formats:
|
4 |
+
- Images: JPG, PNG (800-1200px wide, <500KB)
|
5 |
+
- Videos: MP4 (5 seconds or less, 720p or lower, <2MB)
|
6 |
+
|
7 |
+
File naming:
|
8 |
+
- Avoid spaces in filenames (use underscores instead)
|
9 |
+
- Example: teacher_book.jpg or dad_gift.mp4
|
10 |
+
|
11 |
+
After adding media, update the questions.json file in the parent directory.
|
public/questions.json
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[
|
2 |
+
{
|
3 |
+
"file": "/media/teacher_note.jpg",
|
4 |
+
"who": "The boy",
|
5 |
+
"doing": "is passing",
|
6 |
+
"what": "a note",
|
7 |
+
"to_whom": "to the teacher",
|
8 |
+
"distractors": ["to Mom", "to the dog", "to Grandma"]
|
9 |
+
},
|
10 |
+
{
|
11 |
+
"file": "/media/grandma_cookies.png",
|
12 |
+
"who": "Grandma",
|
13 |
+
"doing": "is giving",
|
14 |
+
"what": "cookies",
|
15 |
+
"to_whom": "to the girl",
|
16 |
+
"distractors": ["to Dad", "to the dog", "to the cat"]
|
17 |
+
},
|
18 |
+
{
|
19 |
+
"file": "/media/dad_present.jpg",
|
20 |
+
"who": "Dad",
|
21 |
+
"doing": "is handing",
|
22 |
+
"what": "a present",
|
23 |
+
"to_whom": "to Mom",
|
24 |
+
"distractors": ["to the teacher", "to Grandpa", "to the boy"]
|
25 |
+
},
|
26 |
+
{
|
27 |
+
"file": "/media/girl_flowers.jpg",
|
28 |
+
"who": "The girl",
|
29 |
+
"doing": "is offering",
|
30 |
+
"what": "flowers",
|
31 |
+
"to_whom": "to Grandma",
|
32 |
+
"distractors": ["to the teacher", "to Dad", "to the boy"]
|
33 |
+
}
|
34 |
+
]
|
src/App.css
CHANGED
@@ -1,128 +1,130 @@
|
|
1 |
-
.
|
2 |
-
max-width:
|
3 |
margin: 0 auto;
|
4 |
padding: 2rem;
|
5 |
text-align: center;
|
|
|
6 |
}
|
7 |
|
8 |
-
|
9 |
-
font-size: 2.5rem;
|
10 |
-
margin-bottom: 1.5rem;
|
11 |
-
color: #333;
|
12 |
-
}
|
13 |
-
|
14 |
-
.game-info {
|
15 |
display: flex;
|
16 |
-
justify-content:
|
17 |
align-items: center;
|
18 |
-
|
19 |
-
font-size: 1.
|
20 |
-
|
21 |
-
|
22 |
-
button {
|
23 |
-
background-color: #646cff;
|
24 |
-
color: white;
|
25 |
-
border: none;
|
26 |
-
padding: 0.5rem 1rem;
|
27 |
-
border-radius: 4px;
|
28 |
-
cursor: pointer;
|
29 |
-
font-size: 1rem;
|
30 |
-
transition: background-color 0.3s;
|
31 |
-
}
|
32 |
-
|
33 |
-
button:hover {
|
34 |
-
background-color: #535bf2;
|
35 |
-
}
|
36 |
-
|
37 |
-
.game-board {
|
38 |
-
display: grid;
|
39 |
-
grid-template-columns: repeat(4, 1fr);
|
40 |
-
grid-gap: 1rem;
|
41 |
-
max-width: 600px;
|
42 |
-
margin: 0 auto;
|
43 |
}
|
44 |
|
45 |
-
.
|
46 |
position: relative;
|
47 |
-
height: 120px;
|
48 |
-
border-radius: 8px;
|
49 |
-
perspective: 1000px;
|
50 |
-
cursor: pointer;
|
51 |
-
transition: transform 0.15s;
|
52 |
-
}
|
53 |
-
|
54 |
-
.card:hover {
|
55 |
-
transform: scale(1.03);
|
56 |
-
}
|
57 |
-
|
58 |
-
.card-front,
|
59 |
-
.card-back {
|
60 |
-
position: absolute;
|
61 |
width: 100%;
|
62 |
-
|
63 |
-
|
64 |
-
|
|
|
65 |
display: flex;
|
66 |
-
align-items: center;
|
67 |
justify-content: center;
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
background-color: white;
|
74 |
-
transform: rotateY(180deg);
|
75 |
-
font-size: 3rem;
|
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 |
display: flex;
|
104 |
-
flex-direction: column;
|
105 |
justify-content: center;
|
106 |
align-items: center;
|
107 |
-
color: white;
|
108 |
-
z-index: 10;
|
109 |
-
}
|
110 |
-
|
111 |
-
.game-over h2 {
|
112 |
-
font-size: 3rem;
|
113 |
-
margin-bottom: 1rem;
|
114 |
}
|
115 |
|
116 |
-
.
|
117 |
-
|
|
|
|
|
|
|
|
|
118 |
}
|
119 |
|
120 |
-
@media (max-width:
|
121 |
-
.
|
122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
}
|
124 |
|
125 |
-
.
|
126 |
-
|
127 |
}
|
128 |
}
|
|
|
1 |
+
.colorful-semantics {
|
2 |
+
max-width: 1100px;
|
3 |
margin: 0 auto;
|
4 |
padding: 2rem;
|
5 |
text-align: center;
|
6 |
+
font-family: 'Arial', sans-serif;
|
7 |
}
|
8 |
|
9 |
+
.loading {
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
display: flex;
|
11 |
+
justify-content: center;
|
12 |
align-items: center;
|
13 |
+
height: 100vh;
|
14 |
+
font-size: 1.5rem;
|
15 |
+
font-weight: bold;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
}
|
17 |
|
18 |
+
.media-container {
|
19 |
position: relative;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
width: 100%;
|
21 |
+
max-width: 800px;
|
22 |
+
height: 45vh;
|
23 |
+
max-height: 400px;
|
24 |
+
margin: 0 auto 3rem;
|
25 |
display: flex;
|
|
|
26 |
justify-content: center;
|
27 |
+
align-items: center;
|
28 |
+
overflow: hidden;
|
29 |
+
border-radius: 8px;
|
30 |
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
31 |
+
background-color: #f0f0f0;
|
|
|
|
|
|
|
32 |
}
|
33 |
|
34 |
+
.game-media {
|
35 |
+
max-width: 100%;
|
36 |
+
max-height: 100%;
|
37 |
+
object-fit: contain;
|
38 |
}
|
39 |
|
40 |
+
.semantic-boxes {
|
41 |
+
display: flex;
|
42 |
+
gap: 1rem;
|
43 |
+
justify-content: center;
|
44 |
+
flex-wrap: wrap;
|
45 |
+
margin-bottom: 2rem;
|
46 |
}
|
47 |
|
48 |
+
.semantic-box {
|
49 |
+
width: 22%;
|
50 |
+
min-width: 120px;
|
51 |
+
padding: 1rem;
|
52 |
+
border-radius: 8px;
|
53 |
+
border: 2px solid #000;
|
54 |
+
font-size: 1.2rem;
|
55 |
+
font-weight: bold;
|
56 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
57 |
+
display: flex;
|
58 |
+
justify-content: center;
|
59 |
+
align-items: center;
|
60 |
+
min-height: 70px;
|
61 |
+
text-align: center;
|
62 |
}
|
63 |
|
64 |
+
.options-container {
|
65 |
+
display: flex;
|
66 |
+
gap: 1rem;
|
67 |
+
justify-content: center;
|
68 |
+
flex-wrap: wrap;
|
69 |
+
margin-top: 2rem;
|
70 |
}
|
71 |
|
72 |
+
.option {
|
73 |
+
background-color: #d3d3d3;
|
74 |
+
border: 2px solid #000;
|
75 |
+
border-radius: 6px;
|
76 |
+
padding: 0.8rem 1.2rem;
|
77 |
+
font-size: 1.1rem;
|
78 |
+
cursor: pointer;
|
79 |
+
transition: transform 0.15s, box-shadow 0.15s;
|
80 |
+
min-width: 120px;
|
81 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
82 |
+
}
|
83 |
+
|
84 |
+
.option:hover {
|
85 |
+
transform: scale(1.05);
|
86 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
87 |
+
}
|
88 |
+
|
89 |
+
.flying-option {
|
90 |
+
padding: 0.8rem 1.2rem;
|
91 |
+
border: 2px solid #000;
|
92 |
+
border-radius: 6px;
|
93 |
+
font-size: 1.1rem;
|
94 |
+
pointer-events: none;
|
95 |
+
z-index: 100;
|
96 |
+
min-width: 120px;
|
97 |
+
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
98 |
+
white-space: nowrap;
|
99 |
display: flex;
|
|
|
100 |
justify-content: center;
|
101 |
align-items: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
}
|
103 |
|
104 |
+
.feedback-message {
|
105 |
+
color: #ff3c3c;
|
106 |
+
font-size: 1.2rem;
|
107 |
+
font-weight: bold;
|
108 |
+
margin-top: 1.5rem;
|
109 |
+
min-height: 28px;
|
110 |
}
|
111 |
|
112 |
+
@media (max-width: 768px) {
|
113 |
+
.semantic-boxes {
|
114 |
+
flex-direction: column;
|
115 |
+
align-items: center;
|
116 |
+
}
|
117 |
+
|
118 |
+
.semantic-box {
|
119 |
+
width: 80%;
|
120 |
+
}
|
121 |
+
|
122 |
+
.options-container {
|
123 |
+
flex-direction: column;
|
124 |
+
align-items: center;
|
125 |
}
|
126 |
|
127 |
+
.option {
|
128 |
+
width: 80%;
|
129 |
}
|
130 |
}
|
src/App.tsx
CHANGED
@@ -1,140 +1,289 @@
|
|
1 |
-
import { useState, useEffect } from 'react'
|
2 |
import './App.css'
|
3 |
|
4 |
-
//
|
5 |
-
interface
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
}
|
11 |
|
12 |
function App() {
|
13 |
-
const
|
14 |
-
const [
|
15 |
-
const [
|
16 |
-
const [
|
17 |
-
const [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
useEffect(() => {
|
21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
}, []);
|
23 |
|
24 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
useEffect(() => {
|
26 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
const timer = setTimeout(() => {
|
28 |
-
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
30 |
return () => clearTimeout(timer);
|
31 |
}
|
32 |
-
}, [
|
33 |
|
34 |
-
//
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
};
|
51 |
|
52 |
-
// Handle
|
53 |
-
const
|
54 |
-
|
55 |
-
if (
|
56 |
-
flippedCards.length === 2 ||
|
57 |
-
flippedCards.includes(id) ||
|
58 |
-
cards[id].matched
|
59 |
-
) {
|
60 |
-
return;
|
61 |
-
}
|
62 |
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
);
|
69 |
|
70 |
-
|
71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
};
|
73 |
|
74 |
-
//
|
75 |
-
const
|
76 |
-
|
|
|
|
|
|
|
77 |
|
78 |
-
if (
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
|
|
86 |
);
|
87 |
} else {
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
)
|
95 |
);
|
96 |
}
|
97 |
-
|
98 |
-
// Increment moves and reset flipped cards
|
99 |
-
setMoves(prevMoves => prevMoves + 1);
|
100 |
-
setFlippedCards([]);
|
101 |
-
|
102 |
-
// Check if all cards are matched
|
103 |
-
const allMatched = cards.every(card => card.matched || flippedCards.includes(card.id));
|
104 |
-
if (allMatched) {
|
105 |
-
setGameOver(true);
|
106 |
-
}
|
107 |
};
|
108 |
|
|
|
|
|
|
|
|
|
109 |
return (
|
110 |
-
<div className="
|
111 |
-
<
|
112 |
-
|
113 |
-
<div>Moves: {moves}</div>
|
114 |
-
<button onClick={initGame}>New Game</button>
|
115 |
</div>
|
116 |
|
117 |
-
<div className="
|
118 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
<div
|
120 |
-
key={
|
121 |
-
className=
|
122 |
-
onClick={() =>
|
123 |
>
|
124 |
-
|
125 |
-
<div className="card-front">{card.content}</div>
|
126 |
</div>
|
127 |
))}
|
128 |
</div>
|
129 |
|
130 |
-
{
|
131 |
-
<div className="
|
132 |
-
|
133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
</div>
|
135 |
)}
|
136 |
</div>
|
137 |
-
)
|
138 |
}
|
139 |
|
140 |
-
export default App
|
|
|
1 |
+
import { useState, useEffect, useRef } from 'react'
|
2 |
import './App.css'
|
3 |
|
4 |
+
// Question interface
|
5 |
+
interface Question {
|
6 |
+
file: string;
|
7 |
+
who: string;
|
8 |
+
doing: string;
|
9 |
+
what: string;
|
10 |
+
to_whom: string;
|
11 |
+
distractors: string[];
|
12 |
+
}
|
13 |
+
|
14 |
+
// State for one round
|
15 |
+
interface GameState {
|
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 [questions, setQuestions] = useState<Question[]>([]);
|
27 |
+
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
28 |
+
const [gameState, setGameState] = useState<GameState | null>(null);
|
29 |
+
const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight });
|
30 |
+
const [feedbackMsg, setFeedbackMsg] = useState('');
|
31 |
+
const [isAnimating, setIsAnimating] = useState(false);
|
32 |
+
const [animationElement, setAnimationElement] = useState<{ content: string, position: { x: number, y: number } } | null>(null);
|
33 |
+
const [isFilled, setIsFilled] = useState(false);
|
34 |
+
|
35 |
+
const wrongMessages = ["Wrong, try again", "Not correct, try again", "Error, come again"];
|
36 |
+
const animationRef = useRef<HTMLDivElement>(null);
|
37 |
+
const destinationRef = useRef<HTMLDivElement>(null);
|
38 |
|
39 |
+
// Colors
|
40 |
+
const ORANGE = "#FF8C00";
|
41 |
+
const YELLOW = "#F0E61E";
|
42 |
+
const GREEN = "#00B900";
|
43 |
+
const PINK = "#FF69B4";
|
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 |
+
loadQuestions();
|
63 |
+
|
64 |
+
// Set up window resize handler
|
65 |
+
const handleResize = () => {
|
66 |
+
setWindowSize({
|
67 |
+
width: window.innerWidth,
|
68 |
+
height: window.innerHeight
|
69 |
+
});
|
70 |
+
};
|
71 |
+
|
72 |
+
window.addEventListener('resize', handleResize);
|
73 |
+
return () => window.removeEventListener('resize', handleResize);
|
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 |
+
const frameInterval = setInterval(() => {
|
88 |
+
if (!isAnimating && !isFilled) {
|
89 |
+
setGameState(prev => {
|
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 |
+
return () => clearInterval(frameInterval);
|
98 |
+
}, [gameState, isAnimating, isFilled]);
|
99 |
+
|
100 |
+
// Process animation completion and next question timing
|
101 |
+
useEffect(() => {
|
102 |
+
if (isFilled) {
|
103 |
const timer = setTimeout(() => {
|
104 |
+
// Move to next question
|
105 |
+
setCurrentQuestionIndex(prev => (prev + 1) % questions.length);
|
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 (isAnimating && animationElement && animationRef.current && destinationRef.current) {
|
118 |
+
const destRect = destinationRef.current.getBoundingClientRect();
|
119 |
+
const targetPosition = {
|
120 |
+
x: destRect.left + destRect.width / 2,
|
121 |
+
y: destRect.top + destRect.height / 2
|
122 |
+
};
|
123 |
+
|
124 |
+
const anim = animationRef.current;
|
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 |
+
}, [isAnimating, animationElement]);
|
137 |
+
|
138 |
+
// Prepare a new game round
|
139 |
+
const prepareRound = (question: Question) => {
|
140 |
+
const choices = [question.to_whom, ...question.distractors];
|
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 |
+
setGameState({
|
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 |
+
setFeedbackMsg('');
|
161 |
+
setIsAnimating(false);
|
162 |
+
setAnimationElement(null);
|
163 |
+
setIsFilled(false);
|
164 |
};
|
165 |
|
166 |
+
// Handle option selection
|
167 |
+
const handleOptionClick = (option: string, event: React.MouseEvent<HTMLDivElement>) => {
|
168 |
+
if (isAnimating || isFilled || !gameState) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
169 |
|
170 |
+
const rect = event.currentTarget.getBoundingClientRect();
|
171 |
+
const position = {
|
172 |
+
x: rect.left + rect.width / 2,
|
173 |
+
y: rect.top + rect.height / 2
|
174 |
+
};
|
|
|
175 |
|
176 |
+
if (option === gameState.answer) {
|
177 |
+
// Correct answer
|
178 |
+
setAnimationElement({
|
179 |
+
content: option,
|
180 |
+
position
|
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 |
+
const currentFrame = gameState.frames[gameState.frameIndex];
|
195 |
+
const isVideo = currentFrame.endsWith('.mp4');
|
196 |
|
197 |
+
if (isVideo) {
|
198 |
+
return (
|
199 |
+
<video
|
200 |
+
className="game-media"
|
201 |
+
src={currentFrame}
|
202 |
+
autoPlay
|
203 |
+
loop
|
204 |
+
muted
|
205 |
+
/>
|
206 |
);
|
207 |
} else {
|
208 |
+
return (
|
209 |
+
<img
|
210 |
+
className="game-media"
|
211 |
+
src={currentFrame}
|
212 |
+
alt="Game visual"
|
213 |
+
/>
|
|
|
214 |
);
|
215 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
216 |
};
|
217 |
|
218 |
+
if (!gameState) {
|
219 |
+
return <div className="loading">Loading questions...</div>;
|
220 |
+
}
|
221 |
+
|
222 |
return (
|
223 |
+
<div className="colorful-semantics">
|
224 |
+
<div className="media-container">
|
225 |
+
{renderMedia()}
|
|
|
|
|
226 |
</div>
|
227 |
|
228 |
+
<div className="semantic-boxes">
|
229 |
+
{/* Who - Orange */}
|
230 |
+
<div className="semantic-box" style={{ backgroundColor: ORANGE }}>
|
231 |
+
{gameState.who}
|
232 |
+
</div>
|
233 |
+
|
234 |
+
{/* Doing - Yellow */}
|
235 |
+
<div className="semantic-box" style={{ backgroundColor: YELLOW }}>
|
236 |
+
{gameState.doing}
|
237 |
+
</div>
|
238 |
+
|
239 |
+
{/* What - Green */}
|
240 |
+
<div className="semantic-box" style={{ backgroundColor: GREEN }}>
|
241 |
+
{gameState.what}
|
242 |
+
</div>
|
243 |
+
|
244 |
+
{/* To Whom - Pink */}
|
245 |
+
<div
|
246 |
+
className="semantic-box"
|
247 |
+
style={{ backgroundColor: PINK }}
|
248 |
+
ref={destinationRef}
|
249 |
+
>
|
250 |
+
{isFilled ? gameState.answer : "?"}
|
251 |
+
</div>
|
252 |
+
</div>
|
253 |
+
|
254 |
+
<div className="options-container">
|
255 |
+
{!isFilled && gameState.choices.map((option, index) => (
|
256 |
<div
|
257 |
+
key={index}
|
258 |
+
className="option"
|
259 |
+
onClick={(e) => handleOptionClick(option, e)}
|
260 |
>
|
261 |
+
{option}
|
|
|
262 |
</div>
|
263 |
))}
|
264 |
</div>
|
265 |
|
266 |
+
{feedbackMsg && (
|
267 |
+
<div className="feedback-message">{feedbackMsg}</div>
|
268 |
+
)}
|
269 |
+
|
270 |
+
{isAnimating && animationElement && (
|
271 |
+
<div
|
272 |
+
className="flying-option"
|
273 |
+
ref={animationRef}
|
274 |
+
style={{
|
275 |
+
backgroundColor: PINK,
|
276 |
+
position: 'fixed',
|
277 |
+
left: animationElement.position.x,
|
278 |
+
top: animationElement.position.y,
|
279 |
+
transform: 'translate(-50%, -50%)'
|
280 |
+
}}
|
281 |
+
>
|
282 |
+
{animationElement.content}
|
283 |
</div>
|
284 |
)}
|
285 |
</div>
|
286 |
+
);
|
287 |
}
|
288 |
|
289 |
+
export default App;
|