Spaces:
Sleeping
Sleeping
Commit
·
0344b6d
1
Parent(s):
8beb1aa
feat: add react + vite components
Browse files- .gitignore +4 -0
- Dockerfile +19 -19
- SETUP_REACT.md +0 -252
- docker-compose.yml +12 -0
- frontend/src/App.jsx +20 -0
- frontend/src/api.js +51 -0
- frontend/src/main.jsx +10 -0
- frontend/src/pages/Game.jsx +295 -0
- frontend/src/pages/Home.jsx +78 -0
- frontend/src/pages/Join.jsx +88 -0
.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 |
-
#
|
| 2 |
-
FROM
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
#
|
|
|
|
| 5 |
WORKDIR /app
|
| 6 |
|
| 7 |
-
#
|
| 8 |
-
|
| 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
|
| 17 |
-
COPY
|
| 18 |
|
| 19 |
-
#
|
| 20 |
-
|
| 21 |
|
| 22 |
-
#
|
| 23 |
-
|
| 24 |
-
CMD python -c "import requests; requests.get('http://localhost:7860/')"
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
CMD ["
|
|
|
|
| 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
|