Tech Stack
The frontend is built with modern web technologies:
React 19.2.0
Latest React with concurrent features and improved hooks
Vite 7.3.1
Ultra-fast build tool with HMR (Hot Module Replacement)
Axios 1.13.5
HTTP client for API communication
Framer Motion 12.34.3
Animation library for smooth UI transitions
Project Structure
source/Front/Crafter_League_of_Legends/
├── src/
│ ├── App.jsx # Main application component
│ ├── main.jsx # Application entry point
│ ├── components/ # Reusable UI components
│ ├── services/ # API and business logic
│ ├── hooks/ # Custom React hooks
│ └── constants/ # Configuration and theme
├── package.json # Dependencies and scripts
├── vite.config.js # Vite configuration
└── index.html # HTML entry point
Core Components
App.jsx - Main Application
The root component orchestrates game state and logic:
import { useCallback, useEffect, useState, useMemo } from 'react'
import { storageService } from '../services/storageService';
import { GAME_CONFIG } from '../constants/theme';
import { gameService } from '../services/gameService';
function App() {
// Core game state
const [gameData, setGameData] = useState(null);
const [selectedItems, setSelectedItems] = useState([]);
const [score, setScore] = useState(storageService.getScore());
const [timeLeft, setTimeLeft] = useState(GAME_CONFIG.timePerQuestion);
const [feedback, setFeedback] = useState(null);
const [isLoading, setIsLoading] = useState(true);
// Dynamic slot calculation based on item components
const requiredSlots = useMemo(() => {
return gameData?.correctComponents?.length || 2;
}, [gameData?.correctComponents?.length]);
// Load new question from API
const loadNewQuestion = useCallback(async () => {
try {
setIsLoading(true);
const data = await gameService.getRandomItem();
setGameData(data);
} catch (err) {
console.error('Error loading question:', err);
} finally {
setIsLoading(false);
}
}, []);
// ... game logic
}
The component uses React hooks extensively for state management and side effects.
Key Features
Uses React’s built-in useState and useCallback hooks:
- gameData: Current question and options from backend
- selectedItems: Items chosen by player
- score: Current score (persisted to localStorage)
- timeLeft: Countdown timer
- feedback: Answer validation result
Automatic timer pause when slots are full:useEffect(() => {
if (!gameData || feedback || timeLeft <= 0) return;
if (selectedItems.length >= requiredSlots) return; // PAUSE
const timer = setInterval(() => {
setTimeLeft((prev) => prev > 0 ? prev - 1 : 0);
}, 1000);
return () => clearInterval(timer);
}, [gameData, feedback, timeLeft, selectedItems.length, requiredSlots]);
Allows duplicates and manages slot capacity:const handleItemClick = useCallback((item) => {
if (feedback) return;
setSelectedItems((prev) => {
const isInSlots = prev.some((s) => s.id === item.id);
if (prev.length < requiredSlots) {
// Add item if slots available
return [...prev, item];
} else if (isInSlots) {
// Remove last occurrence if slots full
const lastIdx = prev.map((s) => s.id).lastIndexOf(item.id);
return prev.filter((_, i) => i !== lastIdx);
}
return prev;
});
}, [feedback, requiredSlots]);
Submits answer and handles scoring:const handleSubmit = useCallback(async () => {
if (selectedItems.length !== requiredSlots) return;
const selectedIds = selectedItems.map((item) => item.id);
const result = await gameService.validateAnswer(
gameData.targetItem.id,
selectedIds
);
if (result.isCorrect) {
const newScore = score + GAME_CONFIG.pointsPerCorrect;
setScore(newScore);
storageService.setScore(newScore);
} else {
const newScore = Math.max(0, score - GAME_CONFIG.pointsPerIncorrect);
setScore(newScore);
}
}, [selectedItems, gameData, score]);
Services Layer
Game Service
Handles all game-related API calls using Axios:
import axios from 'axios';
import { API_CONFIG } from '../constants/theme';
class GameService {
constructor() {
this.api = axios.create({
baseURL: API_CONFIG.baseURL,
timeout: 10000,
});
}
async getRandomItem() {
const response = await this.api.get(API_CONFIG.endpoints.question);
return response.data;
}
async validateAnswer(targetId, selectedIds) {
const response = await this.api.post(API_CONFIG.endpoints.validate, {
targetItemId: targetId,
selectedComponentIds: selectedIds,
});
return response.data;
}
}
export const gameService = new GameService();
Storage Service
Manages localStorage for persistent data:
class StorageService {
getScore() {
return parseInt(localStorage.getItem('score') || '0', 10);
}
setScore(score) {
localStorage.setItem('score', score.toString());
}
getBestScore() {
return parseInt(localStorage.getItem('bestScore') || '0', 10);
}
setBestScore(score) {
localStorage.setItem('bestScore', score.toString());
}
incrementStreak() {
const streak = this.getStreak() + 1;
localStorage.setItem('streak', streak.toString());
}
resetStreak() {
localStorage.setItem('streak', '0');
}
}
export const storageService = new StorageService();
Configuration System
Theme Constants
Located at constants/theme.js:
// Color palette
export const COLORS = {
background: '#0A1428',
accentGold: '#C89B3C',
magicBlue: '#0BC6E3',
progressGreen: '#00BFA5',
panelDark: '#1A2332',
textMain: '#F0E6D2',
itemBorder: '#463714',
successGreen: '#00FF88',
errorRed: '#FF4444',
};
// Game mechanics
export const GAME_CONFIG = {
timePerQuestion: 15, // seconds
pointsPerCorrect: 100,
pointsPerIncorrect: 50,
itemsInCircle: 14,
circleRadius: 270,
centralItemSize: 120,
peripheralItemSize: 64,
maxSlots: 2,
};
// API endpoints
export const API_CONFIG = {
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:5000/api',
endpoints: {
question: '/game/question',
validate: '/game/validate',
},
};
Vite Configuration
Minimal configuration in vite.config.js:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
}
})
react-swc plugin provides faster builds using SWC (Speedy Web Compiler) instead of Babel.
Build Scripts
Defined in package.json:
{
"scripts": {
"dev": "vite", // Start dev server
"build": "vite build", // Production build
"lint": "eslint .", // Lint code
"preview": "vite preview" // Preview production build
}
}
Development
Starts Vite dev server with:
- Hot Module Replacement (HMR)
- Fast refresh for React components
- Runs on
http://localhost:5173
Production Build
Creates optimized production bundle:
- Minified JavaScript and CSS
- Code splitting
- Tree shaking
- Output to
dist/ directory
Styling Approach
The application uses inline styles with JavaScript objects for simplicity and co-location.
Example styling pattern:
const styles = {
app: {
height: '100vh',
width: '100vw',
display: 'flex',
flexDirection: 'column',
background: 'linear-gradient(135deg, #0A1428 0%, #1A2332 100%)',
},
loadingContainer: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: COLORS.background,
},
};
return <div style={styles.app}>...</div>;
React.memo and useCallback
Memoization prevents unnecessary re-renders:const handleItemClick = useCallback((item) => {
// Logic here
}, [feedback, requiredSlots]);
useMemo for Computed Values
Avoid recalculating on every render:const requiredSlots = useMemo(() => {
return gameData?.correctComponents?.length || 2;
}, [gameData?.correctComponents?.length]);
Vite automatically splits code by route/component for optimal loading.
Images from Data Dragon CDN are cached by the browser and backend.
Next Steps
Backend Architecture
Understand how the Spring Boot backend works
Data Dragon Integration
Learn about API integration and caching