import { useState, useEffect, useRef } from 'react' import './App.css' // Question interface interface Question { file: string; who: string; doing: string; what: string; to_where: string; distractors: string[]; } // State for one round interface GameState { frames: string[]; frameIndex: number; who: string; doing: string; what: string; answer: string; choices: string[]; } function App() { const [questions, setQuestions] = useState([]); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [gameState, setGameState] = useState(null); const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight }); const [feedbackMsg, setFeedbackMsg] = useState(''); const [isAnimating, setIsAnimating] = useState(false); const [animationElement, setAnimationElement] = useState<{ content: string, position: { x: number, y: number } } | null>(null); const [isFilled, setIsFilled] = useState(false); const wrongMessages = ["Wrong, try again", "Not correct, try again", "Error, come again"]; const animationRef = useRef(null); const destinationRef = useRef(null); // Colors const ORANGE = "#FF8C00"; const YELLOW = "#F0E61E"; const GREEN = "#00B900"; const _UNUSED_PINK_ORIGINAL = "#FF69B4"; // Keeping original for context, effectively replaced const _UNUSED_BLUE_ORIGINAL = "#007BFF"; // Keeping original for context, effectively replaced const LIGHT_BLUE = "#87CEEB"; // New light blue color const PINK = LIGHT_BLUE; // PINK is now LIGHT_BLUE for animation const BLUE = LIGHT_BLUE; // BLUE is now LIGHT_BLUE for the target box // Load questions on mount useEffect(() => { const loadQuestions = async () => { try { const response = await fetch('/to-where.json'); if (!response.ok) { throw new Error('Failed to load questions'); } const data: Question[] = await response.json(); // Shuffle questions const shuffled = [...data].sort(() => Math.random() - 0.5); setQuestions(shuffled); } catch (error) { console.error('Error loading questions:', error); } }; loadQuestions(); // Set up window resize handler const handleResize = () => { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // Prepare game state when questions load or change useEffect(() => { if (questions.length > 0) { prepareRound(questions[currentQuestionIndex]); } }, [questions, currentQuestionIndex]); // Handle video/image frame updates useEffect(() => { if (!gameState || !gameState.frames.length) return; const frameInterval = setInterval(() => { if (!isAnimating && !isFilled) { setGameState(prev => { if (!prev) return prev; const nextIndex = (prev.frameIndex + 1) % prev.frames.length; return { ...prev, frameIndex: nextIndex }; }); } }, 100); // Adjust frame rate as needed return () => clearInterval(frameInterval); }, [gameState, isAnimating, isFilled]); // Process animation completion and next question timing useEffect(() => { if (isFilled) { const timer = setTimeout(() => { // Move to next question setCurrentQuestionIndex(prev => (prev + 1) % questions.length); setIsFilled(false); setFeedbackMsg(''); setAnimationElement(null); }, 1000); return () => clearTimeout(timer); } }, [isFilled, questions.length]); // Animation effect useEffect(() => { if (isAnimating && animationElement && animationRef.current && destinationRef.current) { const destRect = destinationRef.current.getBoundingClientRect(); const targetPosition = { x: destRect.left + destRect.width / 2, y: destRect.top + destRect.height / 2 }; const anim = animationRef.current; anim.style.transition = 'transform 500ms linear'; anim.style.transform = `translate(${targetPosition.x - animationElement.position.x}px, ${targetPosition.y - animationElement.position.y}px)`; const handleAnimationEnd = () => { setIsAnimating(false); setIsFilled(true); }; anim.addEventListener('transitionend', handleAnimationEnd); return () => anim.removeEventListener('transitionend', handleAnimationEnd); } }, [isAnimating, animationElement]); // Prepare a new game round const prepareRound = (question: Question) => { // Shuffle the distractors first, then take two const shuffledDistractors = [...question.distractors].sort(() => Math.random() - 0.5); const selectedDistractors = shuffledDistractors.slice(0, 2); const choices = [question.to_where, ...selectedDistractors]; // Shuffle the final 3 choices const shuffledChoices = [...choices].sort(() => Math.random() - 0.5); // Determine if media is image or video const isVideo = question.file.endsWith('.mp4'); const frames = isVideo ? [question.file] // For simplicity, we'll just use the path for videos : [question.file]; // Same for images setGameState({ frames, frameIndex: 0, who: question.who, doing: question.doing, what: question.what, answer: question.to_where, choices: shuffledChoices }); setFeedbackMsg(''); setIsAnimating(false); setAnimationElement(null); setIsFilled(false); }; // Handle option selection const handleOptionClick = (option: string, event: React.MouseEvent) => { if (isAnimating || isFilled || !gameState) return; const rect = event.currentTarget.getBoundingClientRect(); const position = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; if (option === gameState.answer) { // Correct answer setAnimationElement({ content: option, position }); setIsAnimating(true); setFeedbackMsg(''); } else { // Wrong answer setFeedbackMsg(wrongMessages[Math.floor(Math.random() * wrongMessages.length)]); } }; // Render current media (image or video) const renderMedia = () => { if (!gameState || !gameState.frames.length) return null; const currentFrame = gameState.frames[gameState.frameIndex]; const isVideo = currentFrame.endsWith('.mp4'); if (isVideo) { return (