clementpep commited on
Commit
0344b6d
·
1 Parent(s): 8beb1aa

feat: add react + vite components

Browse files
.gitignore CHANGED
@@ -23,6 +23,10 @@ wheels/
23
  .installed.cfg
24
  *.egg
25
 
 
 
 
 
26
  # Environment
27
  .env
28
  .venv
 
23
  .installed.cfg
24
  *.egg
25
 
26
+ # Node
27
+ node_modules/
28
+ package-lock.json
29
+
30
  # Environment
31
  .env
32
  .venv
Dockerfile CHANGED
@@ -1,27 +1,27 @@
1
- # Use Python 3.11 slim image for smaller size
2
- FROM python:3.11-slim
 
 
 
 
 
3
 
4
- # Set working directory
 
5
  WORKDIR /app
6
 
7
- # Set environment variables
8
- ENV PYTHONUNBUFFERED=1 \
9
- PYTHONDONTWRITEBYTECODE=1 \
10
- PORT=7860
11
-
12
- # Install dependencies
13
- COPY requirements.txt .
14
  RUN pip install --no-cache-dir -r requirements.txt
15
 
16
- # Copy application files
17
- COPY . .
18
 
19
- # Expose port
20
- EXPOSE 7860
21
 
22
- # Health check
23
- HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
24
- CMD python -c "import requests; requests.get('http://localhost:7860/')"
25
 
26
- # Run the application
27
- CMD ["python", "app.py"]
 
1
+ # Stage 1: Build frontend
2
+ FROM node:18-alpine AS frontend-build
3
+ WORKDIR /app/frontend
4
+ COPY frontend/package*.json ./
5
+ RUN npm install
6
+ COPY frontend/ ./
7
+ RUN npm run build
8
 
9
+ # Stage 2: Python backend
10
+ FROM python:3.11-slim
11
  WORKDIR /app
12
 
13
+ # Install backend deps
14
+ COPY backend/requirements.txt ./
 
 
 
 
 
15
  RUN pip install --no-cache-dir -r requirements.txt
16
 
17
+ # Copy backend
18
+ COPY backend/ ./backend/
19
 
20
+ # Copy built frontend
21
+ COPY --from=frontend-build /app/frontend/dist ./frontend/dist
22
 
23
+ # Expose port for Hugging Face
24
+ EXPOSE 7860
 
25
 
