Spaces:
Sleeping
Sleeping
to where game
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +8 -0
- App.css +148 -0
- App.tsx +299 -0
- Dockerfile +21 -10
- README.md +178 -1
- assets/react.svg +1 -0
- eslint.config.js +28 -0
- index.css +68 -0
- index.html +13 -0
- main.tsx +10 -0
- package-lock.json +0 -0
- package.json +30 -0
- public/.DS_Store +0 -0
- public/admin-guide.md +95 -0
- public/favicon.svg +14 -0
- public/media/sent02.jpg +3 -0
- public/media/sent03.jpg +3 -0
- public/media/sent04.jpg +3 -0
- public/media/sent05.jpg +3 -0
- public/media/sent06.jpg +3 -0
- public/media/sent07.jpg +3 -0
- public/media/sent08.jpg +3 -0
- public/media/sent09.jpg +3 -0
- public/media/sent10.jpg +3 -0
- public/media/sent11.jpg +3 -0
- public/media/sent12.jpg +3 -0
- public/media/sent13.jpg +3 -0
- public/media/sent14.jpg +3 -0
- public/media/sent15.jpg +3 -0
- public/media/sent16.jpg +3 -0
- public/media/sent17.jpg +3 -0
- public/media/sent18.jpg +3 -0
- public/media/sent19.jpg +3 -0
- public/media/sent20.jpg +3 -0
- public/media/sent21.jpg +3 -0
- public/media/sent22.jpg +3 -0
- public/media/sent23.jpg +3 -0
- public/media/sent24.jpg +3 -0
- public/media/sent25.jpg +3 -0
- public/media/sent26.jpg +3 -0
- public/media/sent27.jpg +3 -0
- public/media/sent28.jpg +3 -0
- public/media/sent29.jpg +3 -0
- public/media/sent30.jpg +3 -0
- public/media/sent31.jpg +3 -0
- public/media/sent32.jpg +3 -0
- public/media/sent33.jpg +3 -0
- public/media/sent34.jpg +3 -0
- public/media/sent35.jpg +3 -0
- public/media/sent36.jpg +3 -0
.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 |
-
#
|
| 2 |
-
|
| 3 |
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
WORKDIR /app
|
| 11 |
|
| 12 |
-
|
| 13 |
-
RUN
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
public/media/sent03.jpg
ADDED
|
Git LFS Details
|
public/media/sent04.jpg
ADDED
|
Git LFS Details
|
public/media/sent05.jpg
ADDED
|
Git LFS Details
|
public/media/sent06.jpg
ADDED
|
Git LFS Details
|
public/media/sent07.jpg
ADDED
|
Git LFS Details
|
public/media/sent08.jpg
ADDED
|
Git LFS Details
|
public/media/sent09.jpg
ADDED
|
Git LFS Details
|
public/media/sent10.jpg
ADDED
|
Git LFS Details
|
public/media/sent11.jpg
ADDED
|
Git LFS Details
|
public/media/sent12.jpg
ADDED
|
Git LFS Details
|
public/media/sent13.jpg
ADDED
|
Git LFS Details
|
public/media/sent14.jpg
ADDED
|
Git LFS Details
|
public/media/sent15.jpg
ADDED
|
Git LFS Details
|
public/media/sent16.jpg
ADDED
|
Git LFS Details
|
public/media/sent17.jpg
ADDED
|
Git LFS Details
|
public/media/sent18.jpg
ADDED
|
Git LFS Details
|
public/media/sent19.jpg
ADDED
|
Git LFS Details
|
public/media/sent20.jpg
ADDED
|
Git LFS Details
|
public/media/sent21.jpg
ADDED
|
Git LFS Details
|
public/media/sent22.jpg
ADDED
|
Git LFS Details
|
public/media/sent23.jpg
ADDED
|
Git LFS Details
|
public/media/sent24.jpg
ADDED
|
Git LFS Details
|
public/media/sent25.jpg
ADDED
|
Git LFS Details
|
public/media/sent26.jpg
ADDED
|
Git LFS Details
|
public/media/sent27.jpg
ADDED
|
Git LFS Details
|
public/media/sent28.jpg
ADDED
|
Git LFS Details
|
public/media/sent29.jpg
ADDED
|
Git LFS Details
|
public/media/sent30.jpg
ADDED
|
Git LFS Details
|
public/media/sent31.jpg
ADDED
|
Git LFS Details
|
public/media/sent32.jpg
ADDED
|
Git LFS Details
|
public/media/sent33.jpg
ADDED
|
Git LFS Details
|
public/media/sent34.jpg
ADDED
|
Git LFS Details
|
public/media/sent35.jpg
ADDED
|
Git LFS Details
|
public/media/sent36.jpg
ADDED
|
Git LFS Details
|