elyor-ml commited on
Commit
0f4962a
·
1 Parent(s): ac5a358

to where game

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ node_modules/
2
+ .env
3
+ .env.local
4
+ .env.development.local
5
+ .env.test.local
6
+ .env.production.local
7
+ img-gen/
8
+
App.css ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .colorful-semantics {
2
+ max-width: 1100px;
3
+ margin: 0 0 0 auto;
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: flex-end;
11
+ }
12
+
13
+ .loading {
14
+ display: flex;
15
+ justify-content: center;
16
+ align-items: center;
17
+ height: 100vh;
18
+ font-size: 1.5rem;
19
+ font-weight: bold;
20
+ }
21
+
22
+ .media-container {
23
+ position: relative;
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
+ overflow: hidden;
33
+ border-radius: 12px;
34
+ box-shadow: 0 6px 14px rgba(0, 0, 0, 0.25);
35
+ background-color: #f0f0f0;
36
+ }
37
+
38
+ .game-media {
39
+ max-width: 100%;
40
+ max-height: 100%;
41
+ object-fit: contain;
42
+ }
43
+
44
+ .semantic-boxes {
45
+ display: flex;
46
+ gap: 1rem;
47
+ justify-content: center;
48
+ flex-wrap: nowrap;
49
+ margin-bottom: 2rem;
50
+ }
51
+
52
+ .semantic-box {
53
+ /* Let width grow/shrink based on content so long phrases stay on one line */
54
+ width: auto;
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
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
62
+ display: flex;
63
+ justify-content: center;
64
+ align-items: center;
65
+ min-height: 90px;
66
+ text-align: center;
67
+ white-space: nowrap; /* prevent text from wrapping to a new line */
68
+ }
69
+
70
+ .options-container {
71
+ display: flex;
72
+ gap: 1rem;
73
+ justify-content: center;
74
+ flex-wrap: wrap;
75
+ margin-top: 2rem;
76
+ }
77
+
78
+ .option {
79
+ background-color: #d3d3d3;
80
+ border: 3px solid #000;
81
+ border-radius: 8px;
82
+ padding: 1rem 1.5rem;
83
+ font-size: 1.3rem;
84
+ cursor: pointer;
85
+ transition: transform 0.15s, box-shadow 0.15s;
86
+ min-width: 140px;
87
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
88
+ }
89
+
90
+ .option:hover {
91
+ transform: scale(1.05);
92
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
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
+ .feedback-message {
111
+ color: #ff3c3c;
112
+ font-size: 1.2rem;
113
+ font-weight: bold;
114
+ margin-top: 1.5rem;
115
+ min-height: 28px;
116
+ }
117
+
118
+ .big-question {
119
+ position: fixed;
120
+ right: 220px;
121
+ top: 30%;
122
+ font-size: 6rem;
123
+ font-weight: bold;
124
+ color: #FF69B4;
125
+ text-align: left;
126
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
127
+ z-index: 10;
128
+ }
129
+
130
+ @media (max-width: 768px) {
131
+ .semantic-boxes {
132
+ flex-wrap: wrap;
133
+ align-items: center;
134
+ }
135
+
136
+ .semantic-box {
137
+ width: 80%;
138
+ }
139
+
140
+ .options-container {
141
+ flex-direction: column;
142
+ align-items: center;
143
+ }
144
+
145
+ .option {
146
+ width: 80%;
147
+ }
148
+ }
App.tsx ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_where: 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 _UNUSED_PINK_ORIGINAL = "#FF69B4"; // Keeping original for context, effectively replaced
44
+ const _UNUSED_BLUE_ORIGINAL = "#007BFF"; // Keeping original for context, effectively replaced
45
+ const LIGHT_BLUE = "#87CEEB"; // New light blue color
46
+ const PINK = LIGHT_BLUE; // PINK is now LIGHT_BLUE for animation
47
+ const BLUE = LIGHT_BLUE; // BLUE is now LIGHT_BLUE for the target box
48
+
49
+ // Load questions on mount
50
+ useEffect(() => {
51
+ const loadQuestions = async () => {
52
+ try {
53
+ const response = await fetch('/to-where.json');
54
+ if (!response.ok) {
55
+ throw new Error('Failed to load questions');
56
+ }
57
+ const data: Question[] = await response.json();
58
+ // Shuffle questions
59
+ const shuffled = [...data].sort(() => Math.random() - 0.5);
60
+ setQuestions(shuffled);
61
+ } catch (error) {
62
+ console.error('Error loading questions:', error);
63
+ }
64
+ };
65
+
66
+ loadQuestions();
67
+
68
+ // Set up window resize handler
69
+ const handleResize = () => {
70
+ setWindowSize({
71
+ width: window.innerWidth,
72
+ height: window.innerHeight
73
+ });
74
+ };
75
+
76
+ window.addEventListener('resize', handleResize);
77
+ return () => window.removeEventListener('resize', handleResize);
78
+ }, []);
79
+
80
+ // Prepare game state when questions load or change
81
+ useEffect(() => {
82
+ if (questions.length > 0) {
83
+ prepareRound(questions[currentQuestionIndex]);
84
+ }
85
+ }, [questions, currentQuestionIndex]);
86
+
87
+ // Handle video/image frame updates
88
+ useEffect(() => {
89
+ if (!gameState || !gameState.frames.length) return;
90
+
91
+ const frameInterval = setInterval(() => {
92
+ if (!isAnimating && !isFilled) {
93
+ setGameState(prev => {
94
+ if (!prev) return prev;
95
+ const nextIndex = (prev.frameIndex + 1) % prev.frames.length;
96
+ return { ...prev, frameIndex: nextIndex };
97
+ });
98
+ }
99
+ }, 100); // Adjust frame rate as needed
100
+
101
+ return () => clearInterval(frameInterval);
102
+ }, [gameState, isAnimating, isFilled]);
103
+
104
+ // Process animation completion and next question timing
105
+ useEffect(() => {
106
+ if (isFilled) {
107
+ const timer = setTimeout(() => {
108
+ // Move to next question
109
+ setCurrentQuestionIndex(prev => (prev + 1) % questions.length);
110
+ setIsFilled(false);
111
+ setFeedbackMsg('');
112
+ setAnimationElement(null);
113
+ }, 1000);
114
+
115
+ return () => clearTimeout(timer);
116
+ }
117
+ }, [isFilled, questions.length]);
118
+
119
+ // Animation effect
120
+ useEffect(() => {
121
+ if (isAnimating && animationElement && animationRef.current && destinationRef.current) {
122
+ const destRect = destinationRef.current.getBoundingClientRect();
123
+ const targetPosition = {
124
+ x: destRect.left + destRect.width / 2,
125
+ y: destRect.top + destRect.height / 2
126
+ };
127
+
128
+ const anim = animationRef.current;
129
+ anim.style.transition = 'transform 500ms linear';
130
+ anim.style.transform = `translate(${targetPosition.x - animationElement.position.x}px, ${targetPosition.y - animationElement.position.y}px)`;
131
+
132
+ const handleAnimationEnd = () => {
133
+ setIsAnimating(false);
134
+ setIsFilled(true);
135
+ };
136
+
137
+ anim.addEventListener('transitionend', handleAnimationEnd);
138
+ return () => anim.removeEventListener('transitionend', handleAnimationEnd);
139
+ }
140
+ }, [isAnimating, animationElement]);
141
+
142
+ // Prepare a new game round
143
+ const prepareRound = (question: Question) => {
144
+ // Shuffle the distractors first, then take two
145
+ const shuffledDistractors = [...question.distractors].sort(() => Math.random() - 0.5);
146
+ const selectedDistractors = shuffledDistractors.slice(0, 2);
147
+ const choices = [question.to_where, ...selectedDistractors];
148
+ // Shuffle the final 3 choices
149
+ const shuffledChoices = [...choices].sort(() => Math.random() - 0.5);
150
+
151
+ // Determine if media is image or video
152
+ const isVideo = question.file.endsWith('.mp4');
153
+ const frames = isVideo
154
+ ? [question.file] // For simplicity, we'll just use the path for videos
155
+ : [question.file]; // Same for images
156
+
157
+ setGameState({
158
+ frames,
159
+ frameIndex: 0,
160
+ who: question.who,
161
+ doing: question.doing,
162
+ what: question.what,
163
+ answer: question.to_where,
164
+ choices: shuffledChoices
165
+ });
166
+
167
+ setFeedbackMsg('');
168
+ setIsAnimating(false);
169
+ setAnimationElement(null);
170
+ setIsFilled(false);
171
+ };
172
+
173
+ // Handle option selection
174
+ const handleOptionClick = (option: string, event: React.MouseEvent<HTMLDivElement>) => {
175
+ if (isAnimating || isFilled || !gameState) return;
176
+
177
+ const rect = event.currentTarget.getBoundingClientRect();
178
+ const position = {
179
+ x: rect.left + rect.width / 2,
180
+ y: rect.top + rect.height / 2
181
+ };
182
+
183
+ if (option === gameState.answer) {
184
+ // Correct answer
185
+ setAnimationElement({
186
+ content: option,
187
+ position
188
+ });
189
+ setIsAnimating(true);
190
+ setFeedbackMsg('');
191
+ } else {
192
+ // Wrong answer
193
+ setFeedbackMsg(wrongMessages[Math.floor(Math.random() * wrongMessages.length)]);
194
+ }
195
+ };
196
+
197
+ // Render current media (image or video)
198
+ const renderMedia = () => {
199
+ if (!gameState || !gameState.frames.length) return null;
200
+
201
+ const currentFrame = gameState.frames[gameState.frameIndex];
202
+ const isVideo = currentFrame.endsWith('.mp4');
203
+
204
+ if (isVideo) {
205
+ return (
206
+ <video
207
+ className="game-media"
208
+ src={currentFrame}
209
+ autoPlay
210
+ loop
211
+ muted
212
+ />
213
+ );
214
+ } else {
215
+ return (
216
+ <img
217
+ className="game-media"
218
+ src={currentFrame}
219
+ alt="Game visual"
220
+ />
221
+ );
222
+ }
223
+ };
224
+
225
+ if (!gameState) {
226
+ return <div className="loading">Loading questions...</div>;
227
+ }
228
+
229
+ return (
230
+ <div className="colorful-semantics">
231
+ <div className="big-question" style={{ color: LIGHT_BLUE }}>To where? →</div>
232
+ <div className="media-container">
233
+ {renderMedia()}
234
+ </div>
235
+
236
+ <div className="semantic-boxes">
237
+ {/* Who - Orange */}
238
+ <div className="semantic-box" style={{ backgroundColor: ORANGE }}>
239
+ {gameState.who}
240
+ </div>
241
+
242
+ {/* Doing - Yellow */}
243
+ <div className="semantic-box" style={{ backgroundColor: YELLOW }}>
244
+ {gameState.doing}
245
+ </div>
246
+
247
+ {/* What - Green */}
248
+ {gameState.what && (
249
+ <div className="semantic-box" style={{ backgroundColor: GREEN }}>
250
+ {gameState.what}
251
+ </div>
252
+ )}
253
+
254
+ {/* To Where - Blue (now Light Blue) */}
255
+ <div
256
+ className="semantic-box"
257
+ style={{ backgroundColor: LIGHT_BLUE }}
258
+ ref={destinationRef}
259
+ >
260
+ {isFilled ? gameState.answer : "to where? →"}
261
+ </div>
262
+ </div>
263
+
264
+ <div className="options-container">
265
+ {!isFilled && gameState.choices.map((option, index) => (
266
+ <div
267
+ key={index}
268
+ className="option"
269
+ onClick={(e) => handleOptionClick(option, e)}
270
+ >
271
+ {option}
272
+ </div>
273
+ ))}
274
+ </div>
275
+
276
+ {feedbackMsg && (
277
+ <div className="feedback-message">{feedbackMsg}</div>
278
+ )}
279
+
280
+ {isAnimating && animationElement && (
281
+ <div
282
+ className="flying-option"
283
+ ref={animationRef}
284
+ style={{
285
+ backgroundColor: LIGHT_BLUE,
286
+ position: 'fixed',
287
+ left: animationElement.position.x,
288
+ top: animationElement.position.y,
289
+ transform: 'translate(-50%, -50%)'
290
+ }}
291
+ >
292
+ {animationElement.content}
293
+ </div>
294
+ )}
295
+ </div>
296
+ );
297
+ }
298
+
299
+ export default App;
Dockerfile CHANGED
@@ -1,16 +1,27 @@
1
- # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
- # you will also find guides on how best to write your Dockerfile
3
 
