elyor-ml commited on
Commit
3e4b4c2
·
1 Parent(s): 580f6a6

updated game

Browse files
README.md CHANGED
@@ -1,36 +1,58 @@
1
  ---
2
- title: Memory Card Game
3
  emoji: 🎮
4
- colorFrom: blue
5
- colorTo: purple
6
  sdk: docker
7
  pinned: false
8
  app_port: 3000
9
  ---
10
 
11
- # Memory Card Game
12
 
13
- A fun memory matching game built with React and Vite.
14
 
15
- Play by clicking on cards to reveal them and find matching pairs. The game tracks your moves and lets you know when you've won!
16
 
17
- ## Technology Stack
18
- - React + TypeScript
19
- - Vite
20
- - Docker
21
- # Memory Card Game
22
 
23
- A simple memory card game built with React and Vite.
24
 
25
- ## Features
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- - Match pairs of cards with emoji symbols
28
- - Track number of moves
29
- - Responsive design for desktop and mobile
 
 
 
 
30
 
31
- ## Quick Start
32
 
33
- ### Local Development
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="/vite.svg" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>Memory Card Game</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": "memory-card-game",
3
  "private": true,
4
- "version": "0.0.0",
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

  • SHA256: 80f277cbc2e8f56bbcb87b7e45b4c5d2d9e9b0192ac05717b9b674a71facbe03
  • Pointer size: 131 Bytes
  • Size of remote file: 111 kB
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
- .app {
2
- max-width: 800px;
3
  margin: 0 auto;
4
  padding: 2rem;
5
  text-align: center;
 
6
  }
7
 
8
- h1 {
9
- font-size: 2.5rem;
10
- margin-bottom: 1.5rem;
11
- color: #333;
12
- }
13
-
14
- .game-info {
15
  display: flex;
16
- justify-content: space-between;
17
  align-items: center;
18
- margin-bottom: 2rem;
19
- font-size: 1.2rem;
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
- .card {
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
- height: 100%;
63
- backface-visibility: hidden;
64
- border-radius: 8px;
 
65
  display: flex;
66
- align-items: center;
67
  justify-content: center;
68
- transition: transform 0.6s;
69
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
70
- }
71
-
72
- .card-front {
73
- background-color: white;
74
- transform: rotateY(180deg);
75
- font-size: 3rem;
76
  }
77
 
78
- .card-back {
79
- background-color: #646cff;
80
- color: white;
81
- font-size: 2rem;
82
  }
83
 
84
- .card.flipped .card-front {
85
- transform: rotateY(0);
 
 
 
 
86
  }
87
 
88
- .card.flipped .card-back {
89
- transform: rotateY(180deg);
 
 
 
 
 
 
 
 
 
 
 
 
90
  }
91
 
92
- .card.matched {
93
- opacity: 0.7;
 
 
 
 
94
  }
95
 
96
- .game-over {
97
- position: fixed;
98
- top: 0;
99
- left: 0;
100
- right: 0;
101
- bottom: 0;
102
- background-color: rgba(0, 0, 0, 0.7);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- .game-over p {
117
- font-size: 1.5rem;
 
 
 
 
118
  }
119
 
120
- @media (max-width: 600px) {
121
- .game-board {
122
- grid-template-columns: repeat(3, 1fr);
 
 
 
 
 
 
 
 
 
 
123
  }
124
 
125
- .card {
126
- height: 100px;
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
- // Card interface
5
- interface Card {
6
- id: number;
7
- content: string;
8
- flipped: boolean;
9
- matched: boolean;
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  }
11
 
12
  function App() {
13
- const emojis = ['🐱', '🐶', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼'];
14
- const [cards, setCards] = useState<Card[]>([]);
15
- const [flippedCards, setFlippedCards] = useState<number[]>([]);
16
- const [moves, setMoves] = useState(0);
17
- const [gameOver, setGameOver] = useState(false);
 
 
 
 
 
 
 
18
 
19
- // Initialize game
 
 
 
 
 
 
20
  useEffect(() => {
21
- initGame();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }, []);
23
 
24
- // Check for matches when two cards are flipped
 
 
 
 
 
 
 
25
  useEffect(() => {
26
- if (flippedCards.length === 2) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  const timer = setTimeout(() => {
28
- checkForMatch();
29
- }, 700);
 
 
 
 
 
30
  return () => clearTimeout(timer);
31
  }
32
- }, [flippedCards]);
33
 
34
- // Init game
35
- const initGame = () => {
36
- // Create pairs of cards with emojis
37
- const cardPairs = [...emojis, ...emojis]
38
- .map((emoji, index) => ({
39
- id: index,
40
- content: emoji,
41
- flipped: false,
42
- matched: false
43
- }))
44
- .sort(() => Math.random() - 0.5);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- setCards(cardPairs);
47
- setFlippedCards([]);
48
- setMoves(0);
49
- setGameOver(false);
50
  };
51
 
52
- // Handle card click
53
- const handleCardClick = (id: number) => {
54
- // Ignore if card is already flipped or if 2 cards are already flipped
55
- if (
56
- flippedCards.length === 2 ||
57
- flippedCards.includes(id) ||
58
- cards[id].matched
59
- ) {
60
- return;
61
- }
62
 
63
- // Flip card
64
- setCards(prevCards =>
65
- prevCards.map(card =>
66
- card.id === id ? { ...card, flipped: true } : card
67
- )
68
- );
69
 
70
- // Add card to flipped cards
71
- setFlippedCards(prev => [...prev, id]);
 
 
 
 
 
 
 
 
 
 
72
  };
73
 
74
- // Check for match
75
- const checkForMatch = () => {
76
- const [first, second] = flippedCards;
 
 
 
77
 
78
- if (cards[first].content === cards[second].content) {
79
- // If match, keep cards flipped and mark as matched
80
- setCards(prevCards =>
81
- prevCards.map(card =>
82
- card.id === first || card.id === second
83
- ? { ...card, matched: true }
84
- : card
85
- )
 
86
  );
87
  } else {
88
- // If no match, flip cards back
89
- setCards(prevCards =>
90
- prevCards.map(card =>
91
- card.id === first || card.id === second
92
- ? { ...card, flipped: false }
93
- : card
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="app">
111
- <h1>Memory Game</h1>
112
- <div className="game-info">
113
- <div>Moves: {moves}</div>
114
- <button onClick={initGame}>New Game</button>
115
  </div>
116
 
117
- <div className="game-board">
118
- {cards.map(card => (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  <div
120
- key={card.id}
121
- className={`card ${card.flipped || card.matched ? 'flipped' : ''} ${card.matched ? 'matched' : ''}`}
122
- onClick={() => handleCardClick(card.id)}
123
  >
124
- <div className="card-back">?</div>
125
- <div className="card-front">{card.content}</div>
126
  </div>
127
  ))}
128
  </div>
129
 
130
- {gameOver && (
131
- <div className="game-over">
132
- <h2>Game Over!</h2>
133
- <p>You won in {moves} moves!</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
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;