26
+ # Start server
27
+ CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
SETUP_REACT.md DELETED
@@ -1,252 +0,0 @@
1
- # 🚀 Setup React + Docker - Cluedo Custom v3
2
-
3
- ## ✅ Ce qui est fait
4
-
5
- ### Backend
6
- - ✅ `backend/main.py` - API FastAPI complète
7
- - ✅ `backend/defaults.py` - 3 thèmes prédéfinis (classic, office, fantasy)
8
- - ✅ `backend/models.py`, `game_engine.py`, `game_manager.py` - Logique métier
9
- - ✅ `backend/requirements.txt` - Dépendances Python
10
-
11
- ### Frontend (structure)
12
- - ✅ `frontend/package.json` - Config React + Vite + Tailwind
13
- - ✅ `frontend/vite.config.js` - Config Vite avec proxy API
14
- - ✅ `frontend/tailwind.config.js` - Theme sombre
15
- - ✅ `frontend/src/api.js` - Client API
16
-
17
- ## 📋 Ce qu'il reste à faire
18
-
19
- ### 1. Créer les composants React
20
-
21
- Créer ces fichiers dans `frontend/src/` :
22
-
23
- #### `main.jsx`
24
- ```jsx
25
- import React from 'react'
26
- import ReactDOM from 'react-dom/client'
27
- import App from './App.jsx'
28
- import './index.css'
29
-
30
- ReactDOM.createRoot(document.getElementById('root')).render(
31
- <React.StrictMode>
32
- <App />
33
- </React.StrictMode>,
34
- )
35
- ```
36
-
37
- #### `App.jsx`
38
- ```jsx
39
- import { BrowserRouter, Routes, Route } from 'react-router-dom'
40
- import Home from './pages/Home'
41
- import Join from './pages/Join'
42
- import Game from './pages/Game'
43
-
44
- function App() {
45
- return (
46
- <BrowserRouter>
47
- <div className="min-h-screen bg-gradient-to-br from-dark-950 via-dark-900 to-dark-950">
48
- <Routes>
49
- <Route path="/" element={<Home />} />
50
- <Route path="/join" element={<Join />} />
51
- <Route path="/game/:gameId/:playerId" element={<Game />} />
52
- </Routes>
53
- </div>
54
- </BrowserRouter>
55
- )
56
- }
57
-
58
- export default App
59
- ```
60
-
61
- #### `pages/Home.jsx` - Page d'accueil
62
- - Bouton "Créer une partie" (thème classic par défaut)
63
- - Input nom du joueur
64
- - Lien vers "Rejoindre une partie"
65
-
66
- #### `pages/Join.jsx` - Rejoindre une partie
67
- - Input code de partie (4 caractères)
68
- - Input nom du joueur
69
- - Bouton rejoindre
70
-
71
- #### `pages/Game.jsx` - Interface de jeu
72
- - Plateau avec positions
73
- - Cartes du joueur
74
- - Actions (Dés, Suggestion, Accusation, Passer)
75
- - Historique
76
- - Bouton "Démarrer" (si créateur et 3+ joueurs)
77
-
78
- ### 2. Créer le Dockerfile
79
-
80
- ```dockerfile
81
- # Stage 1: Build frontend
82
- FROM node:18-alpine AS frontend-build
83
- WORKDIR /app/frontend
84
- COPY frontend/package*.json ./
85
- RUN npm install
86
- COPY frontend/ ./
87
- RUN npm run build
88
-
89
- # Stage 2: Python backend
90
- FROM python:3.11-slim
91
- WORKDIR /app
92
-
93
- # Install backend deps
94
- COPY backend/requirements.txt ./
95
- RUN pip install --no-cache-dir -r requirements.txt
96
-
97
- # Copy backend
98
- COPY backend/ ./backend/
99
-
100
- # Copy built frontend
101
- COPY --from=frontend-build /app/frontend/dist ./frontend/dist
102
-
103
- # Expose port for Hugging Face
104
- EXPOSE 7860
105
-
106
- # Start server
107
- CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
108
- ```
109
-
110
- ### 3. Créer docker-compose.yml (pour test local)
111
-
112
- ```yaml
113
- version: '3.8'
114
-
115
- services:
116
- backend:
117
- build: .
118
- ports:
119
- - "7860:7860"
120
- volumes:
121
- - ./backend:/app/backend
122
- - ./frontend/dist:/app/frontend/dist
123
- environment:
124
- - PYTHONUNBUFFERED=1
125
- ```
126
-
127
- ### 4. Créer README.md pour Hugging Face
128
-
129
- ```yaml
130
- ---
131
- title: Cluedo Custom
132
- emoji: 🔍
133
- colorFrom: red
134
- colorTo: purple
135
- sdk: docker
136
- pinned: false
137
- app_port: 7860
138
- ---
139
-
140
- # 🔍 Cluedo Custom
141
-
142
- Créez votre propre jeu de Cluedo avec des suspects, armes et lieux personnalisés !
143
-
144
- ## 🎮 Comment jouer
145
-
146
- 1. **Créer une partie** : Choisissez un thème (Manoir classique, Bureau, Château)
147
- 2. **Partager le code** : Envoyez le code à 4 caractères à vos amis
148
- 3. **Jouer** : Lancez les dés, déplacez-vous, faites des suggestions !
149
-
150
- ## 🛠️ Développement local
151
-
152
- \`\`\`bash
153
- # Build et lancer avec Docker
154
- docker-compose up --build
155
-
156
- # Ou séparément :
157
- # Backend
158
- cd backend && pip install -r requirements.txt && uvicorn main:app --port 8000
159
-
160
- # Frontend
161
- cd frontend && npm install && npm run dev
162
- \`\`\`
163
- ```
164
-
165
- ### 5. Commandes pour tester
166
-
167
- ```bash
168
- # 1. Installer frontend
169
- cd frontend
170
- npm install
171
-
172
- # 2. Builder frontend
173
- npm run build
174
-
175
- # 3. Tester backend seul (sert le frontend builded)
176
- cd ../backend
177
- pip install -r requirements.txt
178
- uvicorn main:app --port 7860
179
-
180
- # Ouvrir http://localhost:7860
181
-
182
- # 4. Build Docker
183
- cd ..
184
- docker build -t cluedo-custom .
185
- docker run -p 7860:7860 cluedo-custom
186
-
187
- # 5. Deploy sur Hugging Face
188
- # - Créer un nouveau Space (SDK: Docker)
189
- # - Push le code
190
- # - Hugging Face va build automatiquement
191
- ```
192
-
193
- ## 🎨 Templates de composants React simplifiés
194
-
195
- Les composants doivent être **ultra-simples** :
196
-
197
- ### Home.jsx (minimal)
198
- - Titre "Cluedo Custom"
199
- - Input "Votre nom"
200
- - Bouton "Créer une partie"
201
- - Lien "Rejoindre une partie existante"
202
-
203
- ### Game.jsx (minimal)
204
- - Section "Plateau" : Liste des pièces avec icônes joueurs
205
- - Section "Mes cartes" : Liste simple
206
- - Section "Actions" :
207
- - Si mon tour : Bouton "Lancer dés" OU "Passer"
208
- - Si déplacé : Selects (suspect/arme/pièce) + "Suggérer" / "Accuser"
209
- - Section "Historique" : 5 dernières actions
210
-
211
- ## ⚡ Quick Start (après avoir créé les composants)
212
-
213
- ```bash
214
- # Terminal 1 - Backend
215
- cd backend
216
- pip install -r requirements.txt
217
- uvicorn main:app --reload --port 8000
218
-
219
- # Terminal 2 - Frontend (dev mode)
220
- cd frontend
221
- npm install
222
- npm run dev
223
- # Ouvre http://localhost:3000
224
-
225
- # Pour production (single server)
226
- cd frontend
227
- npm run build
228
- cd ../backend
229
- uvicorn main:app --port 7860
230
- # Ouvre http://localhost:7860
231
- ```
232
-
233
- ## 🐳 Déployer sur Hugging Face
234
-
235
- 1. Créer un Space : https://huggingface.co/new-space
236
- 2. Choisir SDK: **Docker**
237
- 3. Cloner le repo du Space
238
- 4. Copier tous les fichiers dedans
239
- 5. Ajouter `README.md` avec le header YAML
240
- 6. Push:
241
- ```bash
242
- git add .
243
- git commit -m "Initial commit"
244
- git push
245
- ```
246
-
247
- 7. Attendre le build (~5-10 min)
248
- 8. Votre jeu est en ligne ! 🎉
249
-
250
- ---
251
-
252
- **Note** : Les composants React sont volontairement simplifiés pour aller vite. Tu pourras les améliorer après le déploiement.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docker-compose.yml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ app:
5
+ build: .
6
+ ports:
7
+ - "7860:7860"
8
+ environment:
9
+ - PYTHONUNBUFFERED=1
10
+ volumes:
11
+ - ./backend:/app/backend
12
+ - ./frontend/dist:/app/frontend/dist
frontend/src/App.jsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BrowserRouter, Routes, Route } from 'react-router-dom'
2
+ import Home from './pages/Home'
3
+ import Join from './pages/Join'
4
+ import Game from './pages/Game'
5
+
6
+ function App() {
7
+ return (
8
+ <BrowserRouter>
9
+ <div className="min-h-screen bg-gradient-to-br from-dark-950 via-dark-900 to-dark-950">
10
+ <Routes>
11
+ <Route path="/" element={<Home />} />
12
+ <Route path="/join" element={<Join />} />
13
+ <Route path="/game/:gameId/:playerId" element={<Game />} />
14
+ </Routes>
15
+ </div>
16
+ </BrowserRouter>
17
+ )
18
+ }
19
+
20
+ export default App
frontend/src/api.js CHANGED
@@ -9,6 +9,57 @@ const api = axios.create({
9
  },
10
  });
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  export const gameAPI = {
13
  // Get available themes
14
  getThemes: () => api.get('/themes'),
 
9
  },