4
- FROM python:3.9
 
 
 
 
5
 
6
- RUN useradd -m -u 1000 user
7
- USER user
8
- ENV PATH="/home/user/.local/bin:$PATH"
 
 
 
9
 
10
  WORKDIR /app
11
 
12
- COPY --chown=user ./requirements.txt requirements.txt
13
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
 
 
14
 
15
- COPY --chown=user . /app
16
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
1
+ # Build stage
2
+ FROM node:20-alpine as build
3
 
4
+ WORKDIR /app
5
+
6
+ # Copy package files and install dependencies
7
+ COPY package.json package-lock.json ./
8
+ RUN npm ci
9
 
10
+ # Copy all files and build the project
11
+ COPY . .
12
+ RUN npm run build
13
+
14
+ # Serve stage
15
+ FROM node:20-alpine as serve
16
 
17
  WORKDIR /app
18
 
19
+ # Install serve globally
20
+ RUN npm install -g serve
21
+
22
+ # Copy built files from previous stage
23
+ COPY --from=build /app/dist ./dist
24
 
25
+ # Expose port and start server
26
+ EXPOSE 3000
27
+ CMD ["serve", "-s", "dist", "-l", "3000"]
README.md CHANGED
@@ -6,5 +6,182 @@ colorTo: pink
6
  sdk: docker