10
  });
11
 
12
+ // Helper functions for easier usage
13
+ export const createQuickGame = async (playerName, theme = 'classic') => {
14
+ const response = await api.post('/games/quick-create', { theme, player_name: playerName });
15
+ return response.data;
16
+ };
17
+
18
+ export const joinGame = async (gameId, playerName) => {
19
+ const response = await api.post('/games/join', { game_id: gameId, player_name: playerName });
20
+ return response.data;
21
+ };
22
+
23
+ export const startGame = async (gameId, playerId) => {
24
+ const response = await api.post(`/games/${gameId}/start`, { player_id: playerId });
25
+ return response.data;
26
+ };
27
+
28
+ export const getGameState = async (gameId, playerId) => {
29
+ const response = await api.get(`/games/${gameId}/state/${playerId}`);
30
+ return response.data;
31
+ };
32
+
33
+ export const rollDice = async (gameId, playerId) => {
34
+ const response = await api.post(`/games/${gameId}/roll`, { player_id: playerId });
35
+ return response.data;
36
+ };
37
+
38
+ export const makeSuggestion = async (gameId, playerId, suspect, weapon, room) => {
39
+ const response = await api.post(`/games/${gameId}/suggest`, {
40
+ player_id: playerId,
41
+ suspect,
42
+ weapon,
43
+ room,
44
+ });
45
+ return response.data;
46
+ };
47
+
48
+ export const makeAccusation = async (gameId, playerId, suspect, weapon, room) => {
49
+ const response = await api.post(`/games/${gameId}/accuse`, {
50
+ player_id: playerId,
51
+ suspect,
52
+ weapon,
53
+ room,
54
+ });
55
+ return response.data;
56
+ };
57
+
58
+ export const passTurn = async (gameId, playerId) => {
59
+ const response = await api.post(`/games/${gameId}/pass`, { player_id: playerId });
60
+ return response.data;
61
+ };
62
+
63
  export const gameAPI = {
64
  // Get available themes
65
  getThemes: () => api.get('/themes'),
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.jsx'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
frontend/src/pages/Game.jsx ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+ import { useParams } from 'react-router-dom'
3
+ import { getGameState, startGame, rollDice, makeSuggestion, makeAccusation, passTurn } from '../api'
4
+
5
+ function Game() {
6
+ const { gameId, playerId } = useParams()
7
+ const [gameState, setGameState] = useState(null)
8
+ const [loading, setLoading] = useState(true)
9
+ const [actionLoading, setActionLoading] = useState(false)
10
+
11
+ // Suggestion form
12
+ const [selectedSuspect, setSelectedSuspect] = useState('')
13
+ const [selectedWeapon, setSelectedWeapon] = useState('')
14
+ const [selectedRoom, setSelectedRoom] = useState('')
15
+
16
+ useEffect(() => {
17
+ loadGameState()
18
+ const interval = setInterval(loadGameState, 2000)
19
+ return () => clearInterval(interval)
20
+ }, [gameId, playerId])
21
+
22
+ const loadGameState = async () => {
23
+ try {
24
+ const state = await getGameState(gameId, playerId)
25
+ setGameState(state)
26
+ setLoading(false)
27
+ } catch (error) {
28
+ console.error('Erreur chargement:', error)
29
+ }
30
+ }
31
+
32
+ const handleStartGame = async () => {
33
+ setActionLoading(true)
34
+ try {
35
+ await startGame(gameId, playerId)
36
+ await loadGameState()
37
+ } catch (error) {
38
+ alert(error.response?.data?.detail || 'Erreur au démarrage')
39
+ } finally {
40
+ setActionLoading(false)
41
+ }
42
+ }
43
+
44
+ const handleRollDice = async () => {
45
+ setActionLoading(true)
46
+ try {
47
+ await rollDice(gameId, playerId)
48
+ await loadGameState()
49
+ } catch (error) {
50
+ alert(error.response?.data?.detail || 'Erreur lors du lancer de dés')
51
+ } finally {
52
+ setActionLoading(false)
53
+ }
54
+ }
55
+
56
+ const handleSuggestion = async () => {
57
+ if (!selectedSuspect || !selectedWeapon || !selectedRoom) {
58
+ alert('Sélectionnez tous les éléments')
59
+ return
60
+ }
61
+ setActionLoading(true)
62
+ try {
63
+ await makeSuggestion(gameId, playerId, selectedSuspect, selectedWeapon, selectedRoom)
64
+ await loadGameState()
65
+ setSelectedSuspect('')
66
+ setSelectedWeapon('')
67
+ setSelectedRoom('')
68
+ } catch (error) {
69
+ alert(error.response?.data?.detail || 'Erreur lors de la suggestion')
70
+ } finally {
71
+ setActionLoading(false)
72
+ }
73
+ }
74
+
75
+ const handleAccusation = async () => {
76
+ if (!selectedSuspect || !selectedWeapon || !selectedRoom) {
77
+ alert('Sélectionnez tous les éléments')
78
+ return
79
+ }
80
+ if (!confirm('⚠️ Une accusation incorrecte vous élimine. Continuer ?')) return
81
+
82
+ setActionLoading(true)
83
+ try {
84
+ await makeAccusation(gameId, playerId, selectedSuspect, selectedWeapon, selectedRoom)
85
+ await loadGameState()
86
+ setSelectedSuspect('')
87
+ setSelectedWeapon('')
88
+ setSelectedRoom('')
89
+ } catch (error) {
90
+ alert(error.response?.data?.detail || 'Erreur lors de l\'accusation')
91
+ } finally {
92
+ setActionLoading(false)
93
+ }
94
+ }
95
+
96
+ const handlePassTurn = async () => {
97
+ setActionLoading(true)
98
+ try {
99
+ await passTurn(gameId, playerId)
100
+ await loadGameState()
101
+ } catch (error) {
102
+ alert(error.response?.data?.detail || 'Erreur')
103
+ } finally {
104
+ setActionLoading(false)
105
+ }
106
+ }
107
+
108
+ if (loading) {
109
+ return (
110
+ <div className="min-h-screen flex items-center justify-center">
111
+ <div className="text-accent-400 text-xl">Chargement...</div>
112
+ </div>
113
+ )
114
+ }
115
+
116
+ const me = gameState.players.find(p => p.id === playerId)
117
+ const isMyTurn = gameState.current_player_id === playerId
118
+ const canStart = me?.is_creator && gameState.status === 'waiting' && gameState.players.length >= 3
119
+
120
+ return (
121
+ <div className="min-h-screen p-4">
122
+ <div className="max-w-6xl mx-auto space-y-6">
123
+ {/* Header */}
124
+ <div className="bg-dark-800 p-6 rounded-lg border border-dark-700">
125
+ <div className="flex justify-between items-center">
126
+ <div>
127
+ <h1 className="text-3xl font-bold text-accent-400">🔍 Partie {gameState.game_code}</h1>
128
+ <p className="text-dark-300">Joueur: {me?.name}</p>
129
+ </div>
130
+ <div className="text-right">
131
+ <p className="text-dark-300">Status: {gameState.status === 'waiting' ? '⏳ En attente' : gameState.status === 'playing' ? '🎮 En cours' : '🏆 Terminée'}</p>
132
+ <p className="text-dark-400 text-sm">{gameState.players.length} joueurs</p>
133
+ </div>
134
+ </div>
135
+ {canStart && (
136
+ <button
137
+ onClick={handleStartGame}
138
+ disabled={actionLoading}
139
+ className="mt-4 px-6 py-2 bg-green-600 hover:bg-green-700 disabled:bg-dark-600 text-white font-semibold rounded-lg"
140
+ >
141
+ 🚀 Démarrer la partie
142
+ </button>
143
+ )}
144
+ </div>
145
+
146
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
147
+ {/* Plateau */}
148
+ <div className="bg-dark-800 p-6 rounded-lg border border-dark-700">
149
+ <h2 className="text-xl font-bold text-accent-400 mb-4">🏰 Plateau</h2>
150
+ <div className="space-y-2">
151
+ {gameState.board?.rooms.map((room, idx) => (
152
+ <div key={idx} className="bg-dark-700 p-3 rounded">
153
+ <div className="font-semibold text-white">{room.name}</div>
154
+ <div className="text-sm text-dark-300">
155
+ Joueurs: {room.player_ids.map(id =>
156
+ gameState.players.find(p => p.id === id)?.name || id
157
+ ).join(', ') || 'Aucun'}
158
+ </div>
159
+ </div>
160
+ ))}
161
+ </div>
162
+ </div>
163
+
164
+ {/* Mes cartes */}
165
+ <div className="bg-dark-800 p-6 rounded-lg border border-dark-700">
166
+ <h2 className="text-xl font-bold text-accent-400 mb-4">🃏 Mes cartes</h2>
167
+ <div className="space-y-2">
168
+ {me?.cards.map((card, idx) => (
169
+ <div key={idx} className="bg-dark-700 px-4 py-2 rounded text-white">
170
+ {card.type === 'suspect' && '👤 '}
171
+ {card.type === 'weapon' && '🔪 '}
172
+ {card.type === 'room' && '🏠 '}
173
+ {card.name}
174
+ </div>
175
+ ))}
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+ {/* Actions */}
181
+ {gameState.status === 'playing' && (
182
+ <div className="bg-dark-800 p-6 rounded-lg border border-dark-700">
183
+ <h2 className="text-xl font-bold text-accent-400 mb-4">
184
+ {isMyTurn ? '⚡ À votre tour !' : '⏳ Tour de ' + gameState.players.find(p => p.id === gameState.current_player_id)?.name}
185
+ </h2>
186
+
187
+ {isMyTurn && (
188
+ <div className="space-y-4">
189
+ {!me.has_moved ? (
190
+ <div className="flex gap-4">
191
+ <button
192
+ onClick={handleRollDice}
193
+ disabled={actionLoading}
194
+ className="px-6 py-3 bg-accent-600 hover:bg-accent-700 disabled:bg-dark-600 text-white font-semibold rounded-lg"
195
+ >
196
+ 🎲 Lancer les dés
197
+ </button>
198
+ <button
199
+ onClick={handlePassTurn}
200
+ disabled={actionLoading}
201
+ className="px-6 py-3 bg-dark-600 hover:bg-dark-500 text-white font-semibold rounded-lg"
202
+ >
203
+ ⏭️ Passer
204
+ </button>
205
+ </div>
206
+ ) : (
207
+ <div className="space-y-4">
208
+ <div className="grid grid-cols-3 gap-4">
209
+ <div>
210
+ <label className="block text-sm text-dark-300 mb-2">Suspect</label>
211
+ <select
212
+ value={selectedSuspect}
213
+ onChange={(e) => setSelectedSuspect(e.target.value)}
214
+ className="w-full px-3 py-2 bg-dark-700 text-white rounded border border-dark-600"
215
+ >
216
+ <option value="">--</option>
217
+ {gameState.board?.suspects.map((s, i) => (
218
+ <option key={i} value={s}>{s}</option>
219
+ ))}
220
+ </select>
221
+ </div>
222
+ <div>
223
+ <label className="block text-sm text-dark-300 mb-2">Arme</label>
224
+ <select
225
+ value={selectedWeapon}
226
+ onChange={(e) => setSelectedWeapon(e.target.value)}
227
+ className="w-full px-3 py-2 bg-dark-700 text-white rounded border border-dark-600"
228
+ >
229
+ <option value="">--</option>
230
+ {gameState.board?.weapons.map((w, i) => (
231
+ <option key={i} value={w}>{w}</option>
232
+ ))}
233
+ </select>
234
+ </div>
235
+ <div>
236
+ <label className="block text-sm text-dark-300 mb-2">Pièce</label>
237
+ <select
238
+ value={selectedRoom}
239
+ onChange={(e) => setSelectedRoom(e.target.value)}
240
+ className="w-full px-3 py-2 bg-dark-700 text-white rounded border border-dark-600"
241
+ >
242
+ <option value="">--</option>
243
+ {gameState.board?.rooms.map((r, i) => (
244
+ <option key={i} value={r.name}>{r.name}</option>
245
+ ))}
246
+ </select>
247
+ </div>
248
+ </div>
249
+ <div className="flex gap-4">
250
+ <button
251
+ onClick={handleSuggestion}
252
+ disabled={actionLoading}
253
+ className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-dark-600 text-white font-semibold rounded-lg"
254
+ >
255
+ 💬 Suggérer
256
+ </button>
257
+ <button
258
+ onClick={handleAccusation}
259
+ disabled={actionLoading}
260
+ className="px-6 py-3 bg-red-600 hover:bg-red-700 disabled:bg-dark-600 text-white font-semibold rounded-lg"
261
+ >
262
+ ⚠️ Accuser
263
+ </button>
264
+ <button
265
+ onClick={handlePassTurn}
266
+ disabled={actionLoading}
267
+ className="px-6 py-3 bg-dark-600 hover:bg-dark-500 text-white font-semibold rounded-lg"
268
+ >
269
+ ⏭️ Passer
270
+ </button>
271
+ </div>
272
+ </div>
273
+ )}
274
+ </div>
275
+ )}
276
+ </div>
277
+ )}
278
+
279
+ {/* Historique */}
280
+ <div className="bg-dark-800 p-6 rounded-lg border border-dark-700">
281
+ <h2 className="text-xl font-bold text-accent-400 mb-4">📜 Historique</h2>
282
+ <div className="space-y-2">
283
+ {gameState.history?.slice(-10).reverse().map((event, idx) => (
284
+ <div key={idx} className="text-dark-300 text-sm border-l-2 border-accent-600 pl-3 py-1">
285
+ {event}
286
+ </div>
287
+ ))}
288
+ </div>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ )
293
+ }
294
+
295
+ export default Game
frontend/src/pages/Home.jsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+ import { createQuickGame } from '../api'
4
+
5
+ function Home() {
6
+ const [playerName, setPlayerName] = useState('')
7
+ const [loading, setLoading] = useState(false)
8
+ const navigate = useNavigate()
9
+
10
+ const handleCreateGame = async () => {
11
+ if (!playerName.trim()) {
12
+ alert('Veuillez entrer votre nom')
13
+ return
14
+ }
15
+
16
+ setLoading(true)
17
+ try {
18
+ const response = await createQuickGame(playerName.trim())
19
+ navigate(`/game/${response.game_id}/${response.player_id}`)
20
+ } catch (error) {
21
+ alert('Erreur lors de la création de la partie')
22
+ console.error(error)
23
+ } finally {
24
+ setLoading(false)
25
+ }
26
+ }
27
+
28
+ return (
29
+ <div className="min-h-screen flex items-center justify-center p-4">
30
+ <div className="max-w-md w-full space-y-8 bg-dark-800 p-8 rounded-lg shadow-2xl border border-dark-700">
31
+ <div className="text-center">
32
+ <h1 className="text-5xl font-bold text-accent-400 mb-2">🔍 Cluedo Custom</h1>
33
+ <p className="text-dark-300">Créez votre partie et invitez vos amis</p>
34
+ </div>
35
+
36
+ <div className="space-y-4">
37
+ <div>
38
+ <label className="block text-sm font-medium text-dark-200 mb-2">
39
+ Votre nom
40
+ </label>
41
+ <input
42
+ type="text"
43
+ value={playerName}
44
+ onChange={(e) => setPlayerName(e.target.value)}
45
+ onKeyPress={(e) => e.key === 'Enter' && handleCreateGame()}
46
+ placeholder="Entrez votre nom"
47
+ className="w-full px-4 py-3 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-dark-400 focus:outline-none focus:ring-2 focus:ring-accent-500"
48
+ />
49
+ </div>
50
+
51
+ <button
52
+ onClick={handleCreateGame}
53
+ disabled={loading}
54
+ className="w-full py-3 px-4 bg-accent-600 hover:bg-accent-700 disabled:bg-dark-600 text-white font-semibold rounded-lg transition-colors"
55
+ >
56
+ {loading ? 'Création...' : '🎮 Créer une partie'}
57
+ </button>
58
+
59
+ <div className="text-center pt-4">
60
+ <button
61
+ onClick={() => navigate('/join')}
62
+ className="text-accent-400 hover:text-accent-300 underline"
63
+ >
64
+ Rejoindre une partie existante
65
+ </button>
66
+ </div>
67
+ </div>
68
+
69
+ <div className="mt-8 pt-6 border-t border-dark-700 text-center text-sm text-dark-400">
70
+ <p>Thème par défaut : Manoir Classique</p>
71
+ <p className="mt-1">3-6 joueurs recommandés</p>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ )
76
+ }
77
+
78
+ export default Home
frontend/src/pages/Join.jsx ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+ import { joinGame } from '../api'
4
+
5
+ function Join() {
6
+ const [gameCode, setGameCode] = useState('')
7
+ const [playerName, setPlayerName] = useState('')
8
+ const [loading, setLoading] = useState(false)
9
+ const navigate = useNavigate()
10
+
11
+ const handleJoinGame = async () => {
12
+ if (!gameCode.trim() || !playerName.trim()) {
13
+ alert('Veuillez remplir tous les champs')
14
+ return
15
+ }
16
+
17
+ setLoading(true)
18
+ try {
19
+ const response = await joinGame(gameCode.trim(), playerName.trim())
20
+ navigate(`/game/${response.game_id}/${response.player_id}`)
21
+ } catch (error) {
22
+ alert('Erreur : ' + (error.response?.data?.detail || 'Impossible de rejoindre la partie'))
23
+ console.error(error)
24
+ } finally {
25
+ setLoading(false)
26
+ }
27
+ }
28
+
29
+ return (
30
+ <div className="min-h-screen flex items-center justify-center p-4">
31
+ <div className="max-w-md w-full space-y-8 bg-dark-800 p-8 rounded-lg shadow-2xl border border-dark-700">
32
+ <div className="text-center">
33
+ <h1 className="text-4xl font-bold text-accent-400 mb-2">🚪 Rejoindre une partie</h1>
34
+ <p className="text-dark-300">Entrez le code partagé par votre hôte</p>
35
+ </div>
36
+
37
+ <div className="space-y-4">
38
+ <div>
39
+ <label className="block text-sm font-medium text-dark-200 mb-2">
40
+ Code de partie
41
+ </label>
42
+ <input
43
+ type="text"
44
+ value={gameCode}
45
+ onChange={(e) => setGameCode(e.target.value.toUpperCase())}
46
+ placeholder="Ex: ABC4"
47
+ maxLength={4}
48
+ className="w-full px-4 py-3 bg-dark-700 border border-dark-600 rounded-lg text-white text-center text-2xl font-mono placeholder-dark-400 focus:outline-none focus:ring-2 focus:ring-accent-500 uppercase"
49
+ />
50
+ </div>
51
+
52
+ <div>
53
+ <label className="block text-sm font-medium text-dark-200 mb-2">
54
+ Votre nom
55
+ </label>
56
+ <input
57
+ type="text"
58
+ value={playerName}
59
+ onChange={(e) => setPlayerName(e.target.value)}
60
+ onKeyPress={(e) => e.key === 'Enter' && handleJoinGame()}
61
+ placeholder="Entrez votre nom"
62
+ className="w-full px-4 py-3 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-dark-400 focus:outline-none focus:ring-2 focus:ring-accent-500"
63
+ />
64
+ </div>
65
+
66
+ <button
67
+ onClick={handleJoinGame}
68
+ disabled={loading}
69
+ className="w-full py-3 px-4 bg-accent-600 hover:bg-accent-700 disabled:bg-dark-600 text-white font-semibold rounded-lg transition-colors"
70
+ >
71
+ {loading ? 'Connexion...' : '✅ Rejoindre'}
72
+ </button>
73
+
74
+ <div className="text-center pt-4">
75
+ <button
76
+ onClick={() => navigate('/')}
77
+ className="text-accent-400 hover:text-accent-300 underline"
78
+ >
79
+ ← Retour à l'accueil
80
+ </button>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ )
86
+ }
87
+
88
+ export default Join