7
  pinned: false
8
  ---
 
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  sdk: docker
7
  pinned: false
8
  ---
9
+ # Colorful Semantics - To where? Game
10
 
11
+ A learning game that teaches sentence structure through colorful semantic components.
12
+
13
+ ## Features
14
+
15
+ - Interactive game for learning "to whom" sentence components
16
+ - Visual and animated feedback for correct answers
17
+ - Support for both images and videos
18
+ - Customizable questions and answers
19
+
20
+ ## How to Play
21
+
22
+ 1. Look at the image/video and read the colored sentence parts
23
+ 2. Choose the correct "to whom" option from the choices below
24
+ 3. The game will animate the correct answer to the pink box
25
+ 4. Continue to the next question automatically
26
+
27
+ ## Adding New Questions
28
+
29
+ 1. Add your image or video to the `public/media` folder
30
+ 2. Edit the `public/questions.json` file to add a new question:
31
+
32
+ ```json
33
+ {
34
+ "file": "media/your_image.jpg",
35
+ "who": "The boy",
36
+ "doing": "is giving",
37
+ "what": "a book",
38
+ "to_whom": "to the teacher",
39
+ "distractors": ["to Mom", "to the dog", "to Grandma"]
40
+ }
41
+ ```
42
+
43
+ 3. Each question needs the following fields:
44
+ - `file`: Path to the media file (jpg, png, or mp4)
45
+ - `who`: The subject of the sentence
46
+ - `doing`: The verb phrase
47
+ - `what`: The object
48
+ - `to_whom`: The correct answer (recipient)
49
+ - `distractors`: Array of incorrect options
50
+
51
+ 4. Restart the application to see your new questions
52
+
53
+ ## Development
54
+
55
+ ```bash
56
+ # Install dependencies
57
+ npm install
58
+
59
+ # Start development server
60
+ npm run dev
61
+
62
+ # Build for production
63
+ npm run build
64
+ ```
65
+
66
+
67
+ ## Making sentences
68
+
69
+ The words used for subject and to whome:
70
+ ## subjects and to whom
71
+ Blippi
72
+ Zaki
73
+ Harun
74
+ Rashid
75
+ Kazwa
76
+ Bilal
77
+ honeybee
78
+ dinasaour
79
+ boy
80
+ girl
81
+ man
82
+ woman
83
+ lady
84
+ children
85
+ kids
86
+ cat
87
+ dog
88
+ mouse
89
+ tiger
90
+ witch
91
+ elephent
92
+ Mustafo
93
+ Muhammad
94
+ Hidoyat
95
+ Sardor
96
+ Ulugbek
97
+ Policeman
98
+ Nurse
99
+ Pilot
100
+ Florist
101
+ Builder
102
+ Teacher
103
+ Doctor
104
+ Dentist
105
+ Binman
106
+ Postman
107
+ Firefighter
108
+ Lollypop lady
109
+ Gardner
110
+ Mechanic
111
+ Painter
112
+ Lifeguard
113
+ librarian
114
+ bus driver
115
+ cook / chef
116
+ barber
117
+ Farmer
118
+ Astronaut
119
+ Paramedic
120
+
121
+
122
+ ### verbs
123
+ read
124
+ drink
125
+ case
126
+ sit
127
+ buy
128
+ jump
129
+ hold
130
+ cry
131
+ ride
132
+ wash
133
+ fly
134
+ eat
135
+ dry
136
+ play
137
+ climb
138
+ scratch
139
+ cut
140
+ brush
141
+ bounce
142
+ argue
143
+ work
144
+ fight
145
+ practice
146
+ throw
147
+ take
148
+ bring
149
+ take
150
+ wipe
151
+ drink
152
+
153
+
154
+ ## what
155
+ flower
156
+ icecream
157
+ pen
158
+ pencil
159
+ socks
160
+ underwear
161
+ coat
162
+ wellies
163
+ shoes
164
+ spoon
165
+ fork
166
+ food
167
+ bread
168
+ butter
169
+ salt
170
+ chilly
171
+ bathroom
172
+ towel
173
+ coffee
174
+ tea
175
+ milk
176
+ almond
177
+ monkey nuts
178
+ upstairs
179
+ downstairs
180
+
181
+
182
+
183
+
184
+
185
+ ## License
186
+
187
+ MIT
assets/react.svg ADDED
eslint.config.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ 'react-hooks': reactHooks,
18
+ 'react-refresh': reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ 'react-refresh/only-export-components': [
23
+ 'warn',
24
+ { allowConstantExport: true },
25
+ ],
26
+ },
27
+ },
28
+ )
index.css ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ a {
17
+ font-weight: 500;
18
+ color: #646cff;
19
+ text-decoration: inherit;
20
+ }
21
+ a:hover {
22
+ color: #535bf2;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ display: flex;
28
+ place-items: center;
29
+ min-width: 320px;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 3.2em;
35
+ line-height: 1.1;
36
+ }
37
+
38
+ button {
39
+ border-radius: 8px;
40
+ border: 1px solid transparent;
41
+ padding: 0.6em 1.2em;
42
+ font-size: 1em;
43
+ font-weight: 500;
44
+ font-family: inherit;
45
+ background-color: #1a1a1a;
46
+ cursor: pointer;
47
+ transition: border-color 0.25s;
48
+ }
49
+ button:hover {
50
+ border-color: #646cff;
51
+ }
52
+ button:focus,
53
+ button:focus-visible {
54
+ outline: 4px auto -webkit-focus-ring-color;
55
+ }
56
+
57
+ @media (prefers-color-scheme: light) {
58
+ :root {
59
+ color: #213547;
60
+ background-color: #ffffff;
61
+ }
62
+ a:hover {
63
+ color: #747bff;
64
+ }
65
+ button {
66
+ background-color: #f9f9f9;
67
+ }
68
+ }
index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
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>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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",
10
+ "lint": "eslint .",
11
+ "preview": "vite preview"
12
+ },
13
+ "dependencies": {
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/js": "^9.22.0",
19
+ "@types/react": "^19.0.10",
20
+ "@types/react-dom": "^19.0.4",
21
+ "@vitejs/plugin-react": "^4.3.4",
22
+ "eslint": "^9.22.0",
23
+ "eslint-plugin-react-hooks": "^5.2.0",
24
+ "eslint-plugin-react-refresh": "^0.4.19",
25
+ "globals": "^16.0.0",
26
+ "typescript": "~5.7.2",
27
+ "typescript-eslint": "^8.26.1",
28
+ "vite": "^6.3.1"
29
+ }
30
+ }
public/.DS_Store ADDED
Binary file (6.15 kB). View file
 
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/sent02.jpg ADDED

Git LFS Details

  • SHA256: d376dcacf91f0782f4f38db1fb4dd23475cfc5c1a83a248bcddb258c3ab45606
  • Pointer size: 131 Bytes
  • Size of remote file: 150 kB
public/media/sent03.jpg ADDED

Git LFS Details

  • SHA256: a0954f4681b51ccc35d684e8efa151b568ca79ebe3a84d082b5130eaf3f7cad7
  • Pointer size: 131 Bytes
  • Size of remote file: 222 kB
public/media/sent04.jpg ADDED

Git LFS Details

  • SHA256: 485f342efdac4ea6eb3db46498bc1b1e93345bec9612b14a97e57f9c32378798
  • Pointer size: 131 Bytes
  • Size of remote file: 176 kB
public/media/sent05.jpg ADDED

Git LFS Details

  • SHA256: 59f2267bf74bda501aee740bf84b254bfb2de74baddfd2947894701e743f2982
  • Pointer size: 131 Bytes
  • Size of remote file: 163 kB
public/media/sent06.jpg ADDED

Git LFS Details

  • SHA256: 9a1e56c9a2b3d7976b67e34094474c533dcff2bfb9354fc1850b322835e2409e
  • Pointer size: 131 Bytes
  • Size of remote file: 239 kB
public/media/sent07.jpg ADDED

Git LFS Details

  • SHA256: e0a5d826e28949b6b4580ab5c7a533242b25ccfbf5837e5c3ccbaed219e37c40
  • Pointer size: 131 Bytes
  • Size of remote file: 181 kB
public/media/sent08.jpg ADDED

Git LFS Details

  • SHA256: 43e430bea17a0daea839f7b355f993ddaecf90e24afb322a1531fc6775dfac9a
  • Pointer size: 131 Bytes
  • Size of remote file: 220 kB
public/media/sent09.jpg ADDED

Git LFS Details

  • SHA256: 7129a05017e6241f0d436188260d3090a3c356ce55b6d04c64180a02217f021a
  • Pointer size: 131 Bytes
  • Size of remote file: 177 kB
public/media/sent10.jpg ADDED

Git LFS Details

  • SHA256: 0d40bd84da904315947c8fccdac07d71f205861fdf43e930960f005dadcdb515
  • Pointer size: 130 Bytes
  • Size of remote file: 94 kB
public/media/sent11.jpg ADDED

Git LFS Details

  • SHA256: cb638d73f13fac3623e6622b1cd9f833bb255c36f7d41d7fdbdca9d626624e5c
  • Pointer size: 131 Bytes
  • Size of remote file: 188 kB
public/media/sent12.jpg ADDED

Git LFS Details

  • SHA256: 4d07f33fb9a49c6a831039e7fd2063e0101009b24dc08dfc223a311abfb43db5
  • Pointer size: 131 Bytes
  • Size of remote file: 211 kB
public/media/sent13.jpg ADDED

Git LFS Details

  • SHA256: c9b6d7ab35aec42d33ed87b4ebcad9e861ed788d51f40c4aeee1fe9e542af803
  • Pointer size: 131 Bytes
  • Size of remote file: 101 kB
public/media/sent14.jpg ADDED

Git LFS Details

  • SHA256: 2f9960e73eebd60f26add29647148a5ce240e8b27f07c93b3c3781a11d49a324
  • Pointer size: 131 Bytes
  • Size of remote file: 192 kB
public/media/sent15.jpg ADDED

Git LFS Details

  • SHA256: a9995bb48648eb7c9e887781bbd8ce85d8120ea1e7db961e60e8b82bacce493c
  • Pointer size: 131 Bytes
  • Size of remote file: 154 kB
public/media/sent16.jpg ADDED

Git LFS Details

  • SHA256: 5161fadf1066cd9c23d6f199f1aa64d50a3615b8d6921c427d33b531fd40d287
  • Pointer size: 131 Bytes
  • Size of remote file: 177 kB
public/media/sent17.jpg ADDED

Git LFS Details

  • SHA256: 4d3283cb568a57ec57460b54a3dfd72a4d2718d9ac3438bc7c4250f1946fd1da
  • Pointer size: 131 Bytes
  • Size of remote file: 142 kB
public/media/sent18.jpg ADDED

Git LFS Details

  • SHA256: 2c62fbcbdbae737267364886582fd7d4cb2d5cceaffb6f5356748fc86a332beb
  • Pointer size: 131 Bytes
  • Size of remote file: 146 kB
public/media/sent19.jpg ADDED

Git LFS Details

  • SHA256: b1b6c78a1c9edf9018f983ae00b2e74aeec4fc03454938f8f24cefab2fbb2544
  • Pointer size: 131 Bytes
  • Size of remote file: 171 kB
public/media/sent20.jpg ADDED

Git LFS Details

  • SHA256: 04e0ea93f40aabe3618a788ee0846de67bc648df8376826d1330bba96d60d5bc
  • Pointer size: 131 Bytes
  • Size of remote file: 198 kB
public/media/sent21.jpg ADDED

Git LFS Details

  • SHA256: 36b177b59146c34a0699f7149ac18b997a2c4c5cd7b548feac924abcfed4f4bc
  • Pointer size: 131 Bytes
  • Size of remote file: 158 kB
public/media/sent22.jpg ADDED

Git LFS Details

  • SHA256: a924477da6c0774c1d6aea14c7888421f8ffca293d9d56150fce55c37922ecba
  • Pointer size: 131 Bytes
  • Size of remote file: 274 kB
public/media/sent23.jpg ADDED

Git LFS Details

  • SHA256: dcc4e27bf36965f8c12e5372f039b0469297677e97d8eda06628180211bebdf5
  • Pointer size: 131 Bytes
  • Size of remote file: 185 kB
public/media/sent24.jpg ADDED

Git LFS Details

  • SHA256: 674a905fb2860e18540321619bc20b0865f872039c18125ce17f52f00edb7295
  • Pointer size: 130 Bytes
  • Size of remote file: 91.4 kB
public/media/sent25.jpg ADDED

Git LFS Details

  • SHA256: b89ce9519fa64d2de7b4f00dac0fbff40f794c190767abbed3b2e918cfc7b60c
  • Pointer size: 131 Bytes
  • Size of remote file: 231 kB
public/media/sent26.jpg ADDED

Git LFS Details

  • SHA256: d1b4e3f85bcd88ce1b44d840fa102be4f1abe0fcf6ec7c4a3d4bd89a488ff744
  • Pointer size: 131 Bytes
  • Size of remote file: 125 kB
public/media/sent27.jpg ADDED

Git LFS Details

  • SHA256: ad22fa84032c8108bdc8e0fd3d7d852ab741b53424eeb5382aa0cc71a4d38b90
  • Pointer size: 131 Bytes
  • Size of remote file: 210 kB
public/media/sent28.jpg ADDED

Git LFS Details

  • SHA256: 6a747bb10d8960a790115d1619774bf2c75821f3cce7ee5876f9cc15d998b248
  • Pointer size: 131 Bytes
  • Size of remote file: 195 kB
public/media/sent29.jpg ADDED

Git LFS Details

  • SHA256: 218af835073b4ab9044bbe0cf7053ee6a564ee5fea177c4df6baa988ff234340
  • Pointer size: 131 Bytes
  • Size of remote file: 153 kB
public/media/sent30.jpg ADDED

Git LFS Details

  • SHA256: 38695e0525a27e810bb3aaf68ece2386255b3c766309c515681eb1cab753229a
  • Pointer size: 131 Bytes
  • Size of remote file: 166 kB
public/media/sent31.jpg ADDED

Git LFS Details

  • SHA256: a44be8af73a4f97a152ae651f853a258bf56089c11e44560667018d140f9882f
  • Pointer size: 131 Bytes
  • Size of remote file: 137 kB
public/media/sent32.jpg ADDED

Git LFS Details

  • SHA256: c9ca1fb97ff24841e5fa0d46fb9b7bbb571a4de866d99f4248904b658a24d28a
  • Pointer size: 131 Bytes
  • Size of remote file: 118 kB
public/media/sent33.jpg ADDED

Git LFS Details

  • SHA256: d28d30759a987526529a3275109746ec54f840372426002ebc0becb6a08e0cdb
  • Pointer size: 131 Bytes
  • Size of remote file: 183 kB
public/media/sent34.jpg ADDED

Git LFS Details

  • SHA256: 8c91f8a4923c49787d8e2aeb07e3828a212b2650d806c67a97f82a30dcd417f9
  • Pointer size: 131 Bytes
  • Size of remote file: 133 kB
public/media/sent35.jpg ADDED

Git LFS Details

  • SHA256: eea0476bbc0a46c6ce4e01185ddca07d00c4f33325ed2b665650f262d7d952f5
  • Pointer size: 131 Bytes
  • Size of remote file: 158 kB
public/media/sent36.jpg ADDED

Git LFS Details

  • SHA256: da720b6e582174f2f6a2c322bdb92962aa8a9fb68637166c9b0bb08ac4dd9a71
  • Pointer size: 131 Bytes
  • Size of remote file: 221 kB