Merge pull request 'final POC' (#103) from Backend_Fix into main

Reviewed-on: #103
This commit was merged in pull request #103.
This commit is contained in:
2025-11-24 22:29:53 +00:00
49 changed files with 4634 additions and 4620 deletions
@@ -1,750 +0,0 @@
# Frontend → Backend Felesleges Adatok Dokumentáció
## 📋 Összefoglaló
Ez a dokumentum tartalmazza azokat a mezőket és adatokat, amiket a frontend küld a backendnek, de **nem szükségesek** vagy **nem használtak** a backend oldalon.
**🎯 Fő probléma:** A frontend sok felesleges mezőt küld, ahelyett hogy egyetlen `answer` mezőt használna típus-specifikus formátumban.
**💾 Adatmegtakarítás:** ~40-60% payload csökkentés várható a tisztítás után!
---
## 📊 Gyors Összefoglaló Táblázat
| Mező | Használat | Cselekvés |
|------|-----------|-----------|
| `name` | ✅ Használt | Megtartani |
| `type` | ✅ Használt | Megtartani |
| `ctype` | ✅ Használt | Megtartani |
| `cards` | ✅ Használt | Megtartani |
| `description` | ❌ **Nincs a DB-ben** | **TÖRÖLNI** |
| | | |
| **Kártya mezők:** | | |
| `card.text` | ✅ Használt | Megtartani |
| `card.type` | ✅ Használt | Megtartani |
| `card.answer` | ✅ Használt | Megtartani (típus-specifikus!) |
| `card.consequence` | ✅ Használt (LUCK) | Megtartani |
| | | |
| `card.id` (frontend) | ❌ Nem releváns | **NE KÜLDJÜK** |
| `card.question` | ❌ Duplikáció | **TÖRÖLNI** (text-be) |
| `card.statement` | ❌ Duplikáció | **TÖRÖLNI** (text-be) |
| `card.options` | ❌ Felesleges | **KONVERTÁLNI** (answer-be) |
| `card.correctAnswer` | ❌ Felesleges | **KONVERTÁLNI** (answer-be) |
| `card.leftItems` | ❌ Felesleges | **KONVERTÁLNI** (answer-be) |
| `card.rightItems` | ❌ Felesleges | **KONVERTÁLNI** (answer-be) |
| `card.correctPairs` | ❌ Felesleges | **KONVERTÁLNI** (answer-be) |
| `card.acceptedAnswers` | ❌ Felesleges | **KONVERTÁLNI** (answer-be) |
| `card.hint` | ❌ Nincs implementálva | **TÖRÖLNI** |
---
## 🎯 Deck Létrehozás/Frissítés (createDeck / updateDeck)
### Backend által HASZNÁLT mezők:
```typescript
// CreateDeckCommand / UpdateDeckCommand
{
name: string, // ✅ HASZNÁLT - Pakli neve
type: number, // ✅ HASZNÁLT - 0=LUCK, 1=JOKER, 2=QUESTION
userid: string, // ✅ HASZNÁLT - Automatikusan hozzáadódik az authRequired middleware-ből
cards: any[], // ✅ HASZNÁLT - Kártyák tömbje
ctype?: number, // ✅ HASZNÁLT - 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION
state?: number, // ✅ HASZNÁLT - De csak admin állíthatja (0=ACTIVE, 1=SOFT_DELETE)
authLevel: number // ✅ HASZNÁLT - Automatikusan jön az auth middleware-ből
}
```
### Frontend által KÜLDÖTT de FELESLEGES mezők:
#### 1. **`description` mező** - ❌ NEM HASZNÁLT
**Helyek:** `DeckCreator.jsx` (line ~100-110, ~170)
```javascript
// FELESLEGES - Backend nem tárolja, nem használja
const payload = {
name: deck.name?.trim() || "Névtelen pakli",
type: typeMapping[deck.type] ?? 2,
ctype: ctypeMapping[deck.privacy] ?? 1,
cards: cleanedCards
// description: deck.description // ❌ Ez NINCS a backend sémában!
}
```
**Megjegyzés a kódban (line ~171):**
```javascript
// Note: description field is not sent to backend as it's not supported yet
```
**Javaslat:**
- Ha a `description` soha nem lesz használva → töröljük a frontend state-ből
- Ha később implementálni fogjuk → adjuk hozzá a backend DeckAggregate entitáshoz először
---
## 📇 Kártya Mezők (cards array)
### Backend Card Interface:
```typescript
export interface Card {
text: string; // ✅ KÖTELEZŐ
type?: CardType; // ✅ OPCIONÁLIS - 0=QUIZ, 1=PAIRING, 2=OWN_ANSWER, 3=TRUE_FALSE, 4=CLOSER
answer?: string | null; // ✅ OPCIONÁLIS
consequence?: Consequence | null; // ✅ OPCIONÁLIS (csak LUCK kártyáknál)
}
```
### Frontend által KÜLDÖTT de ESETLEG FELESLEGES kártya mezők:
#### A. **Duplikált mezők** (ugyanaz az adat több néven):
```javascript
// DeckCreator.jsx - cleanedCards mapping (line ~130-165)
// 1. TEXT mező duplikáció - ⚠️ REDUNDÁNS
cleanedCard.text = card.text || card.question || card.statement || ""
if (card.question !== undefined) cleanedCard.question = card.question // ❌ Felesleges?
if (card.statement !== undefined) cleanedCard.statement = card.statement // ❌ Felesleges?
// Backend csak a `text` mezőt használja!
// A `question` és `statement` valószínűleg NEM SZÜKSÉGESEK
```
**Megjegyzés:** A backend `Card` interfészben **nincs** `question` vagy `statement` mező, csak `text`.
#### B. **QUESTION típusú kártyák extra mezői** - ⚠️ ELLENŐRIZENDŐ
```javascript
// Ezek a mezők a DeckCreator.jsx-ben kerülnek hozzáadásra (line ~145-155)
if (card.question !== undefined) cleanedCard.question = card.question
if (card.statement !== undefined) cleanedCard.statement = card.statement
if (card.options !== undefined) cleanedCard.options = card.options
if (card.correctAnswer !== undefined) cleanedCard.correctAnswer = card.correctAnswer
if (card.leftItems !== undefined) cleanedCard.leftItems = card.leftItems
if (card.rightItems !== undefined) cleanedCard.rightItems = card.rightItems
if (card.correctPairs !== undefined) cleanedCard.correctPairs = card.correctPairs
if (card.acceptedAnswers !== undefined) cleanedCard.acceptedAnswers = card.acceptedAnswers
if (card.hint !== undefined) cleanedCard.hint = card.hint
```
**Backend Card interfész ezeket NEM tartalmazza:**
-`question` - Nincs a Card interface-ben
-`statement` - Nincs a Card interface-ben
-`options` - Nincs a Card interface-ben
-`correctAnswer` - Nincs a Card interface-ben
-`leftItems` - Nincs a Card interface-ben
-`rightItems` - Nincs a Card interface-ben
-`correctPairs` - Nincs a Card interface-ben
-`acceptedAnswers` - Nincs a Card interface-ben
-`hint` - Nincs a Card interface-ben
**KRITIKUS KÉRDÉS:**
- Ezek a mezők **JSON-ként tárolódnak** a `cards` mezőben?
- A backend TypeORM `@Column({ type: 'json' })` deklaráció miatt bármit el tud tárolni
- De a **Card interface** szerint csak `text`, `type`, `answer`, `consequence` mezőket használ
**Két lehetséges eset:**
1. **Ha a backend JSON mezőként tárolja de nem használja ezeket:**
- ❌ FELESLEGESEK - Adatbázis helyet pazarolnak
- Javaslat: Tisztítsuk meg a frontend-et, ne küldje őket
2. **Ha a backend valahol mégis használja (pl. game logic-ban):**
- ✅ SZÜKSÉGESEK - De akkor frissíteni kell a Card interface-t
---
## 🎮 Consequence mező - ✅ RENDBEN (de típus ellenőrzés szükséges)
```javascript
// DeckCreator.jsx (line ~160-162)
if (deck.type === 'LUCK' && card.consequence) {
cleanedCard.consequence = card.consequence
}
```
**Backend Consequence interface:**
```typescript
export interface Consequence {
type: ConsequenceType; // 0-5 közötti szám
value?: number;
}
```
**Javaslat:** Ellenőrizni kell hogy a frontend mindig valid `ConsequenceType` enum értéket küld-e (0-5).
---
## 🔍 Részletes Backend vs Frontend Mapping
### Deck Level
| Frontend Mező | Backend Mező | Használat | Megjegyzés |
|--------------|-------------|----------|-----------|
| `deck.id` | `id` | ✅ Használt | UUID |
| `deck.name` | `name` | ✅ Használt | string (max 255) |
| `deck.type` | `type` | ✅ Használt | 0/1/2 (enum) |
| `deck.privacy` | `ctype` | ✅ Használt | 0/1/2 (enum) |
| `deck.description` | - | ❌ **NEM LÉTEZIK** | **FELESLEGES** |
| `deck.cards` | `cards` | ✅ Használt | JSON array |
| `deck.creationdate` | `creationdate` | ✅ Használt | Date (readonly) |
| `deck.updatedate` | `updateDate` | ✅ Használt | Date (readonly) |
### Card Level (QUESTION típusú kártyák)
| Frontend Mező | Backend Card Interface | Használat | Megjegyzés |
|--------------|----------------------|----------|-----------|
| `card.id` | - | ❌ **Lokális azonosító** | Csak frontend-en, backenden nem releváns |
| `card.text` | `text` | ✅ Használt | Fő szöveg |
| `card.question` | - | ❓ **Ellenőrizendő** | Lehet felesleges (text duplikáció?) |
| `card.statement` | - | ❓ **Ellenőrizendő** | Lehet felesleges (text duplikáció?) |
| `card.type` / `card.subType` | `type` | ✅ Használt | CardType enum (0-4) |
| `card.answer` | `answer` | ✅ Használt | String vagy null |
| `card.options` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.correctAnswer` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.leftItems` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.rightItems` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.correctPairs` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.acceptedAnswers` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.hint` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.consequence` | `consequence` | ✅ Használt | Csak LUCK típusnál |
---
## ⚠️ BACKEND GAME LOGIC VIZSGÁLAT - ✅ KÉSZ
### 1. Kártya mezők tényleges használata - ELLENŐRIZVE ✅
**Ellenőrzött fájlok:**
-`SerpentRace_Backend/src/Application/Services/CardProcessingService.ts`
-`SerpentRace_Backend/src/Application/Services/CardDrawingService.ts`
**EREDMÉNY: A backend CSAK az `answer` mezőt használja!**
**Backend Card használat:**
```typescript
export interface Card {
text: string; // ✅ Kérdés szövege
type?: CardType; // ✅ Kártya típus (0-4)
answer?: string | null; // ✅ EGYETLEN valid mező a válaszokhoz!
consequence?: Consequence | null; // ✅ Csak LUCK kártyákhoz
}
```
**Fontos:** A backend `answer` mező **típus-specifikus formátumú**:
1. **QUIZ (type: 0)**`answer` = `QuizOption[]` array:
```typescript
answer: [
{ answer: "A", text: "First option", correct: false },
{ answer: "B", text: "Second option", correct: true },
...
]
```
2. **SENTENCE_PAIRING (type: 1)** → `answer` = Párosítás array:
```typescript
answer: [
{ left: "Apple", right: "Red" },
{ left: "Banana", right: "Yellow" }
]
```
3. **OWN_ANSWER (type: 2)** → `answer` = String vagy String array:
```typescript
answer: ["correct answer 1", "correct answer 2"]
```
4. **TRUE_FALSE (type: 3)** → `answer` = Boolean:
```typescript
answer: true // vagy false
```
5. **CLOSER (type: 4)** → `answer` = Object:
```typescript
answer: { correct: 42, percent: 10 }
```
**KÖVETKEZTETÉS:**
- ❌ **A frontend által küldött `options`, `correctAnswer`, `acceptedAnswers`, `leftItems`, `rightItems`, `correctPairs` mezők MIND FELESLEGESEK!**
- ✅ **Csak az `answer` mezőt kellene küldeni, megfelelő formátumban!**
### 2. Card Type Mapping
**Frontend:**
```javascript
const cardTypeMapping = {
'quiz': 0, // QUIZ
'pairing': 1, // SENTENCE_PAIRING
'text': 2, // OWN_ANSWER
'truefalse': 3, // TRUE_FALSE
'closer': 4 // CLOSER
}
```
**Backend CardType enum:**
```typescript
export enum CardType {
QUIZ = 0,
SENTENCE_PAIRING = 1,
OWN_ANSWER = 2,
TRUE_FALSE = 3,
CLOSER = 4
}
```
✅ **Ez HELYES** - A mapping megfelelő
### 3. Frontend kártya ID kezelés
```javascript
// DeckCreator.jsx (line ~242)
const updatedCard = {
...cardData,
id: isCreatingCard ? Date.now() : cardData.id
}
// Line ~129
if (card.id) {
cleanedCard.id = card.id
}
```
**Probléma:** A frontend `Date.now()` timestamp ID-kat generál, de a backend UUID-kat használ.
**Javaslat:**
- ❌ NE küldjük a frontend-generált `id`-t a backendnek
- A backend a create során generál UUID-t
- Update-nél a backend már ismeri az ID-t (URL parameter-ből jön)
---
## 📝 JAVASOLT TISZTÍTÁSOK
### Prioritás 1: BIZTOS FELESLEGESEK
1. **`description` mező törlése**
- Fájl: `DeckCreator.jsx`
- Sorok: ~20, ~40-45, ~100-105
- Töröljük a state-ből és ne küldjük a backendnek
2. **Frontend-generált kártya `id` ne menjen a backendre**
- Fájl: `DeckCreator.jsx`
- Sor: ~129
- Kommenteljük ki vagy töröljük: `if (card.id) cleanedCard.id = card.id`
### Prioritás 2: BIZONYÍTOTTAN FELESLEGESEK ✅
3. **Duplikált text mezők (`question`, `statement`)** - ❌ FELESLEGES
- A backend **csak `text`-et használ**
- Töröljük: `question` és `statement` mezők küldését
4. **QUESTION kártya részletes mezők - MIND FELESLEGESEK ❌**
- A backend GameService **NEM használja** ezeket:
- ❌ `options` - Felesleges (backend: `answer` array használ)
- ❌ `correctAnswer` - Felesleges (backend: `answer` array-ben `correct: true`)
- ❌ `leftItems` / `rightItems` / `correctPairs` - Felesleges (backend: `answer` array-ben `{left, right}` párok)
- ❌ `acceptedAnswers` - Felesleges (backend: `answer` string array)
- ❌ `hint` - Nincs implementálva a backenden
**HELYETTE:** Konvertáljuk ezeket megfelelő `answer` formátumra!
---
## 🔄 HELYES KONVERZIÓ - Példák
### Jelenlegi (FELESLEGES mezőkkel):
```javascript
// ❌ ROSSZ - Felesleges mezők küldése
const cleanedCard = {
text: "Mi a főváros?",
type: 0, // QUIZ
question: "Mi a főváros?", // ❌ DUPLIKÁCIÓ
options: ["Budapest", "Berlin", "Prága"], // ❌ FELESLEGES
correctAnswer: 0 // ❌ FELESLEGES
}
```
### Helyes (Optimalizált):
```javascript
// ✅ JÓ - Csak szükséges mezők
const cleanedCard = {
text: "Mi a főváros?",
type: 0, // QUIZ
answer: [
{ answer: "A", text: "Budapest", correct: true },
{ answer: "B", text: "Berlin", correct: false },
{ answer: "C", text: "Prága", correct: false }
]
}
```
### Konverziós Példák Típusonként:
#### 1. QUIZ (type: 0) - Feleletválasztós
**Frontend állapot:**
```javascript
card = {
subType: 'multiplechoice',
question: "Melyik a helyes?",
options: ["A válasz", "B válasz", "C válasz"],
correctAnswer: 1
}
```
**Helyes backend formátum:**
```javascript
cleanedCard = {
text: "Melyik a helyes?",
type: 0,
answer: [
{ answer: "A", text: "A válasz", correct: false },
{ answer: "B", text: "B válasz", correct: true }, // correctAnswer: 1
{ answer: "C", text: "C válasz", correct: false }
]
}
```
#### 2. SENTENCE_PAIRING (type: 1) - Párosítás
**Frontend állapot:**
```javascript
card = {
subType: 'matching',
question: "Párosítsd össze!",
leftItems: ["Alma", "Banán"],
rightItems: ["Piros", "Sárga"],
correctPairs: { 0: 0, 1: 1 } // leftItems[0] -> rightItems[0]
}
```
**Helyes backend formátum:**
```javascript
cleanedCard = {
text: "Párosítsd össze!",
type: 1,
answer: [
{ left: "Alma", right: "Piros" },
{ left: "Banán", right: "Sárga" }
]
}
```
#### 3. OWN_ANSWER (type: 2) - Szöveges válasz
**Frontend állapot:**
```javascript
card = {
subType: 'text',
question: "Mi a főváros?",
acceptedAnswers: ["Budapest", "budapest", "Bp"]
}
```
**Helyes backend formátum:**
```javascript
cleanedCard = {
text: "Mi a főváros?",
type: 2,
answer: ["Budapest", "budapest", "Bp"]
}
```
#### 4. TRUE_FALSE (type: 3) - Igaz/Hamis
**Frontend állapot:**
```javascript
card = {
subType: 'truefalse',
statement: "A Föld lapos.",
correctAnswer: 1, // 0=Igaz, 1=Hamis
isTrue: false
}
```
**Helyes backend formátum:**
```javascript
cleanedCard = {
text: "A Föld lapos.",
type: 3,
answer: false
}
```
#### 5. CLOSER (type: 4) - Tippelés
**Frontend állapot:**
```javascript
card = {
subType: 'closer',
question: "Hány lakosa van Budapestnek?",
correctAnswer: 1750000,
tolerance: 10 // ±10%
}
```
**Helyes backend formátum:**
```javascript
cleanedCard = {
text: "Hány lakosa van Budapestnek?",
type: 4,
answer: {
correct: 1750000,
percent: 10
}
}
```
---
## 🔧 TESZTELÉSI TERV
1. **Logolás hozzáadása a backenden:**
```typescript
// CreateDeckCommandHandler.ts, UpdateDeckCommandHandler.ts
console.log('Received card data:', cmd.cards)
console.log('Card keys:', Object.keys(cmd.cards[0]))
```
2. **Frontendről küldött payload ellenőrzése:**
```javascript
// DeckCreator.jsx - handleSaveDeck
console.log('Payload before send:', JSON.stringify(payload, null, 2))
```
3. **Adatbázisban tárolt JSON ellenőrzése:**
```sql
SELECT id, name, cards FROM Decks WHERE id = 'xyz' LIMIT 1;
```
---
## ✅ KÖVETKEZŐ LÉPÉSEK
1. ✅ **Dokumentáció elkészült** - Ez a fájl
2. ✅ **Backend game logic ellenőrzés** - KÉSZ! Csak `answer` mezőt használ
3. ⏳ **Frontend konverzió implementálás** - Következő feladat:
- Új függvény: `convertCardToBackendFormat(card, deckType)`
- Minden kártyatípushoz megfelelő `answer` formátum generálása
- Felesleges mezők eltávolítása
4. ⏳ **Tesztelés** - Minden működik-e a változások után?
---
## 🛠️ IMPLEMENTÁCIÓS TERV
### 1. Létrehozandó segédfüggvény: `cardBackendConverter.js`
```javascript
// src/utils/cardBackendConverter.js
/**
* Konvertálja a frontend kártya formátumot backend-kompatibilis formátumra
* @param {Object} card - Frontend kártya objektum
* @param {string} deckType - Pakli típusa ('LUCK', 'JOKER', 'QUESTION')
* @returns {Object} Backend-kompatibilis kártya objektum
*/
export function convertCardToBackendFormat(card, deckType) {
const baseCard = {
text: card.text || card.question || card.statement || "",
}
// CardType mapping
const cardTypeMapping = {
'quiz': 0,
'multiplechoice': 0, // Alias
'pairing': 1,
'matching': 1, // Alias
'text': 2,
'truefalse': 3,
'closer': 4
}
const cardType = cardTypeMapping[card.subType] ?? cardTypeMapping[card.subType?.toLowerCase()]
if (cardType !== undefined) {
baseCard.type = cardType
}
// Típus-specifikus answer konverzió
switch (cardType) {
case 0: // QUIZ
if (card.options && Array.isArray(card.options)) {
baseCard.answer = card.options.map((opt, idx) => ({
answer: String.fromCharCode(65 + idx), // A, B, C, D...
text: opt,
correct: idx === card.correctAnswer
}))
}
break
case 1: // SENTENCE_PAIRING
if (card.leftItems && card.rightItems && card.correctPairs) {
baseCard.answer = Object.entries(card.correctPairs).map(([leftIdx, rightIdx]) => ({
left: card.leftItems[parseInt(leftIdx)],
right: card.rightItems[parseInt(rightIdx)]
}))
}
break
case 2: // OWN_ANSWER
if (card.acceptedAnswers && Array.isArray(card.acceptedAnswers)) {
baseCard.answer = card.acceptedAnswers.filter(a => a && a.trim())
}
break
case 3: // TRUE_FALSE
if (card.correctAnswer !== undefined) {
baseCard.answer = card.correctAnswer === 0 // 0=Igaz, 1=Hamis
} else if (card.isTrue !== undefined) {
baseCard.answer = card.isTrue
}
break
case 4: // CLOSER
if (card.correctAnswer !== undefined && card.tolerance !== undefined) {
baseCard.answer = {
correct: card.correctAnswer,
percent: card.tolerance
}
}
break
}
// LUCK típusú kártyákhoz consequence
if (deckType === 'LUCK' && card.consequence) {
baseCard.consequence = card.consequence
}
return baseCard
}
```
### 2. Módosítandó fájl: `DeckCreator.jsx`
**Jelenlegi kód (line ~120-165):**
```javascript
// ❌ RÉGI - Felesleges mezők küldése
const cleanedCards = validCards.map(card => {
const cleanedCard = {}
if (card.id) cleanedCard.id = card.id
if (card.subType && cardTypeMapping[card.subType] !== undefined) {
cleanedCard.type = cardTypeMapping[card.subType]
}
cleanedCard.text = card.text || card.question || card.statement || ""
if (card.question !== undefined) cleanedCard.question = card.question // FELESLEGES
if (card.statement !== undefined) cleanedCard.statement = card.statement // FELESLEGES
if (card.options !== undefined) cleanedCard.options = card.options // FELESLEGES
// ... stb
return cleanedCard
})
```
**Új kód:**
```javascript
// ✅ ÚJ - Csak szükséges mezők
import { convertCardToBackendFormat } from '../../utils/cardBackendConverter'
const cleanedCards = validCards.map(card =>
convertCardToBackendFormat(card, deck.type)
)
```
### 3. Tesztelési checklist
- [ ] QUIZ kártyák helyes answer formátummal mentődnek
- [ ] SENTENCE_PAIRING kártyák helyes left-right párokkal mentődnek
- [ ] OWN_ANSWER kártyák acceptedAnswers array-ként mentődnek
- [ ] TRUE_FALSE kártyák boolean answer-rel mentődnek
- [ ] CLOSER kártyák {correct, percent} formátummal mentődnek
- [ ] LUCK kártyák consequence mezője megmarad
- [ ] Mentett paklik betöltése és szerkesztése működik
- [ ] Játék során kártyák feldolgozása helyes
---
**Utolsó frissítés:** 2025-11-03
**Készítette:** GitHub Copilot
**Cél:** Adatoptimalizálás és felesleges payload csökkentés
---
## 📈 VÁRHATÓ EREDMÉNYEK
### Payload méret csökkenés példa:
**ELŐTTE (jelenleg):**
```json
{
"name": "Teszt Pakli",
"type": 2,
"ctype": 1,
"description": "Ez egy leírás", // ❌ FELESLEGES
"cards": [
{
"id": 1730123456789, // ❌ FELESLEGES
"text": "Mi a főváros?",
"question": "Mi a főváros?", // ❌ DUPLIKÁCIÓ
"type": 0,
"options": ["Budapest", "Berlin", "Prága"], // ❌ FELESLEGES
"correctAnswer": 0 // ❌ FELESLEGES
}
]
}
// Méret: ~280 byte
```
**UTÁNA (optimalizált):**
```json
{
"name": "Teszt Pakli",
"type": 2,
"ctype": 1,
"cards": [
{
"text": "Mi a főváros?",
"type": 0,
"answer": [
{"answer": "A", "text": "Budapest", "correct": true},
{"answer": "B", "text": "Berlin", "correct": false},
{"answer": "C", "text": "Prága", "correct": false}
]
}
]
}
// Méret: ~190 byte
```
**💾 Megtakarítás: ~32% ebben a példában!**
---
## 🎉 VÉGSŐ ÖSSZEFOGLALÁS
### Felesleges mezők száma:
- **Deck level:** 1 mező (`description`)
- **Card level:** 9 mező (`id`, `question`, `statement`, `options`, `correctAnswer`, `leftItems`, `rightItems`, `correctPairs`, `acceptedAnswers`, `hint`)
### Összes felesleges mező: **10 db**
### Ajánlott lépések:
1. ✅ Dokumentáció áttekintése
2. 🔄 `cardBackendConverter.js` implementálása
3. 🔧 `DeckCreator.jsx` módosítása
4. ✅ Tesztelés minden kártyatípussal
5. 🚀 Deploy
**Becsült munkaidő:** 2-3 óra implementálás + 1 óra tesztelés
---
## 📞 Kérdések / Problémák esetén
Ha bármilyen kérdés merül fel az implementálás során:
1. Ellenőrizd a backend `CardProcessingService.ts` fájlt
2. Nézd meg a példákat ebben a dokumentációban
3. Teszteld lokálisan először egy kis paklival
**Fontos:** A backend JSON mezőként tárolja a `cards` array-t, ezért bármit elfogad - de csak a dokumentált mezőket használja!
+56
View File
@@ -8,6 +8,9 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@tailwindcss/vite": "^4.1.7",
"axios": "^1.12.2",
"framer-motion": "^12.19.1",
@@ -326,6 +329,59 @@
"node": ">=6.9.0"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
+3
View File
@@ -10,6 +10,9 @@
"preview": "vite preview"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@tailwindcss/vite": "^4.1.7",
"axios": "^1.12.2",
"framer-motion": "^12.19.1",
@@ -99,7 +99,7 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
if (data.type === 'QUESTION') {
// Quiz típus validálás
if (data.subType === 'quiz') {
if (!data.question || !data.question.trim()) {
if (!data.text || !data.text.trim()) {
notifyError("Kérdés megadása kötelező!")
return false
}
@@ -110,7 +110,7 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
}
// Igaz/Hamis típus validálás
else if (data.subType === 'truefalse') {
if (!data.statement || !data.statement.trim()) {
if (!data.text || !data.text.trim()) {
notifyError("Állítás megadása kötelező!")
return false
}
@@ -121,7 +121,7 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
}
// Párosítás típus validálás
else if (data.subType === 'matching') {
if (!data.taskDescription || !data.taskDescription.trim()) {
if (!data.text || !data.text.trim()) {
notifyError("Feladat leírása kötelező!")
return false
}
@@ -136,7 +136,7 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
}
// Szöveges válasz típus validálás
else if (data.subType === 'text') {
if (!data.question || !data.question.trim()) {
if (!data.text || !data.text.trim()) {
notifyError("Kérdés megadása kötelező!")
return false
}
@@ -147,7 +147,7 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
}
// Általános validálás (ha nincs subType megadva)
else {
if (!data.question && !data.statement) {
if (!data.text || !data.text.trim()) {
notifyError("Kérdés vagy állítás megadása kötelező!")
return false
}
@@ -129,8 +129,8 @@ export default function TaskCardEditor({ card, onChange }) {
Kérdés
</label>
<textarea
value={card.question || ''}
onChange={(e) => updateField('question', e.target.value)}
value={card.text || ''}
onChange={(e) => updateField('text', e.target.value)}
className="w-full px-4 py-3 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200 resize-none"
rows="3"
placeholder="Írd be a kérdést..."
@@ -190,8 +190,8 @@ export default function TaskCardEditor({ card, onChange }) {
Állítás
</label>
<textarea
value={card.statement || ''}
onChange={(e) => updateField('statement', e.target.value)}
value={card.text || ''}
onChange={(e) => updateField('text', e.target.value)}
className="w-full px-4 py-3 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200 resize-none"
rows="3"
placeholder="Írd be az állítást..."
@@ -251,8 +251,8 @@ export default function TaskCardEditor({ card, onChange }) {
</label>
<input
type="text"
value={card.taskDescription || ''}
onChange={(e) => updateField('taskDescription', e.target.value)}
value={card.text || ''}
onChange={(e) => updateField('text', e.target.value)}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200"
placeholder="Pl.: Párosítsd a országokat a fővárosukkal"
/>
@@ -326,8 +326,8 @@ export default function TaskCardEditor({ card, onChange }) {
Kérdés
</label>
<textarea
value={card.question || ''}
onChange={(e) => updateField('question', e.target.value)}
value={card.text || ''}
onChange={(e) => updateField('text', e.target.value)}
className="w-full px-4 py-3 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200 resize-none"
rows="3"
placeholder="Írd be a kérdést..."
@@ -20,8 +20,17 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
setError("Add meg a játék kódját!")
return
}
// Check if user has a name (logged in or guest)
const nameToSend = username ?? guestName?.trim()
if (!nameToSend) {
setGuestError("Adj meg egy nevet a csatlakozáshoz!")
return
}
setError("")
onJoinGame(joinCode)
setGuestError("")
onJoinGame(joinCode, nameToSend)
}
const handleCreate = () => {
@@ -0,0 +1,52 @@
/**
* Card Type Constants
* Must match backend CardType enum from DeckAggregate.ts
*/
export const CardType = {
QUIZ: 0, // Multiple choice (A, B, C, D)
SENTENCE_PAIRING: 1, // Match left to right parts
OWN_ANSWER: 2, // Free text answer
TRUE_FALSE: 3, // True or False question
CLOSER: 4 // Guess the closest number
};
/**
* Get card type name for display
*/
export const getCardTypeName = (type) => {
switch (type) {
case CardType.QUIZ:
return 'Kvíz';
case CardType.SENTENCE_PAIRING:
return 'Mondatpárosítás';
case CardType.OWN_ANSWER:
return 'Saját válasz';
case CardType.TRUE_FALSE:
return 'Igaz vagy Hamis';
case CardType.CLOSER:
return 'Közelebb';
default:
return 'Kérdés';
}
};
/**
* Check if card type requires text input
*/
export const requiresTextInput = (type) => {
return type === CardType.OWN_ANSWER || type === CardType.TRUE_FALSE || type === CardType.CLOSER;
};
/**
* Check if card type has multiple choice options
*/
export const hasMultipleChoice = (type) => {
return type === CardType.QUIZ;
};
/**
* Check if card type has sentence pairing
*/
export const hasSentencePairing = (type) => {
return type === CardType.SENTENCE_PAIRING;
};
@@ -16,42 +16,48 @@ export const GameWebSocketProvider = ({ children }) => {
const socketRef = useRef(null);
const gameTokenRef = useRef(null);
const [isConnected, setIsConnected] = useState(false);
// Single game object containing all game data
const [gameState, setGameState] = useState(null);
const [boardData, setBoardData] = useState(null);
// Structure: {
// gameCode: string,
// boardData: object,
// turnInfo: { currentPlayer, currentPlayerName, turnNumber },
// players: [{ playerId, playerName, isOnline, isReady }],
// playerPositions: { playerName: position },
// connectedPlayers: [],
// readyPlayers: [],
// state: string,
// isGamemaster: boolean
// }
const [error, setError] = useState(null);
const [isGamemaster, setIsGamemaster] = useState(false);
const [gameStarted, setGameStarted] = useState(false);
const [pendingPlayers, setPendingPlayers] = useState([]);
const [approvalStatus, setApprovalStatus] = useState(null);
const [playerIdentifier, setPlayerIdentifier] = useState(null);
const [isMyTurnFlag, setIsMyTurnFlag] = useState(false); // Directly controlled by game:your-turn
const [playerDiceRolls, setPlayerDiceRolls] = useState({});
// Memoized derived values
// Memoized derived values - extract from single game object
const players = useMemo(() => {
const connectedPlayers = gameState?.connectedPlayers || [];
const currentPlayers = gameState?.currentPlayers || [];
if (currentPlayers.length > 0) {
return currentPlayers;
}
if (connectedPlayers.length > 0) {
return connectedPlayers.map((nameOrObj, index) => {
const playerName = typeof nameOrObj === 'string'
? nameOrObj
: (nameOrObj.playerName || nameOrObj.name || `Player ${index + 1}`);
return {
id: `player-${index}`,
name: playerName,
isOnline: true,
isReady: gameState?.readyPlayers?.includes(playerName) || false,
};
});
}
return [];
}, [gameState?.connectedPlayers, gameState?.currentPlayers, gameState?.readyPlayers]);
return gameState?.players || [];
}, [gameState?.players]);
const currentTurn = useMemo(() => gameState?.currentPlayer || null, [gameState?.currentPlayer]);
const playerPositions = useMemo(() => {
return gameState?.playerPositions || {};
}, [gameState?.playerPositions]);
const boardData = useMemo(() => {
return gameState?.boardData || null;
}, [gameState?.boardData]);
const currentTurn = useMemo(() => gameState?.turnInfo?.currentPlayer || null, [gameState?.turnInfo?.currentPlayer]);
const currentTurnName = useMemo(() => gameState?.turnInfo?.currentPlayerName || null, [gameState?.turnInfo?.currentPlayerName]);
// isMyTurn is simply the flag set by game:your-turn event
const isMyTurn = isMyTurnFlag;
/**
* Connect to game WebSocket with a game token
@@ -79,6 +85,16 @@ export const GameWebSocketProvider = ({ children }) => {
log('🔌 Connecting to game WebSocket...');
gameTokenRef.current = gameToken;
// Decode token to get player identifier
try {
const payload = JSON.parse(atob(gameToken.split('.')[1]));
const identifier = payload.userId || payload.playerName;
setPlayerIdentifier(identifier);
log('🎮 Player identifier:', identifier);
} catch (err) {
logError('Failed to decode game token:', err);
}
// Connect to /game namespace
socketRef.current = io(`${API_CONFIG.wsURL}/game`, {
transports: ['websocket'],
@@ -102,11 +118,22 @@ export const GameWebSocketProvider = ({ children }) => {
logError('❌ Connection error:', err);
setIsConnected(false);
setError(err.message);
// If reconnection fails completely, navigate to home
if (err.message?.includes('timeout') || err.message?.includes('xhr poll error')) {
setTimeout(() => {
if (!socketRef.current?.connected) {
logError('⚠️ Connection failed - redirecting to home');
window.location.href = '/';
}
}, 10000); // Give 10 seconds for reconnection attempts
}
});
socket.on('disconnect', (reason) => {
log('🔌 Disconnected:', reason);
setIsConnected(false);
setIsMyTurnFlag(false); // Clear turn flag on disconnect
});
// Game state handlers
@@ -115,7 +142,72 @@ export const GameWebSocketProvider = ({ children }) => {
if (state?.isGamemaster !== undefined) {
setIsGamemaster(state.isGamemaster);
}
setGameState(state);
// Merge state into single game object
setGameState(prev => {
// Build players array from various sources
let playersArray = prev?.players || [];
// If we have currentPlayers or players from backend, use them (already have proper structure)
if (state.currentPlayers && Array.isArray(state.currentPlayers) && state.currentPlayers.length > 0) {
playersArray = state.currentPlayers.map(p => ({
playerId: p.playerId || p.id,
playerName: p.playerName || p.name,
name: p.playerName || p.name, // Add name for compatibility
isOnline: p.isOnline !== undefined ? p.isOnline : true,
isReady: p.isReady || false
}));
} else if (state.players && Array.isArray(state.players) && state.players.length > 0) {
playersArray = state.players.map(p => ({
playerId: p.playerId || p.id,
playerName: p.playerName || p.name,
name: p.playerName || p.name, // Add name for compatibility
isOnline: p.isOnline !== undefined ? p.isOnline : true,
isReady: p.isReady || false
}));
}
// If players array is still empty but we have connectedPlayers (array of strings), convert them
else if (playersArray.length === 0 && state.connectedPlayers && Array.isArray(state.connectedPlayers) && state.connectedPlayers.length > 0) {
playersArray = state.connectedPlayers.map((playerName, index) => ({
playerId: `player-${index}`, // Temporary ID until we get the real one
playerName: playerName,
name: playerName, // Add name for compatibility
isOnline: true,
isReady: false
}));
}
// Initialize playerPositions from backend data if joining game in progress
let playerPositions = prev?.playerPositions || {};
// If we don't have positions yet but backend sent players with positions, initialize from them
if (Object.keys(playerPositions).length === 0 && state.players && Array.isArray(state.players)) {
const positionsFromBackend = {};
state.players.forEach(p => {
const playerName = p.playerName || p.name;
if (playerName && p.position !== undefined) {
positionsFromBackend[playerName] = p.position;
}
});
if (Object.keys(positionsFromBackend).length > 0) {
playerPositions = positionsFromBackend;
}
}
const merged = {
...prev,
gameCode: state.gameCode || prev?.gameCode,
boardData: state.boardData || prev?.boardData,
state: state.state || prev?.state,
connectedPlayers: state.connectedPlayers || prev?.connectedPlayers || [],
readyPlayers: state.readyPlayers || prev?.readyPlayers || [],
// Preserve turn info - only turn-changed updates it
turnInfo: prev?.turnInfo || { currentPlayer: null, currentPlayerName: null, turnNumber: 0 },
// Use the built players array
players: playersArray,
// Initialize playerPositions from backend if joining in progress, otherwise preserve
playerPositions: playerPositions
};
return merged;
});
});
socket.on('game:state-update', (state) => {
@@ -123,7 +215,19 @@ export const GameWebSocketProvider = ({ children }) => {
if (state?.isGamemaster !== undefined) {
setIsGamemaster(state.isGamemaster);
}
setGameState(state);
// Merge state update into single game object
setGameState(prev => ({
...prev,
gameCode: state.gameCode || prev?.gameCode,
boardData: state.boardData || prev?.boardData,
state: state.state || prev?.state,
connectedPlayers: state.connectedPlayers || prev?.connectedPlayers,
readyPlayers: state.readyPlayers || prev?.readyPlayers,
// Preserve turn info and positions
turnInfo: prev?.turnInfo,
players: prev?.players,
playerPositions: prev?.playerPositions
}));
});
socket.on('game:joined', (data) => {
@@ -131,6 +235,26 @@ export const GameWebSocketProvider = ({ children }) => {
if (data.isGamemaster !== undefined) {
setIsGamemaster(data.isGamemaster);
}
// Initialize or update gameState with gameCode and gameId from join confirmation
setGameState(prev => {
if (prev && prev.gameCode) {
// If already has gameCode, just add gameId if missing
return {
...prev,
gameId: data.gameId || prev.gameId,
gameCode: data.gameCode,
playerName: data.playerName,
isAuthenticated: data.isAuthenticated
};
}
return {
...prev,
gameId: data.gameId, // Store gameId from backend
gameCode: data.gameCode,
playerName: data.playerName,
isAuthenticated: data.isAuthenticated
};
});
});
socket.on('game:player-joined', (data) => {
@@ -138,60 +262,128 @@ export const GameWebSocketProvider = ({ children }) => {
setGameState(prev => {
if (!prev) return prev;
const currentConnected = prev.connectedPlayers || [];
if (!currentConnected.includes(data.playerName)) {
return {
...prev,
connectedPlayers: [...currentConnected, data.playerName]
};
const currentPlayers = prev.players || [];
// Add to connectedPlayers if not already there
const updatedConnected = currentConnected.includes(data.playerName)
? currentConnected
: [...currentConnected, data.playerName];
// Add to players array if not already there (without position - that's in playerPositions)
const playerExists = currentPlayers.some(p =>
p.playerName === data.playerName || p.name === data.playerName
);
const updatedPlayers = playerExists
? currentPlayers
: [...currentPlayers, {
playerId: data.playerId,
playerName: data.playerName,
name: data.playerName, // Add name for compatibility with Lobby display
isOnline: true,
isReady: false
}];
return {
...prev,
connectedPlayers: updatedConnected,
players: updatedPlayers
};
});
window.dispatchEvent(new CustomEvent('game:player-joined', { detail: data }));
});
socket.on('game:player-left', (data) => {
log('👋 Player left:', data.playerName);
setGameState(prev => {
if (!prev) return prev;
return {
...prev,
connectedPlayers: (prev.connectedPlayers || []).filter(p => p !== data.playerName)
};
});
window.dispatchEvent(new CustomEvent('game:player-left', { detail: data }));
});
socket.on('game:player-ready', (data) => {
log('✅ Player ready:', data.playerName);
setGameState(prev => {
if (!prev) return prev;
const readyPlayers = prev.readyPlayers || [];
if (data.ready && !readyPlayers.includes(data.playerName)) {
return { ...prev, readyPlayers: [...readyPlayers, data.playerName] };
} else if (!data.ready) {
return { ...prev, readyPlayers: readyPlayers.filter(p => p !== data.playerName) };
}
return prev;
});
window.dispatchEvent(new CustomEvent('game:player-ready', { detail: data }));
});
socket.on('game:all-ready', (data) => {
log('✅ All players ready! Game can start.');
window.dispatchEvent(new CustomEvent('game:all-ready', { detail: data }));
});
socket.on('game:start', (data) => {
log('🎮 Game started:', data);
setGameStarted(true);
// Store board data if provided
if (data.boardData) {
setBoardData(data.boardData);
log('✅ Board data stored from game:start event');
}
// Update game state with turn info
if (data.playerOrder) {
setGameState(prev => ({
...prev,
playerOrder: data.playerOrder,
currentPlayer: data.currentPlayer,
turnSequence: data.playerOrder
}));
}
});
socket.on('game:started', (data) => {
log('🎮 Game started (legacy event):', data);
setGameStarted(true);
});
socket.on('game:player-moved', (moveData) => {
log('🏃 Player moved:', moveData.playerName);
// Update game state with position initialization
setGameState(prev => {
if (!prev?.currentPlayers) return prev;
return {
// Initialize all player positions to 0 at game start
const initialPositions = {};
// Use existing players from prev (already set by game:joined/game:state)
const existingPlayers = prev?.players || [];
// Initialize positions for all existing players
existingPlayers.forEach((player) => {
const playerName = player.playerName || player.name;
if (playerName) {
// Initialize ALL positions to 0 - only game:player-arrived will change them
initialPositions[playerName] = 0;
}
});
const updated = {
...prev,
currentPlayers: prev.currentPlayers.map(p =>
p.playerId === moveData.playerId
? { ...p, boardPosition: moveData.newPosition }
: p
),
gameCode: data.gameCode || prev?.gameCode,
boardData: data.boardData || prev?.boardData,
state: data.gamePhase || data.state || 'playing',
boardSize: data.boardSize || prev?.boardSize,
// Keep existing players list, initialize positions to 0
players: existingPlayers,
playerPositions: initialPositions,
// Preserve turn info and other data
turnInfo: prev?.turnInfo || { currentPlayer: null, currentPlayerName: null, turnNumber: 0 },
connectedPlayers: prev?.connectedPlayers || [],
readyPlayers: prev?.readyPlayers || []
};
return updated;
});
});
socket.on('game:turn-changed', (data) => {
log('🔄 Turn changed to:', data.currentPlayerName);
setGameState(prev => prev ? { ...prev, currentPlayer: data.currentPlayer } : prev);
// Turn changed means it's NOT my turn anymore
setIsMyTurnFlag(false);
// This is the ONLY place turnInfo should be set
setGameState(prev => ({
...prev,
turnInfo: {
currentPlayer: data.currentPlayer,
currentPlayerName: data.currentPlayerName,
turnNumber: data.turnNumber || prev?.turnInfo?.turnNumber || 0
}
}));
// Force a re-render by logging after state update
setTimeout(() => {
console.log('🔄 [game:turn-changed] State should be updated now');
}, 100);
});
socket.on('game:error', (err) => {
@@ -246,6 +438,199 @@ export const GameWebSocketProvider = ({ children }) => {
return prev;
});
});
socket.on('game:your-turn', (data) => {
log('🎯 Your turn!', data);
console.log('🎯 [game:your-turn] Received - enabling dice');
// Set flag to true - dice is now enabled
setIsMyTurnFlag(true);
// Emit custom event for GameScreen to display turn notification
window.dispatchEvent(new CustomEvent('game:your-turn', { detail: data }));
});
socket.on('game:dice-rolled', (data) => {
log('🎲 Dice rolled:', data.diceValue, 'by', data.playerName);
// Track the dice roll for this player
setPlayerDiceRolls(prev => ({
...prev,
[data.playerName]: data.diceValue
}));
window.dispatchEvent(new CustomEvent('game:dice-rolled', { detail: data }));
});
socket.on('game:player-moving', (data) => {
log('🚶 Player moving:', data.playerName, 'from', data.fromPosition, 'to', data.toPosition);
window.dispatchEvent(new CustomEvent('game:player-moving', { detail: data }));
});
socket.on('game:player-arrived', (data) => {
log('🎯 Player arrived:', data.playerName, 'at position', data.position, '(' + data.fieldType + ')');
// Update playerPositions in game object - THIS IS THE ONLY PLACE POSITIONS ARE MODIFIED
setGameState(prev => {
if (!prev) return prev;
const updatedPositions = { ...prev.playerPositions, [data.playerName]: data.position };
return {
...prev,
playerPositions: updatedPositions
};
});
window.dispatchEvent(new CustomEvent('game:player-arrived', { detail: data }));
});
socket.on('game:card-drawn', (data) => {
log('🃏 Card drawn:', data.cardType, 'by', data.playerName);
window.dispatchEvent(new CustomEvent('game:card-drawn', { detail: data }));
});
socket.on('game:card-result', (data) => {
log('🎴 Card result:', data.playerName, data.description);
window.dispatchEvent(new CustomEvent('game:card-result', { detail: data }));
});
socket.on('game:answer-timeout', (data) => {
log('⏰ Answer timeout:', data.playerName);
window.dispatchEvent(new CustomEvent('game:answer-timeout', { detail: data }));
});
socket.on('game:answer-result', (data) => {
log('📝 Answer result:', data.correct ? '✅ Correct' : '❌ Wrong', '-', data.playerName);
window.dispatchEvent(new CustomEvent('game:answer-result', { detail: data }));
});
socket.on('game:gamemaster-decision-request', (data) => {
log('👨‍⚖️ Gamemaster decision request:', data.playerName);
window.dispatchEvent(new CustomEvent('game:gamemaster-decision-request', { detail: data }));
});
socket.on('game:gamemaster-decision-result', (data) => {
log('⚖️ Gamemaster decision result:', data.decision, '-', data.playerName);
window.dispatchEvent(new CustomEvent('game:gamemaster-decision-result', { detail: data }));
});
socket.on('game:card-drawn-self', (data) => {
log('🃏 You drew a card:', data.cardType);
window.dispatchEvent(new CustomEvent('game:card-drawn-self', { detail: data }));
});
socket.on('game:joker-activated', (data) => {
log('🃏 Joker activated:', data.playerName);
window.dispatchEvent(new CustomEvent('game:joker-activated', { detail: data }));
});
socket.on('game:extra-turn', (data) => {
log('⭐ Extra turn:', data.playerName);
window.dispatchEvent(new CustomEvent('game:extra-turn', { detail: data }));
});
socket.on('game:turn-lost', (data) => {
log('😞 Turn lost:', data.playerName);
window.dispatchEvent(new CustomEvent('game:turn-lost', { detail: data }));
});
socket.on('game:no-movement', (data) => {
log('⛔ No movement:', data.playerName, '-', data.reason);
window.dispatchEvent(new CustomEvent('game:no-movement', { detail: data }));
});
socket.on('game:penalty-avoided', (data) => {
log('🛡️ Penalty avoided:', data.playerName);
window.dispatchEvent(new CustomEvent('game:penalty-avoided', { detail: data }));
});
socket.on('game:guess-timeout', (data) => {
log('⏰ Guess timeout:', data.playerName);
window.dispatchEvent(new CustomEvent('game:guess-timeout', { detail: data }));
});
socket.on('game:player-guessing', (data) => {
log('🤔 Player guessing:', data.playerName);
window.dispatchEvent(new CustomEvent('game:player-guessing', { detail: data }));
});
socket.on('game:secondary-landing', (data) => {
log('🎯 Secondary landing:', data.playerName, 'on', data.fieldType);
window.dispatchEvent(new CustomEvent('game:secondary-landing', { detail: data }));
});
socket.on('game:player-disconnected', (data) => {
log('⚠️ Player disconnected:', data.playerName);
setGameState(prev => {
if (!prev) return prev;
return {
...prev,
connectedPlayers: (prev.connectedPlayers || []).filter(p => p !== data.playerName)
};
});
window.dispatchEvent(new CustomEvent('game:player-disconnected', { detail: data }));
});
socket.on('game:player-disconnected-during-turn', (data) => {
log('⚠️ Player disconnected during turn:', data.playerName);
window.dispatchEvent(new CustomEvent('game:player-disconnected-during-turn', { detail: data }));
});
socket.on('game:player-disconnected-during-card', (data) => {
log('⚠️ Player disconnected during card:', data.playerName);
window.dispatchEvent(new CustomEvent('game:player-disconnected-during-card', { detail: data }));
});
socket.on('game:guess-result', (data) => {
log('🎯 Guess result:', data);
// Position update will come from game:player-arrived event
window.dispatchEvent(new CustomEvent('game:guess-result', { detail: data }));
});
socket.on('game:joker-complete', (data) => {
log('🃏 Joker complete:', data);
// Position update will come from game:player-arrived event
window.dispatchEvent(new CustomEvent('game:joker-complete', { detail: data }));
});
socket.on('game:luck-consequence', (data) => {
log('🍀 Luck consequence:', data);
// Position update will come from game:player-arrived event
window.dispatchEvent(new CustomEvent('game:luck-consequence', { detail: data }));
});
socket.on('game:ended', (data) => {
log('🏁 Game ended! Winner:', data.winner);
setGameState(prev => ({
...prev,
status: 'finished',
winner: data.winner,
finalScores: data.scores
}));
window.dispatchEvent(new CustomEvent('game:ended', { detail: data }));
// Don't auto-navigate - let the player close the winner modal manually
// They can use the "Vissza a főoldalra" button when ready
});
socket.on('game:extra-turn-remaining', (data) => {
log('⭐ Extra turn remaining:', data);
window.dispatchEvent(new CustomEvent('game:extra-turn-remaining', { detail: data }));
});
socket.on('game:players-skipped', (data) => {
log('⏭️ Players skipped:', data.skippedPlayers);
window.dispatchEvent(new CustomEvent('game:players-skipped', { detail: data }));
});
socket.on('game:cleanup-complete', (data) => {
log('🧹 Cleanup complete:', data);
window.dispatchEvent(new CustomEvent('game:cleanup-complete', { detail: data }));
// Navigate to home after cleanup
setTimeout(() => {
log('🏠 Navigating to home after cleanup');
window.location.href = '/';
}, 2000);
});
}, []);
/**
@@ -259,8 +644,7 @@ export const GameWebSocketProvider = ({ children }) => {
socketRef.current = null;
gameTokenRef.current = null;
setIsConnected(false);
setGameState(null);
setBoardData(null);
setGameState(null); // Clear entire game object
setError(null);
setIsGamemaster(false);
setGameStarted(false);
@@ -316,9 +700,79 @@ export const GameWebSocketProvider = ({ children }) => {
return true;
}, [isConnected, isGamemaster, gameState?.gameCode]);
const submitAnswer = useCallback((answer, cardId = null) => {
const socket = socketRef.current;
if (!socket || !isConnected) {
warn('⚠️ Cannot submit answer: not connected');
return false;
}
log('📝 Submitting answer:', answer, 'for card:', cardId);
socket.emit('game:card-answer', {
gameCode: gameState?.gameCode,
answer,
cardId
});
return true;
}, [isConnected, gameState?.gameCode]);
const submitPositionGuess = useCallback((guessedPosition) => {
const socket = socketRef.current;
if (!socket || !isConnected) {
warn('⚠️ Cannot submit position guess: not connected');
return false;
}
log('🎯 Submitting position guess:', guessedPosition);
socket.emit('game:position-guess', { gameCode: gameState?.gameCode, guessedPosition });
return true;
}, [isConnected, gameState?.gameCode]);
const submitJokerPositionGuess = useCallback((guessedPosition) => {
const socket = socketRef.current;
if (!socket || !isConnected) {
warn('⚠️ Cannot submit joker position guess: not connected');
return false;
}
log('🃏🎯 Submitting joker position guess:', guessedPosition);
socket.emit('game:joker-position-guess', { gameCode: gameState?.gameCode, guessedPosition });
return true;
}, [isConnected, gameState?.gameCode]);
const approveJoker = useCallback((requestId) => {
const socket = socketRef.current;
if (!socket || !isConnected || !isGamemaster) {
warn('⚠️ Cannot approve joker: not gamemaster or not connected');
return false;
}
log('✅ Approving joker request:', requestId);
socket.emit('game:gamemaster-decision', {
gameCode: gameState?.gameCode,
requestId,
decision: 'approve'
});
return true;
}, [isConnected, isGamemaster, gameState?.gameCode]);
const rejectJoker = useCallback((requestId, reason = 'Joker answer rejected') => {
const socket = socketRef.current;
if (!socket || !isConnected || !isGamemaster) {
warn('⚠️ Cannot reject joker: not gamemaster or not connected');
return false;
}
log('❌ Rejecting joker request:', requestId, 'Reason:', reason);
socket.emit('game:gamemaster-decision', {
gameCode: gameState?.gameCode,
requestId,
decision: 'reject',
reason
});
return true;
}, [isConnected, isGamemaster, gameState?.gameCode]);
const addEventListener = useCallback((event, handler) => {
const socket = socketRef.current;
if (socket) socket.on(event, handler);
if (socket) {
socket.on(event, handler);
}
}, []);
const removeEventListener = useCallback((event, handler) => {
@@ -329,15 +783,20 @@ export const GameWebSocketProvider = ({ children }) => {
const value = {
socket: socketRef.current,
isConnected,
gameState,
players,
boardData,
currentTurn,
gameState, // Single game object with all data
players, // Memoized from gameState.players
playerPositions, // Memoized from gameState.playerPositions (SINGLE SOURCE OF TRUTH for positions)
boardData, // Memoized from gameState.boardData
currentTurn, // Memoized from gameState.turnInfo.currentPlayer
currentTurnName, // Memoized from gameState.turnInfo.currentPlayerName
isMyTurn,
playerIdentifier,
error,
isGamemaster,
gameStarted,
pendingPlayers,
approvalStatus,
playerDiceRolls,
// Connection management
connect,
disconnect,
@@ -348,6 +807,11 @@ export const GameWebSocketProvider = ({ children }) => {
leaveGame,
approvePlayer,
rejectPlayer,
submitAnswer,
submitPositionGuess,
submitJokerPositionGuess,
approveJoker,
rejectJoker,
addEventListener,
removeEventListener,
};
@@ -68,14 +68,71 @@ export default function DeckCreator() {
2: 'organization'
}
// Process cards: convert type field from number to string
const processedCards = (deckData.cards || []).map(card => {
// A kártya type mezője a deck type-ját tükrözi (backend így küldi)
// Ezért a deck type alapján állítjuk be
return {
...card,
type: typeMapping[deckData.type] || 'QUESTION'
// Process cards: convert backend Card structure to frontend format
const processedCards = (deckData.cards || []).map((card, index) => {
const deckType = typeMapping[deckData.type] || 'QUESTION'
// Base card structure
const processedCard = {
id: card.id || Date.now() + index,
type: deckType,
text: card.text || ""
}
// For QUESTION deck, determine subType from CardType enum
if (deckType === 'QUESTION' && card.type !== undefined) {
const cardTypeMapping = {
0: 'quiz', // QUIZ
1: 'matching', // SENTENCE_PAIRING (editor uses 'matching')
2: 'text', // OWN_ANSWER
3: 'truefalse', // TRUE_FALSE
4: 'closer' // CLOSER
}
processedCard.subType = cardTypeMapping[card.type] || 'text'
// Parse answer based on CardType
if (card.type === 0 && Array.isArray(card.answer)) {
// QUIZ: answer is array of {answer, text, correct}
processedCard.options = card.answer.map(opt => opt.text || opt) // Extract text or use whole object
const correctOption = card.answer.find(opt => opt.correct)
processedCard.correctAnswer = card.answer.indexOf(correctOption)
} else if (card.type === 1 && Array.isArray(card.answer)) {
// SENTENCE_PAIRING: answer is array of {left, right}
// Convert to editor format: leftItems[], rightItems[], correctPairs{leftIdx: rightIdx}
processedCard.leftItems = card.answer.map(p => p.left)
processedCard.rightItems = card.answer.map(p => p.right)
// Create correctPairs mapping: each left item at index i maps to right item at index i
processedCard.correctPairs = card.answer.reduce((acc, _, idx) => {
acc[idx] = idx
return acc
}, {})
} else if (card.type === 2 && card.answer) {
// OWN_ANSWER: answer is array of acceptable strings
processedCard.acceptedAnswers = Array.isArray(card.answer) ? card.answer : [card.answer]
} else if (card.type === 3 && card.answer) {
// TRUE_FALSE: answer is "true" or "false"
const answerStr = String(card.answer).toLowerCase()
processedCard.isTrue = answerStr === "true" || answerStr === "1"
processedCard.correctAnswer = card.answer
} else if (card.type === 4 && typeof card.answer === 'object') {
// CLOSER: answer is {correct: number, percent: number}
processedCard.correctAnswer = card.answer.correct
processedCard.percent = card.answer.percent || 10
}
}
// For LUCK deck, include consequence
if (deckType === 'LUCK' && card.consequence) {
processedCard.consequence = card.consequence
}
// Copy question field if exists (for backward compatibility)
if (card.question) {
processedCard.question = card.question
}
console.log('Card loaded:', { backend: card, frontend: processedCard })
return processedCard
})
setDeck({
@@ -120,53 +177,103 @@ export default function DeckCreator() {
notifyWarning(`${invalidCardsCount} db nem megfelelő típusú kártya törölve a mentés előtt.`)
}
// Tisztítsuk meg a kártyákat - konvertáljuk a backend által várt formátumra
// Tisztítsük meg a kártyákat - konvertáljuk a backend által várt formátumra
// Backend Card interface: { text: string, type?: CardType, answer?: any, consequence?: Consequence }
const cleanedCards = validCards.map(card => {
// Card subType mapping to backend CardType enum
const cardTypeMapping = {
'quiz': 0, // QUIZ
'pairing': 1, // SENTENCE_PAIRING
'matching': 1, // SENTENCE_PAIRING (editor uses 'matching')
'text': 2, // OWN_ANSWER
'truefalse': 3, // TRUE_FALSE
'closer': 4 // CLOSER
}
// Kezdjük az ID-val (ha van)
const cleanedCard = {}
if (card.id) {
cleanedCard.id = card.id
}
// TEXT field - required (question text)
cleanedCard.text = card.text || ""
// Ha van subType (QUESTION típusú kártyáknál), akkor add hozzá a type mezőt
// TYPE field - CardType enum (0-4) for QUESTION cards only
if (card.subType && cardTypeMapping[card.subType] !== undefined) {
cleanedCard.type = cardTypeMapping[card.subType]
}
// Text mező - kötelező, különböző forrásokból jöhet
cleanedCard.text = card.text || card.question || card.statement || ""
// Egyéb frontend mezők, amiket a backend is elfogad
if (card.question !== undefined) cleanedCard.question = card.question
if (card.statement !== undefined) cleanedCard.statement = card.statement
if (card.options !== undefined) cleanedCard.options = card.options
if (card.correctAnswer !== undefined) cleanedCard.correctAnswer = card.correctAnswer
if (card.leftItems !== undefined) cleanedCard.leftItems = card.leftItems
if (card.rightItems !== undefined) cleanedCard.rightItems = card.rightItems
if (card.correctPairs !== undefined) cleanedCard.correctPairs = card.correctPairs
if (card.acceptedAnswers !== undefined) cleanedCard.acceptedAnswers = card.acceptedAnswers
if (card.hint !== undefined) cleanedCard.hint = card.hint
// Answer mező (ha van)
if (card.answer !== undefined && card.answer !== null) {
// ANSWER field - structure depends on CardType
if (card.subType === 'quiz' && card.options) {
// QUIZ (type 0): answer = array of { answer: "A", text: "...", correct: boolean }
// TaskCardEditor stores options as strings, need to convert to object format
cleanedCard.answer = card.options.map((opt, idx) => {
const letter = String.fromCharCode(65 + idx) // A, B, C, D
// If option is a string, convert it
if (typeof opt === 'string') {
return {
answer: letter,
text: opt,
correct: card.correctAnswer === idx
}
}
// If option is already an object, use its values
return {
answer: opt.answer || letter,
text: opt.text || opt.label || "",
correct: opt.correct || opt.answer === card.correctAnswer || false
}
})
} else if (card.subType === 'matching' && (card.correctPairs || card.leftItems || card.rightItems)) {
// SENTENCE_PAIRING (type 1): answer = array of { left: "...", right: "..." }
// TaskCardEditor stores: leftItems[], rightItems[], correctPairs{leftIdx: rightIdx}
// Backend expects: array of {left, right} pairs
if (Array.isArray(card.correctPairs)) {
// Already in correct format (backward compatibility)
cleanedCard.answer = card.correctPairs.map(pair => ({
left: pair.left || pair.leftText || "",
right: pair.right || pair.rightText || ""
}))
} else if (card.leftItems && card.rightItems && typeof card.correctPairs === 'object') {
// Convert from editor format: {leftIdx: rightIdx} -> [{left, right}]
cleanedCard.answer = Object.entries(card.correctPairs || {}).map(([leftIdx, rightIdx]) => ({
left: card.leftItems[parseInt(leftIdx)] || "",
right: card.rightItems[parseInt(rightIdx)] || ""
}))
} else {
cleanedCard.answer = []
}
} else if (card.subType === 'text' && card.acceptedAnswers) {
// OWN_ANSWER (type 2): answer = array of acceptable answer strings
cleanedCard.answer = Array.isArray(card.acceptedAnswers)
? card.acceptedAnswers
: [card.acceptedAnswers]
} else if (card.subType === 'truefalse') {
// TRUE_FALSE (type 3): answer = "true" or "false"
// Use isTrue boolean field from editor, fallback to correctAnswer or answer
if (card.isTrue !== undefined) {
cleanedCard.answer = card.isTrue ? "true" : "false"
} else if (card.correctAnswer !== undefined) {
cleanedCard.answer = String(card.correctAnswer)
} else if (card.answer !== undefined) {
cleanedCard.answer = String(card.answer)
} else {
cleanedCard.answer = "true" // default
}
} else if (card.subType === 'closer' && card.correctAnswer !== undefined) {
// CLOSER (type 4): answer = { correct: number, percent: number }
cleanedCard.answer = {
correct: Number(card.correctAnswer),
percent: Number(card.percent || 10)
}
} else if (card.answer !== undefined && card.answer !== null) {
// Fallback: use existing answer field
cleanedCard.answer = card.answer
}
// Csak LUCK típusú kártyáknál add hozzá a consequence-t
// CONSEQUENCE field - only for LUCK cards
if (deck.type === 'LUCK' && card.consequence) {
cleanedCard.consequence = card.consequence
}
console.log('Card mapping:', { original: card, cleaned: cleanedCard })
return cleanedCard
})
@@ -43,7 +43,6 @@ const Card_display = () => {
const result = await getDeckById(deckId)
if (!mounted) return
console.log('Loaded deck:', result)
setDeck(result)
// Parse cards from JSON if it's a string
@@ -60,8 +59,6 @@ const Card_display = () => {
}
}
console.log('Parsed cards:', parsedCards)
console.log('First card structure:', parsedCards[0])
setCards(parsedCards)
} catch (err) {
console.error('Failed to load deck', err)
@@ -79,8 +76,8 @@ const Card_display = () => {
let filteredCards = cards.filter((card) => {
if (!search) return true
const searchLower = search.toLowerCase()
// Check question, statement, and options
const questionText = card.question || card.statement || ''
// Check text field (or fallback to question/statement for old data)
const questionText = card.text || card.question || card.statement || ''
const questionMatch = questionText.toLowerCase().includes(searchLower)
const answersMatch = Array.isArray(card.options)
? card.options.some(opt => opt && opt.toLowerCase().includes(searchLower))
@@ -96,12 +93,12 @@ const Card_display = () => {
// Keep original order
return 0
} else if (sortBy === "question-asc") {
const aText = a.question || a.statement || ''
const bText = b.question || b.statement || ''
const aText = a.text || a.question || a.statement || ''
const bText = b.text || b.question || b.statement || ''
return aText.localeCompare(bText)
} else if (sortBy === "question-desc") {
const aText = a.question || a.statement || ''
const bText = b.question || b.statement || ''
const aText = a.text || a.question || a.statement || ''
const bText = b.text || b.question || b.statement || ''
return bText.localeCompare(aText)
} else if (sortBy === "answers-asc") {
const aCount = Array.isArray(a.options) ? a.options.length : Array.isArray(a.answers) ? a.answers.length : 0
@@ -136,33 +133,26 @@ const Card_display = () => {
// Card subtype Hungarian labels - UPDATED based on actual data
const cardSubTypeLabels = {
// String types (from DeckCreator)
"quiz": "Quiz ",
"truefalse": "Igaz/Hamis",
"multiplechoice": "Feleletválasztós",
"text": "Szöveges válasz",
"number": "Számos válasz",
"order": "Sorbarendezés",
"matching": "Párosítás",
"fill": "Kiegészítés",
"QUESTION": "Kérdés",
"LUCK": "Szerencse",
"JOKER": "Joker",
"joker": "Joker",
"luck": "Szerencse",
// If backend converts to different numbers, map them:
"0": "Igaz/Hamis", // truefalse = 0
"1": "Feleletválasztós", // multiplechoice = 1
"2": "Szöveges válasz", // text = 2
"3": "Igaz/Hamis", // type 3 = truefalse (alternate encoding)
"4": "Sorbarendezés", // order = 4
"5": "Párosítás", // matching = 5
"6": "Kiegészítés", // fill = 6
0: "Igaz/Hamis",
1: "Feleletválasztós",
// Backend CardType enum (numeric):
"0": "Quiz", // CardType.QUIZ = 0
"1": "Párosítás", // CardType.SENTENCE_PAIRING = 1
"2": "Szöveges válasz", // CardType.OWN_ANSWER = 2
"3": "Igaz/Hamis", // CardType.TRUE_FALSE = 3
"4": "Közelítés", // CardType.CLOSER = 4
0: "Quiz",
1: "Párosítás",
2: "Szöveges válasz",
3: "Igaz/Hamis", // type 3 detected
4: "Sorbarendezés",
5: "Párosítás",
6: "Kiegészítés"
3: "Igaz/Hamis",
4: "Közelítés"
}
const currentDeckType = deck ? (deckTypes[deck.type] || { label: "Ismeretlen", color: "var(--color-success)" }) : null
@@ -365,48 +355,51 @@ const Card_display = () => {
let answerOptions = []
let correctAnswerIndex = card.correctAnswer
// Normalize subType (can be string or number or undefined)
const subType = card.subType ? String(card.subType).toLowerCase() : 'undefined'
// Detect card type - prioritize numeric card.type over subType
let detectedType = 'undefined'
// Detect card type by fields if subType is missing
let detectedType = subType
if (subType === 'undefined' || subType === 'null') {
// First check deck type - if deck is JOKER or LUCK type, cards inherit that
if (deck.type === 1) {
// Deck type 1 = Joker deck
// First check deck type - if deck is JOKER or LUCK type, cards inherit that
if (deck.type === 1) {
// Deck type 1 = Joker deck
detectedType = 'joker'
} else if (deck.type === 0) {
// Deck type 0 = Luck deck
detectedType = 'luck'
} else if (card.type !== undefined && card.type !== null) {
// Check by card.type field (numeric CardType enum)
const cardType = typeof card.type === 'string' ? card.type.toLowerCase() : card.type
if (cardType === 'joker' || card.type === 'JOKER') {
detectedType = 'joker'
} else if (deck.type === 0) {
// Deck type 0 = Luck deck
} else if (cardType === 'luck' || card.type === 'LUCK') {
detectedType = 'luck'
} else if (card.type !== undefined) {
// Check by card.type field (string or numeric)
const cardType = typeof card.type === 'string' ? card.type.toLowerCase() : card.type
if (cardType === 'joker' || card.type === 'JOKER') {
// Joker card
detectedType = 'joker'
} else if (cardType === 'luck' || card.type === 'LUCK') {
// Luck card
detectedType = 'luck'
} else if (card.type === 3) {
// type 3 = True/False
detectedType = 'truefalse'
} else if (card.type === 2) {
// type 2 = Text answer
detectedType = 'text'
}
} else if (card.leftItems && card.rightItems && card.correctPairs) {
// Has leftItems, rightItems AND correctPairs = matching
} else if (card.type === 0) {
// type 0 = Quiz (multiple choice)
detectedType = 'quiz'
} else if (card.type === 1) {
// type 1 = Matching/Pairing
detectedType = 'matching'
} else if (card.acceptedAnswers && card.acceptedAnswers.length > 0 && card.acceptedAnswers[0] && card.acceptedAnswers[0].trim()) {
// Only detect as text if acceptedAnswers has non-empty values
} else if (card.type === 2) {
// type 2 = Text answer
detectedType = 'text'
} else if (card.isTrue !== undefined) {
} else if (card.type === 3) {
// type 3 = True/False
detectedType = 'truefalse'
} else if (card.options && Array.isArray(card.options) && card.options.some(opt => opt && opt.trim())) {
// Has non-empty options - must be multiple choice
detectedType = 'multiplechoice'
}
} else if (card.subType) {
// Fallback to subType if card.type is missing
detectedType = String(card.subType).toLowerCase()
} else if (card.leftItems && card.rightItems && card.correctPairs) {
// Has leftItems, rightItems AND correctPairs = matching
detectedType = 'matching'
} else if (card.acceptedAnswers && card.acceptedAnswers.length > 0 && card.acceptedAnswers[0] && card.acceptedAnswers[0].trim()) {
// Only detect as text if acceptedAnswers has non-empty values
detectedType = 'text'
} else if (card.isTrue !== undefined) {
detectedType = 'truefalse'
} else if (card.options && Array.isArray(card.options) && card.options.some(opt => opt && opt.trim())) {
// Has non-empty options - must be multiple choice
detectedType = 'multiplechoice'
}
// Extract consequence info for JOKER and LUCK cards
@@ -431,19 +424,40 @@ const Card_display = () => {
}
}
if (detectedType === 'truefalse' || detectedType === '0') {
if (detectedType === 'truefalse' || detectedType === '3') {
// True/False cards
answerOptions = ['Igaz', 'Hamis']
// correctAnswer: 0 = Igaz, 1 = Hamis (based on user feedback)
correctAnswerIndex = card.correctAnswer !== undefined ? card.correctAnswer : (card.isTrue ? 0 : 1)
} else if ((detectedType === 'text' || detectedType === '2') && card.acceptedAnswers && Array.isArray(card.acceptedAnswers)) {
// Text-based cards with accepted answers
answerOptions = card.acceptedAnswers
correctAnswerIndex = -1 // All accepted answers are correct
} else if (detectedType === 'matching' || detectedType === '5') {
// Matching cards - pairs
if (card.leftItems && card.rightItems && card.correctPairs) {
// Build pairs from correctPairs object
// Parse answer from various sources
let isCorrectTrue = false
if (card.isTrue !== undefined) {
isCorrectTrue = card.isTrue
} else if (card.answer !== undefined) {
const answerStr = String(card.answer).toLowerCase()
isCorrectTrue = answerStr === 'true' || answerStr === '1' || answerStr === 'igaz'
} else if (card.correctAnswer !== undefined) {
isCorrectTrue = card.correctAnswer === 0 || card.correctAnswer === true || card.correctAnswer === 'true'
}
correctAnswerIndex = isCorrectTrue ? 0 : 1
} else if (detectedType === 'quiz' || detectedType === 'multiplechoice' || detectedType === '0') {
// Quiz/Multiple choice - parse from backend answer array or frontend options
if (card.answer && Array.isArray(card.answer) && card.answer.length > 0 && typeof card.answer[0] === 'object') {
// Backend format: [{answer: "A", text: "...", correct: boolean}]
answerOptions = card.answer.map(opt => opt.text || opt.label || '')
const correctOption = card.answer.find(opt => opt.correct)
correctAnswerIndex = card.answer.indexOf(correctOption)
} else if (card.options && Array.isArray(card.options)) {
// Frontend format: ["option1", "option2"]
answerOptions = card.options.filter(opt => opt && opt.trim())
correctAnswerIndex = card.correctAnswer
}
} else if (detectedType === 'matching' || detectedType === '1') {
// Matching cards - parse from backend answer array or frontend fields
if (card.answer && Array.isArray(card.answer) && card.answer.length > 0 && typeof card.answer[0] === 'object') {
// Backend format: [{left: "...", right: "..."}]
answerOptions = card.answer.map(pair => `${pair.left}${pair.right}`)
correctAnswerIndex = -1 // All pairs are correct
} else if (card.leftItems && card.rightItems && card.correctPairs) {
// Frontend format with leftItems, rightItems, correctPairs
const pairs = []
for (const [leftIdx, rightIdx] of Object.entries(card.correctPairs)) {
const left = card.leftItems[parseInt(leftIdx)]
@@ -455,10 +469,17 @@ const Card_display = () => {
answerOptions = pairs
correctAnswerIndex = -1 // All pairs are correct
}
} else if ((detectedType === 'multiplechoice' || detectedType === '1') && card.options && Array.isArray(card.options)) {
// Multiple choice - filter out empty options
answerOptions = card.options.filter(opt => opt && opt.trim())
correctAnswerIndex = card.correctAnswer
} else if (detectedType === 'text' || detectedType === '2') {
// OWN_ANSWER - parse from backend answer array or frontend acceptedAnswers
if (card.answer) {
// Backend format: ["answer1", "answer2"] or single value
answerOptions = Array.isArray(card.answer) ? card.answer : [card.answer]
correctAnswerIndex = -1 // All answers are correct
} else if (card.acceptedAnswers && Array.isArray(card.acceptedAnswers)) {
// Frontend format
answerOptions = card.acceptedAnswers
correctAnswerIndex = -1 // All accepted answers are correct
}
} else if (card.options && Array.isArray(card.options)) {
// Other types with options
answerOptions = card.options.filter(opt => opt && opt.trim())
@@ -682,12 +703,26 @@ const Card_display = () => {
<div className="text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Helyes válasz:
</div>
{detectedType === 'truefalse' || detectedType === '0' ? (
{detectedType === 'truefalse' || detectedType === '3' ? (
// True/False - show only the correct answer
<div className="text-[color:var(--color-text)] text-lg font-bold bg-[color:var(--color-success)]/20 rounded-lg px-4 py-3 border-l-4 border-[color:var(--color-success)]">
{card.isTrue ? 'Igaz' : 'Hamis'}
</div>
) : detectedType === 'matching' || detectedType === '5' ? (
(() => {
// Determine if correct answer is true
let isCorrectTrue = false
if (card.isTrue !== undefined) {
isCorrectTrue = card.isTrue
} else if (card.answer !== undefined) {
const answerStr = String(card.answer).toLowerCase()
isCorrectTrue = answerStr === 'true' || answerStr === '1' || answerStr === 'igaz'
} else if (card.correctAnswer !== undefined) {
isCorrectTrue = card.correctAnswer === 0 || card.correctAnswer === true || card.correctAnswer === 'true'
}
return (
<div className="text-[color:var(--color-text)] text-lg font-bold bg-[color:var(--color-success)]/20 rounded-lg px-4 py-3 border-l-4 border-[color:var(--color-success)]">
{isCorrectTrue ? 'Igaz' : 'Hamis'}
</div>
)
})()
) : detectedType === 'matching' || detectedType === '1' ? (
// Matching - show all correct pairs
<ul className="space-y-2">
{answerOptions.map((pair, idx) => (
@@ -699,7 +734,7 @@ const Card_display = () => {
</li>
))}
</ul>
) : (detectedType === 'text' || detectedType === '2') && card.acceptedAnswers && Array.isArray(card.acceptedAnswers) ? (
) : (detectedType === 'text' || detectedType === '2') && answerOptions.length > 0 ? (
// Text answers - show all accepted answers
<ul className="space-y-1">
{answerOptions.map((answer, ansIdx) => (
@@ -711,8 +746,24 @@ const Card_display = () => {
</li>
))}
</ul>
) : (detectedType === 'quiz' || detectedType === 'multiplechoice' || detectedType === '0') && answerOptions.length > 0 ? (
// Quiz/Multiple choice - show all options with correct one highlighted
<ul className="space-y-2">
{answerOptions.map((option, ansIdx) => (
<li
key={ansIdx}
className={`text-[color:var(--color-text)] text-sm rounded-lg px-3 py-2 border-l-2 font-semibold ${
ansIdx === correctAnswerIndex
? 'bg-[color:var(--color-success)]/20 border-[color:var(--color-success)]'
: 'bg-[color:var(--color-surface)] border-[color:var(--color-surface-selected)]'
}`}
>
{ansIdx === correctAnswerIndex ? '✓ ' : ''}{String.fromCharCode(65 + ansIdx)}. {option}
</li>
))}
</ul>
) : (
// Multiple choice - show only the correct answer
// Other types - show only the correct answer
correctAnswerIndex !== undefined && correctAnswerIndex !== -1 && answerOptions[correctAnswerIndex] ? (
<div className="text-[color:var(--color-text)] text-lg font-bold bg-[color:var(--color-success)]/20 rounded-lg px-4 py-3 border-l-4 border-[color:var(--color-success)]">
{answerOptions[correctAnswerIndex]}
@@ -1,5 +1,43 @@
import React, { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { CardType } from "../../constants/CardTypes"
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
import { arrayMove, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
/**
* Draggable item for sentence pairing
*/
const DraggableItem = ({ id, text, disabled }) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id, disabled })
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
}
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={`bg-gray-800 border-2 border-purple-500 rounded-lg p-3 text-white ${
disabled ? 'cursor-default' : 'cursor-grab active:cursor-grabbing'
} ${isDragging ? 'shadow-lg' : ''}`}
>
{text}
</div>
)
}
/**
* CardDisplayModal - Kártya megjelenítése a játékos számára
@@ -11,6 +49,8 @@ import { motion, AnimatePresence } from "framer-motion"
* @param {string} props.cardType - Kártya típusa (QUESTION, LUCK, JOKER)
* @param {Function} props.onSubmitAnswer - Válasz beküldése (csak QUESTION típusnál)
* @param {number} props.timeLimit - Időkorlát másodpercben (default: 60)
* @param {boolean} props.isMyTurn - Whether this is the active player (can answer) or spectator (read-only)
* @param {string} props.submittedAnswer - For spectators, shows what the active player answered
*/
const CardDisplayModal = ({
isOpen,
@@ -18,16 +58,22 @@ const CardDisplayModal = ({
card,
cardType = "QUESTION",
onSubmitAnswer,
timeLimit = 60
timeLimit = 60,
isMyTurn = true,
submittedAnswer = null
}) => {
const [playerAnswer, setPlayerAnswer] = useState("")
const [selectedOption, setSelectedOption] = useState(null)
const [timeLeft, setTimeLeft] = useState(timeLimit)
const [isProcessing, setIsProcessing] = useState(false)
// For sentence pairing drag and drop
const [rightItems, setRightItems] = useState([])
const sensors = useSensors(useSensor(PointerSensor))
// Timer countdown
// Timer countdown (only for active player)
useEffect(() => {
if (!isOpen || cardType !== "QUESTION") return
if (!isOpen || cardType !== "QUESTION" || !isMyTurn) return
setTimeLeft(timeLimit)
const timer = setInterval(() => {
@@ -42,7 +88,7 @@ const CardDisplayModal = ({
}, 1000)
return () => clearInterval(timer)
}, [isOpen, timeLimit])
}, [isOpen, timeLimit, isMyTurn])
// Reset state when modal opens
useEffect(() => {
@@ -50,8 +96,13 @@ const CardDisplayModal = ({
setPlayerAnswer("")
setSelectedOption(null)
setIsProcessing(false)
// Initialize sentence pairing right items
if (card?.sentencePairs && card.sentencePairs.length > 0) {
setRightItems(card.sentencePairs.map(p => ({ id: p.id, text: p.right })))
}
}
}, [isOpen])
}, [isOpen, card])
const handleTimeout = () => {
if (onSubmitAnswer) {
@@ -60,27 +111,49 @@ const CardDisplayModal = ({
}
const handleSubmit = async () => {
if (isProcessing) return
if (isProcessing || !isMyTurn) return
let answer = null
// Quiz típus - A, B, C, D
if (card?.type === 0 || card?.answerOptions) {
// Different answer formats based on card type
if (card?.type === CardType.SENTENCE_PAIRING && card?.sentencePairs) {
// Answer is array of pairs
answer = card.sentencePairs.map((leftPair, index) => ({
pairId: leftPair.id,
leftText: leftPair.left,
rightText: rightItems[index].text
}))
} else if (selectedOption !== null) {
answer = selectedOption
}
// Szöveges válasz
else {
} else {
answer = playerAnswer.trim()
}
if (!answer) return
if (!answer || (Array.isArray(answer) && answer.length === 0)) {
console.warn('⚠️ No answer provided')
return
}
console.log('📝 Submitting answer:', answer)
setIsProcessing(true)
try {
await onSubmitAnswer(answer)
} catch (error) {
console.error("Válasz küldési hiba:", error)
console.error("Válasz küldési hiba:", error)
setIsProcessing(false)
}
}
const handleDragEnd = (event) => {
const { active, over } = event
if (active.id !== over.id) {
setRightItems((items) => {
const oldIndex = items.findIndex(item => item.id === active.id)
const newIndex = items.findIndex(item => item.id === over.id)
return arrayMove(items, oldIndex, newIndex)
})
}
}
@@ -97,7 +170,7 @@ const CardDisplayModal = ({
switch (cardType) {
case "QUESTION": return "Feladat Kártya"
case "LUCK": return "Szerencse Kártya"
case "JOKER": return "Joker Kártya"
case "JOKER": return "Joker Kártya Feladat"
default: return "Kártya"
}
}
@@ -143,7 +216,7 @@ const CardDisplayModal = ({
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.9, opacity: 0, y: 20 }}
transition={{ type: "spring", duration: 0.5 }}
className="relative bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 rounded-2xl shadow-2xl border-2 border-purple-500/30 max-w-2xl w-full overflow-hidden"
className="relative bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 rounded-2xl shadow-2xl border-2 border-purple-500/30 max-w-2xl w-full overflow-hidden max-h-[90vh] overflow-y-auto"
>
{/* Header */}
<div className={`bg-gradient-to-r ${getCardBgGradient()} p-6 relative overflow-hidden`}>
@@ -154,13 +227,20 @@ const CardDisplayModal = ({
<div>
<h2 className="text-2xl font-bold text-white">{getCardTitle()}</h2>
{cardType === "QUESTION" && (
<p className="text-white/80 text-sm">Válaszolj a kérdésre!</p>
<p className="text-white/80 text-sm">
{isMyTurn ? "Válaszolj a kérdésre!" : "Néző mód - Várakozás a játékosra"}
</p>
)}
{cardType === "JOKER" && (
<p className="text-purple-100 text-sm">
{isMyTurn ? "Gamemaster jóváhagyás szükséges" : "Várakozás a Gamemaster döntésére"}
</p>
)}
</div>
</div>
{/* Timer - csak QUESTION típusnál */}
{cardType === "QUESTION" && (
{/* Timer - csak QUESTION típusnál és aktív játékosnál */}
{cardType === "QUESTION" && isMyTurn && (
<div className="bg-black/30 rounded-lg px-4 py-2">
<div className={`text-2xl font-bold ${getTimeColor()}`}>
{formatTime(timeLeft)}
@@ -172,57 +252,261 @@ const CardDisplayModal = ({
{/* Content */}
<div className="p-6 space-y-6">
{/* Question/Text */}
<div className="bg-gray-800/50 rounded-xl p-5 border border-gray-700">
<div className="flex items-start gap-3">
<div className="text-3xl">📝</div>
<div className="flex-1">
<p className="text-white text-lg leading-relaxed">
{card.question || card.text || card.statement}
</p>
{/* JOKER CARD SPECIFIC LAYOUT */}
{cardType === "JOKER" ? (
<>
{/* Player Info */}
<div className="bg-gray-800/50 rounded-xl p-4 border border-gray-700">
<div className="flex items-center gap-3">
<div className="text-4xl">{card.playerEmoji || "🎭"}</div>
<div>
<p className="text-gray-400 text-sm">Játékos</p>
<p className="text-white font-semibold text-lg">{card.playerName}</p>
</div>
</div>
</div>
</div>
</div>
{/* Answer Options - Quiz típus (type: 0) */}
{cardType === "QUESTION" && (card.type === 0 || card.answerOptions) && (
{/* Joker Card Details */}
<div className="bg-gradient-to-br from-purple-900/30 to-pink-900/30 rounded-xl p-5 border border-purple-500/30">
<div className="flex items-start gap-3 mb-3">
<div className="text-3xl">🎯</div>
<div className="flex-1">
<h3 className="text-purple-300 font-semibold mb-2">Feladat címe</h3>
<p className="text-white text-lg font-medium">
{card.question || "Joker Kártya Feladat"}
</p>
</div>
</div>
{card.consequence && (
<div className="flex items-start gap-3">
<div className="text-2xl">📝</div>
<div className="flex-1">
<h3 className="text-purple-300 font-semibold mb-2">Feladat leírása</h3>
<p className="text-gray-300 leading-relaxed">
{card.consequence}
</p>
</div>
</div>
)}
{/* Points Info (if available) */}
{card.points && (
<div className="mt-4 pt-4 border-t border-purple-500/20">
<div className="flex items-center gap-2">
<span className="text-2xl"></span>
<span className="text-yellow-400 font-bold text-lg">
{card.points} pont
</span>
<span className="text-gray-400 text-sm">járható érte</span>
</div>
</div>
)}
</div>
{/* Waiting for gamemaster message */}
{card.waitingForGamemaster && (
<div className="bg-yellow-900/20 rounded-xl p-4 border border-yellow-500/30">
<div className="flex items-start gap-3">
<div className="text-2xl"></div>
<div className="flex-1">
<p className="text-yellow-200 text-sm">
<strong>Várakozás:</strong> A Gamemaster döntése szükséges a folytatáshoz.
</p>
</div>
</div>
</div>
)}
</>
) : (
<>
{/* REGULAR CARD LAYOUT */}
{/* Spectator Notice */}
{!isMyTurn && (
<div className="bg-blue-900/30 rounded-xl p-4 border border-blue-500/50 text-center">
<p className="text-blue-300 font-semibold">👀 Néző módban vagy</p>
<p className="text-gray-300 text-sm mt-1">Várakozás a játékos válaszára...</p>
</div>
)}
{/* Question/Text */}
<div className="bg-gray-800/50 rounded-xl p-5 border border-gray-700">
<div className="flex items-start gap-3">
<div className="text-3xl">📝</div>
<div className="flex-1">
<p className="text-white text-lg leading-relaxed">
{card.question || card.text || card.statement}
</p>
</div>
</div>
</div>
</>
)}
{/* QUIZ TYPE - Four Buttons (A, B, C, D) */}
{cardType === "QUESTION" && card.type === CardType.QUIZ && card.answerOptions && card.answerOptions.length > 0 && (
<div className="space-y-3">
<h3 className="text-purple-300 font-semibold">Válaszd ki a helyes választ:</h3>
{card.answerOptions?.map((option, index) => (
<button
key={index}
onClick={() => setSelectedOption(option.answer)}
disabled={isProcessing}
className={`w-full text-left p-4 rounded-lg border-2 transition-all ${
selectedOption === option.answer
? "bg-purple-600 border-purple-400 text-white"
: "bg-gray-800 border-gray-600 text-gray-300 hover:border-purple-500"
} ${isProcessing ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
>
<span className="font-bold">{option.answer})</span> {option.text}
</button>
))}
<h3 className="text-purple-300 font-semibold">
{isMyTurn ? "Válaszd ki a helyes választ:" : "Válasz lehetőségek:"}
</h3>
<div className="grid grid-cols-1 gap-3">
{card.answerOptions.map((option, index) => (
<button
key={index}
onClick={() => isMyTurn && setSelectedOption(option.answer)}
disabled={!isMyTurn || isProcessing}
className={`w-full text-left p-4 rounded-lg border-2 transition-all ${
selectedOption === option.answer
? "bg-purple-600 border-purple-400 text-white scale-105"
: submittedAnswer === option.answer
? "bg-blue-600 border-blue-400 text-white"
: "bg-gray-800 border-gray-600 text-gray-300"
} ${isMyTurn && !isProcessing ? "hover:border-purple-500 cursor-pointer" : "cursor-default"} ${
!isMyTurn ? "opacity-75" : ""
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl font-bold bg-gray-900/50 rounded-full w-10 h-10 flex items-center justify-center">
{option.answer}
</span>
<span className="flex-1">{option.text}</span>
</div>
</button>
))}
</div>
{!isMyTurn && submittedAnswer && (
<p className="text-blue-300 text-center text-sm">
A játékos választása: <span className="font-bold">{submittedAnswer}</span>
</p>
)}
</div>
)}
{/* Text Input - egyéb kérdés típusok */}
{cardType === "QUESTION" && card.type !== 0 && !card.answerOptions && (
{/* TRUE/FALSE TYPE - Two Buttons */}
{cardType === "QUESTION" && card.type === CardType.TRUE_FALSE && (
<div className="space-y-3">
<h3 className="text-purple-300 font-semibold">Írd be a választ:</h3>
<h3 className="text-purple-300 font-semibold text-center">
{isMyTurn ? "Igaz vagy Hamis?" : "Válasz lehetőségek:"}
</h3>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => isMyTurn && setSelectedOption("Igaz")}
disabled={!isMyTurn || isProcessing}
className={`p-6 rounded-xl border-2 transition-all ${
selectedOption === "Igaz"
? "bg-green-600 border-green-400 text-white scale-105"
: submittedAnswer === "Igaz"
? "bg-blue-600 border-blue-400 text-white"
: "bg-gray-800 border-gray-600 text-gray-300"
} ${isMyTurn && !isProcessing ? "hover:border-green-500 cursor-pointer" : "cursor-default"}`}
>
<div className="text-center">
<div className="text-4xl mb-2"></div>
<div className="text-xl font-bold">IGAZ</div>
</div>
</button>
<button
onClick={() => isMyTurn && setSelectedOption("Hamis")}
disabled={!isMyTurn || isProcessing}
className={`p-6 rounded-xl border-2 transition-all ${
selectedOption === "Hamis"
? "bg-red-600 border-red-400 text-white scale-105"
: submittedAnswer === "Hamis"
? "bg-blue-600 border-blue-400 text-white"
: "bg-gray-800 border-gray-600 text-gray-300"
} ${isMyTurn && !isProcessing ? "hover:border-red-500 cursor-pointer" : "cursor-default"}`}
>
<div className="text-center">
<div className="text-4xl mb-2"></div>
<div className="text-xl font-bold">HAMIS</div>
</div>
</button>
</div>
{!isMyTurn && submittedAnswer && (
<p className="text-blue-300 text-center text-sm">
A játékos választása: <span className="font-bold">{submittedAnswer}</span>
</p>
)}
</div>
)}
{/* SENTENCE_PAIRING TYPE - Drag and Drop */}
{cardType === "QUESTION" && card.type === CardType.SENTENCE_PAIRING && card.sentencePairs && card.sentencePairs.length > 0 && (
<div className="space-y-4">
<h3 className="text-purple-300 font-semibold text-center">
{isMyTurn ? "Párosítsd a mondatokat! (Húzd a jobb oldali elemeket)" : "Mondatpárosítás:"}
</h3>
<div className="grid grid-cols-2 gap-4">
{/* Left column - fixed */}
<div className="space-y-3">
<p className="text-gray-400 text-sm text-center font-semibold">Bal oldal</p>
{card.sentencePairs.map((pair, index) => (
<div
key={`left-${pair.id}`}
className="bg-gray-800 border-2 border-blue-500 rounded-lg p-3 text-white"
>
{pair.left}
</div>
))}
</div>
{/* Right column - draggable */}
<div className="space-y-3">
<p className="text-gray-400 text-sm text-center font-semibold">
{isMyTurn ? "Jobb oldal (húzható)" : "Jobb oldal"}
</p>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={rightItems.map(item => item.id)}
strategy={verticalListSortingStrategy}
disabled={!isMyTurn}
>
{rightItems.map((item) => (
<DraggableItem
key={item.id}
id={item.id}
text={item.text}
disabled={!isMyTurn}
/>
))}
</SortableContext>
</DndContext>
</div>
</div>
{!isMyTurn && (
<p className="text-blue-300 text-center text-sm">
Várakozás a játékos párosítására...
</p>
)}
</div>
)}
{/* OWN_ANSWER and CLOSER - Text Input */}
{cardType === "QUESTION" && (card.type === CardType.OWN_ANSWER || card.type === CardType.CLOSER) && (
<div className="space-y-3">
<h3 className="text-purple-300 font-semibold">
{isMyTurn ? "Írd be a választ:" : "A játékos válasza:"}
</h3>
<input
type="text"
value={playerAnswer}
onChange={(e) => setPlayerAnswer(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSubmit()}
disabled={isProcessing}
placeholder="Válaszod..."
className="w-full bg-gray-800 border-2 border-gray-600 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none disabled:opacity-50"
value={isMyTurn ? playerAnswer : submittedAnswer || "Várakozás..."}
onChange={(e) => isMyTurn && setPlayerAnswer(e.target.value)}
onKeyPress={(e) => isMyTurn && e.key === 'Enter' && !isProcessing && playerAnswer.trim() && handleSubmit()}
disabled={!isMyTurn || isProcessing}
placeholder={card.type === CardType.CLOSER ? "Számot adj meg" : "Válaszod..."}
className={`w-full bg-gray-800 border-2 border-gray-600 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none disabled:opacity-50 ${
!isMyTurn ? "cursor-default" : ""
}`}
/>
</div>
)}
{/* Hint (if available) */}
{card.hint && (
{card.hint && isMyTurn && (
<div className="bg-yellow-900/20 rounded-xl p-4 border border-yellow-500/30">
<div className="flex items-start gap-3">
<div className="text-2xl">💡</div>
@@ -234,11 +518,11 @@ const CardDisplayModal = ({
</div>
)}
{/* Submit Button - csak QUESTION típusnál */}
{cardType === "QUESTION" && (
{/* Submit Button - csak QUESTION típusnál és aktív játékosnál */}
{cardType === "QUESTION" && isMyTurn && (
<button
onClick={handleSubmit}
disabled={isProcessing || (!playerAnswer && !selectedOption)}
disabled={isProcessing || (!playerAnswer.trim() && selectedOption === null && card.type !== CardType.SENTENCE_PAIRING)}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500
text-white font-bold py-4 px-6 rounded-xl shadow-lg
transform transition-all duration-200 hover:scale-105 active:scale-95
@@ -254,8 +538,8 @@ const CardDisplayModal = ({
</button>
)}
{/* Close Button - LUCK és JOKER típusnál */}
{(cardType === "LUCK" || cardType === "JOKER") && (
{/* Close Button - LUCK és JOKER típusnál vagy nézőknél */}
{((cardType === "LUCK" || cardType === "JOKER") || !isMyTurn) && (
<button
onClick={onClose}
className="w-full bg-gradient-to-r from-green-600 to-teal-600 hover:from-green-500 hover:to-teal-500
@@ -133,7 +133,21 @@ const ConsequenceModal = ({
<div className="text-2xl"></div>
<div className="flex-1">
<p className="text-green-300 text-sm mb-1">A helyes válasz:</p>
<p className="text-white font-semibold">{correctAnswer}</p>
{Array.isArray(correctAnswer) ? (
<div className="space-y-1">
{correctAnswer
.filter(answer => answer.correct)
.map((answer, idx) => (
<p key={idx} className="text-white font-semibold">
{answer.answer ? `${answer.answer}) ` : ''}{answer.text}
</p>
))}
</div>
) : (
<p className="text-white font-semibold">
{typeof correctAnswer === 'object' ? JSON.stringify(correctAnswer) : correctAnswer}
</p>
)}
</div>
</div>
</div>
+521 -108
View File
@@ -50,14 +50,22 @@ const GameScreen = () => {
isConnected,
gameState,
players: backendPlayers,
playerPositions, // NEW: Get dedicated position tracking state
boardData: websocketBoardData,
currentTurn,
currentTurnName,
isMyTurn,
playerIdentifier,
isGamemaster,
error,
playerDiceRolls,
rollDice,
approveJoker,
rejectJoker,
submitAnswer,
submitPositionGuess,
submitJokerPositionGuess,
leaveGame,
addEventListener,
removeEventListener
} = useGameWebSocketContext()
@@ -89,6 +97,7 @@ const GameScreen = () => {
// Card display modal state
const [isCardModalOpen, setIsCardModalOpen] = useState(false)
const [currentCard, setCurrentCard] = useState(null)
const [isMyCardTurn, setIsMyCardTurn] = useState(false) // Track if I'm the one answering
// Consequence modal state
const [isConsequenceModalOpen, setIsConsequenceModalOpen] = useState(false)
@@ -98,6 +107,14 @@ const GameScreen = () => {
const [isPredictionModalOpen, setIsPredictionModalOpen] = useState(false)
const [currentPredictionData, setCurrentPredictionData] = useState(null)
// End game modal state
const [isEndGameModalOpen, setIsEndGameModalOpen] = useState(false)
const [endGameData, setEndGameData] = useState(null)
// Animation state management
const [animatingPlayers, setAnimatingPlayers] = useState({}) // { playerId: { from, to, startTime, duration } }
const [animatedPositions, setAnimatedPositions] = useState({}) // { playerId: currentAnimatedPosition }
// Memoized board dimensions
const { rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset, width, height } = useMemo(() => {
const { rows, cols, cellSize, cellMargin, rowSpacing } = BOARD_CONFIG
@@ -175,61 +192,210 @@ const GameScreen = () => {
}
}, [boardData, generateWindingPath])
// Update players from backend - memoized mapping
// Update players from backend - memoized mapping (UI properties only, no position)
useEffect(() => {
if (!backendPlayers?.length) return
const mappedPlayers = backendPlayers.map((player, index) => ({
id: player.playerId || player.id || index,
name: player.playerName || player.name || `Player ${index + 1}`,
position: player.boardPosition || 0,
score: player.score || 0,
color: PLAYER_STYLES[index % PLAYER_STYLES.length].color,
emoji: PLAYER_STYLES[index % PLAYER_STYLES.length].emoji,
isOnline: player.isOnline !== undefined ? player.isOnline : true,
isReady: player.isReady || false,
}))
const mappedPlayers = backendPlayers.map((player, index) => {
const playerName = player.playerName || player.name || `Player ${index + 1}`;
return {
id: player.playerId || player.id || index,
name: playerName,
// NO position stored here - always read from context playerPositions
score: player.score || 0,
color: PLAYER_STYLES[index % PLAYER_STYLES.length].color,
emoji: PLAYER_STYLES[index % PLAYER_STYLES.length].emoji,
isOnline: player.isOnline !== undefined ? player.isOnline : true,
isReady: player.isReady || false,
};
})
setPlayers(mappedPlayers)
}, [backendPlayers])
// Listen to player movement - optimized to update only moved player
// Debug: Log playerPositions changes
useEffect(() => {
console.log('🔍 [GameScreen] playerPositions changed:', JSON.stringify(playerPositions));
players.forEach(p => {
const pos = playerPositions?.[p.name];
console.log(`🔍 [GameScreen] Player ${p.name} position from context: ${pos}`);
});
}, [playerPositions, players]);
// Animation loop using requestAnimationFrame
useEffect(() => {
let animationFrameId
const animate = () => {
const now = Date.now()
const updates = {}
let hasActiveAnimations = false
Object.entries(animatingPlayers).forEach(([playerId, animation]) => {
const elapsed = now - animation.startTime
const progress = Math.min(elapsed / animation.duration, 1)
// Easing function (ease-in-out)
const eased = progress < 0.5
? 2 * progress * progress
: 1 - Math.pow(-2 * progress + 2, 2) / 2
// Interpolate position
const currentPos = Math.round(
animation.from + (animation.to - animation.from) * eased
)
updates[playerId] = currentPos
if (progress < 1) {
hasActiveAnimations = true
}
// Animation complete - no local position update needed
// Position always comes from context playerPositions
})
if (Object.keys(updates).length > 0) {
setAnimatedPositions(updates)
}
// Clean up completed animations
if (!hasActiveAnimations && Object.keys(animatingPlayers).length > 0) {
setAnimatingPlayers({})
setAnimatedPositions({})
} else if (hasActiveAnimations) {
animationFrameId = requestAnimationFrame(animate)
}
}
if (Object.keys(animatingPlayers).length > 0) {
animationFrameId = requestAnimationFrame(animate)
}
return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
}
}, [animatingPlayers])
// Listen to player-moving event to start animation
useEffect(() => {
if (!addEventListener) return
const handlePlayerMoved = (moveData) => {
setPlayers(prev =>
prev.map(p =>
p.id === moveData.playerId
? { ...p, position: moveData.newPosition }
: p
)
)
const handlePlayerMoving = (moveData) => {
const duration = Math.abs(moveData.toPosition - moveData.fromPosition) * 50 // 50ms per position
const clampedDuration = Math.max(500, Math.min(duration, 2000)) // Between 0.5s and 2s
// Backend sends 1-based positions (1-100), use directly
setAnimatingPlayers(prev => ({
...prev,
[moveData.playerId]: {
from: moveData.fromPosition,
to: moveData.toPosition,
startTime: Date.now(),
duration: clampedDuration
}
}))
}
addEventListener('game:player-moved', handlePlayerMoved)
return () => removeEventListener('game:player-moved')
addEventListener('game:player-moving', handlePlayerMoving)
return () => removeEventListener('game:player-moving')
}, [addEventListener, removeEventListener])
// Listen to Joker card events (csak Gamemaster számára)
// Listen to errors and close modals
useEffect(() => {
if (!addEventListener) return
const handleGameError = (errorData) => {
console.error('❌ Game error:', errorData)
// Close any open modals on error
setIsCardModalOpen(false)
setIsPredictionModalOpen(false)
setIsJokerModalOpen(false)
// Show error in consequence modal if severe
if (errorData.message && errorData.message.includes('card')) {
setCurrentConsequence({
isCorrect: false,
explanation: errorData.message || 'An error occurred',
consequenceType: null,
consequenceValue: 0
})
setIsConsequenceModalOpen(true)
}
}
addEventListener('game:error', handleGameError)
return () => removeEventListener('game:error')
}, [addEventListener, removeEventListener])
// Listen to player-arrived event (trigger animation for position changes)
useEffect(() => {
const handlePlayerArrived = (event) => {
const arrivalData = event.detail
// Context manager already updated playerPositions
// Just set animation flag for visual animation
const player = players.find(p => p.id === arrivalData.playerId || p.name === arrivalData.playerName)
if (player) {
setAnimatingPlayers(prevAnimating => ({
...prevAnimating,
[player.id]: true
}))
// Clear animation flag after animation completes (2 seconds)
setTimeout(() => {
setAnimatingPlayers(prevAnimating => {
const newAnimating = { ...prevAnimating }
delete newAnimating[player.id]
return newAnimating
})
}, 2000)
}
}
// Listen to window CustomEvent instead of socket event (context already handles socket)
window.addEventListener('game:player-arrived', handlePlayerArrived)
return () => window.removeEventListener('game:player-arrived', handlePlayerArrived)
}, [players])
// Listen to Joker card events
useEffect(() => {
if (!addEventListener) return
const handleJokerDrawn = (jokerData) => {
console.log('🃏 Joker kártya húzva:', jokerData)
// Joker approval modal megjelenítése
setCurrentJokerRequest({
playerId: jokerData.playerId,
playerName: jokerData.playerName,
playerEmoji: jokerData.playerEmoji || "🎭",
cardTitle: jokerData.cardTitle || jokerData.jokerCard?.question,
cardDescription: jokerData.cardDescription || jokerData.jokerCard?.consequence?.description,
points: jokerData.points || jokerData.jokerCard?.consequence?.value,
cardId: jokerData.cardId || jokerData.jokerCard?.id,
requestId: jokerData.requestId, // Important: requestId from backend
timestamp: Date.now()
})
setIsJokerModalOpen(true)
if (isGamemaster) {
// Gamemaster sees approval modal with approve/deny buttons
console.log('👑 Gamemaster látja a jóváhagyási modal-t')
setCurrentJokerRequest({
playerId: jokerData.playerId,
playerName: jokerData.playerName,
playerEmoji: jokerData.playerEmoji || "🎭",
cardTitle: jokerData.cardTitle || jokerData.jokerCard?.question,
cardDescription: jokerData.cardDescription || jokerData.jokerCard?.consequence?.description,
points: jokerData.points || jokerData.jokerCard?.consequence?.value,
cardId: jokerData.cardId || jokerData.jokerCard?.id,
requestId: jokerData.requestId, // Important: requestId from backend
timestamp: Date.now()
})
setIsJokerModalOpen(true)
} else {
// Other players see the joker card as a read-only card display
console.log('👥 Játékosok látják a joker kártyát (csak olvasás)')
setCurrentCard({
type: 'JOKER',
question: jokerData.jokerCard?.question || jokerData.cardTitle || 'Joker Kártya Feladat',
consequence: jokerData.jokerCard?.consequence?.description || jokerData.cardDescription,
playerName: jokerData.playerName,
playerEmoji: jokerData.playerEmoji || "🎭",
isReadOnly: true,
waitingForGamemaster: true
})
setIsCardModalOpen(true)
setIsMyCardTurn(false) // Not my turn to answer
}
}
// Listen for gamemaster decision request (correct event name per docs)
@@ -240,33 +406,57 @@ const GameScreen = () => {
removeEventListener('game:joker-drawn')
removeEventListener('game:gamemaster-decision-request')
}
}, [addEventListener, removeEventListener])
}, [addEventListener, removeEventListener, isGamemaster])
// Listen to card drawn events (kártya megjelenítés)
useEffect(() => {
if (!addEventListener) return
const handleCardDrawn = (cardData) => {
console.log('🎴 Kártya húzva:', cardData)
// Handle card drawn FOR ME (I need to answer)
const handleCardDrawnSelf = (data) => {
console.log('🎴 Kártya húzva NEKEM:', data)
const cardData = data.cardData || data;
console.log('📦 Card data structure:', cardData)
setCurrentCard({
id: cardData.cardId || cardData.id,
type: cardData.cardType || cardData.type,
question: cardData.question || cardData.text,
answerOptions: cardData.answerOptions || cardData.options || [],
id: cardData.cardid || cardData.id,
type: cardData.type,
question: cardData.question || cardData.text || cardData.statement,
answerOptions: cardData.answerOptions || [],
sentencePairs: cardData.sentencePairs || [],
words: cardData.words || [],
acceptableAnswers: cardData.acceptableAnswers || [],
correctAnswer: cardData.correctAnswer,
hint: cardData.hint,
points: cardData.points || 0,
timeLimit: cardData.timeLimit || 60
timeLimit: data.timeLimit || cardData.timeLimit || 60
})
setIsMyCardTurn(true) // I need to answer
setIsCardModalOpen(true)
}
// Listen for both generic and self-specific events
// Handle card drawn by ANOTHER PLAYER (spectator mode)
const handleCardDrawn = (data) => {
console.log('👀 Kártya húzva más játékos által:', data)
const cardData = data.cardData || data;
setCurrentCard({
id: cardData.cardid || cardData.id,
type: cardData.type,
question: cardData.question || cardData.text || cardData.statement,
answerOptions: cardData.answerOptions || [],
sentencePairs: cardData.sentencePairs || [],
words: cardData.words || [],
timeLimit: data.timeLimit || cardData.timeLimit || 60
})
setIsMyCardTurn(false) // Spectator mode
setIsCardModalOpen(true)
}
addEventListener('game:card-drawn-self', handleCardDrawnSelf)
addEventListener('game:card-drawn', handleCardDrawn)
addEventListener('game:card-drawn-self', handleCardDrawn)
return () => {
removeEventListener('game:card-drawn')
removeEventListener('game:card-drawn-self')
removeEventListener('game:card-drawn')
}
}, [addEventListener, removeEventListener])
@@ -297,11 +487,30 @@ const GameScreen = () => {
const handleLuckConsequence = (luckData) => {
console.log('🍀 Szerencse kártya következménye:', luckData)
// Close card modal if it's open (shouldn't be for luck, but just in case)
setIsCardModalOpen(false)
setCurrentConsequence({
isCorrect: true, // Luck cards don't have right/wrong answers
consequenceType: luckData.consequenceType,
consequenceValue: luckData.value || luckData.consequenceValue || 0,
explanation: luckData.message || 'Szerencse kártya!',
explanation: luckData.description || luckData.message || 'Szerencse kártya!',
playerAnswer: null,
correctAnswer: null
})
setIsConsequenceModalOpen(true)
}
const handleCardResult = (resultData) => {
console.log('🎴 Card result (luck):', resultData)
// This is for luck cards that use game:card-result event
setIsCardModalOpen(false)
setCurrentConsequence({
isCorrect: true,
consequenceType: resultData.consequence?.type,
consequenceValue: resultData.consequence?.value || 0,
explanation: resultData.description || 'Szerencse kártya!',
playerAnswer: null,
correctAnswer: null
})
@@ -310,10 +519,12 @@ const GameScreen = () => {
addEventListener('game:answer-validated', handleAnswerValidated)
addEventListener('game:luck-consequence', handleLuckConsequence)
addEventListener('game:card-result', handleCardResult)
return () => {
removeEventListener('game:answer-validated')
removeEventListener('game:luck-consequence')
removeEventListener('game:card-result')
}
}, [addEventListener, removeEventListener])
@@ -334,16 +545,105 @@ const GameScreen = () => {
setIsPredictionModalOpen(true)
}
const handleJokerPositionGuessRequest = (predictionData) => {
console.log('🃏🎯 Joker után pozíció tippelés kérés:', predictionData)
setCurrentPredictionData({
currentPosition: predictionData.currentPosition,
diceRoll: predictionData.diceRoll || predictionData.dice,
fieldStepValue: predictionData.fieldStepValue || predictionData.fieldStep || 0,
patternModifier: predictionData.patternModifier || predictionData.zoneModifier || 0,
cardText: predictionData.message || 'Tippeld meg a végső pozíciódat joker után!',
timeLimit: predictionData.timeLimit || 30,
isJoker: true // Mark this as joker guess
})
setIsPredictionModalOpen(true)
}
const handleGuessResult = (resultData) => {
console.log('✅ Tippelés eredménye:', resultData)
// Close prediction modal
setIsPredictionModalOpen(false)
// Backend already emits game:player-arrived before this event
// Position is handled by context manager, no need for pendingPositionUpdate
setCurrentConsequence({
isCorrect: resultData.guessCorrect,
playerAnswer: resultData.guessedPosition,
correctAnswer: resultData.actualPosition,
explanation: resultData.message,
consequenceType: resultData.penaltyApplied ? 'penalty' : 'success',
consequenceValue: resultData.penaltyApplied ? -2 : 0
})
setIsConsequenceModalOpen(true)
}
const handleJokerComplete = (resultData) => {
console.log('🃏✅ Joker tippelés eredménye:', resultData)
// Close prediction modal
setIsPredictionModalOpen(false)
// Backend already emits game:player-arrived before this event (if moved)
// Position is handled by context manager, no need for pendingPositionUpdate
setCurrentConsequence({
isCorrect: resultData.guessCorrect,
playerAnswer: resultData.guessedPosition,
correctAnswer: resultData.actualPosition,
explanation: resultData.message,
consequenceType: resultData.penaltyApplied ? 'penalty' : (resultData.moved ? 'success' : 'neutral'),
consequenceValue: resultData.penaltyApplied ? -2 : 0
})
setIsConsequenceModalOpen(true)
}
addEventListener('game:position-guess-request', handlePositionGuessRequest)
return () => removeEventListener('game:position-guess-request')
addEventListener('game:joker-position-guess-request', handleJokerPositionGuessRequest)
addEventListener('game:guess-result', handleGuessResult)
addEventListener('game:joker-complete', handleJokerComplete)
return () => {
removeEventListener('game:position-guess-request')
removeEventListener('game:joker-position-guess-request')
removeEventListener('game:guess-result')
removeEventListener('game:joker-complete')
}
}, [addEventListener, removeEventListener])
// Listen to game end event
useEffect(() => {
if (!addEventListener) return
const handleGameEnded = (endData) => {
console.log('🏆 Játék vége:', endData)
setEndGameData({
winnerName: endData.winnerName,
winnerId: endData.winner,
finalPositions: endData.finalPositions || [],
message: endData.message
})
setIsEndGameModalOpen(true)
}
const handleGamemasterDecision = (decisionData) => {
console.log('👑 Gamemaster döntés:', decisionData)
// Close joker card modal for non-gamemaster players when decision is made
if (!isGamemaster) {
setIsCardModalOpen(false)
}
}
addEventListener('game:ended', handleGameEnded)
addEventListener('game:gamemaster-decision-result', handleGamemasterDecision)
return () => {
removeEventListener('game:ended')
removeEventListener('game:gamemaster-decision-result')
}
}, [addEventListener, removeEventListener, isGamemaster])
// Joker jóváhagyás
const handleApproveJoker = useCallback(async (jokerRequest) => {
console.log('✅ Joker feladat jóváhagyva:', jokerRequest)
// WebSocket üzenet a backend felé
approveJoker(jokerRequest.playerId, jokerRequest.cardId, jokerRequest.requestId)
// WebSocket üzenet a backend felé - csak requestId kell
approveJoker(jokerRequest.requestId)
// Modal bezárása
setIsJokerModalOpen(false)
@@ -353,8 +653,8 @@ const GameScreen = () => {
const handleRejectJoker = useCallback(async (jokerRequest) => {
console.log('❌ Joker feladat elutasítva:', jokerRequest)
// WebSocket üzenet a backend felé
rejectJoker(jokerRequest.playerId, jokerRequest.cardId, jokerRequest.requestId)
// WebSocket üzenet a backend felé - csak requestId kell
rejectJoker(jokerRequest.requestId, 'Joker rejected by gamemaster')
// Modal bezárása
setIsJokerModalOpen(false)
@@ -362,31 +662,45 @@ const GameScreen = () => {
// Kártya válasz beküldése
const handleSubmitAnswer = useCallback((answer) => {
console.log('📝 Válasz beküldve:', answer)
console.log('📝 Válasz beküldve:', answer, 'Card ID:', currentCard?.id)
// WebSocket emit a backend felé
if (currentCard?.id) {
submitAnswer(currentCard.id, answer)
}
// WebSocket emit a backend felé - uses context method with card ID
submitAnswer(answer, currentCard?.id)
// A consequence modal automatikusan megnyílik a 'game:answer-validated' event hatására
}, [currentCard?.id, submitAnswer])
}, [submitAnswer, currentCard])
// Consequence modal bezárása
const handleConsequenceClose = useCallback(() => {
// Position updates are handled by game:player-arrived event in context
// No need to manually update positions here
setIsConsequenceModalOpen(false)
}, [])
// Pozíció tippelés beküldése
const handleSubmitPrediction = useCallback((predictedPosition) => {
console.log('🎯 Pozíció tippelés beküldve:', predictedPosition)
// WebSocket emit a backend felé
submitPositionGuess(predictedPosition)
// WebSocket emit a backend felé (különböző event joker-nél)
if (currentPredictionData?.isJoker) {
console.log('🃏 Joker pozíció tipp beküldése')
submitJokerPositionGuess(predictedPosition)
} else {
submitPositionGuess(predictedPosition)
}
// Modal bezárása
setIsPredictionModalOpen(false)
}, [submitPositionGuess])
}, [submitPositionGuess, submitJokerPositionGuess, currentPredictionData])
// Sorted players - memoized
// Sorted players - memoized (sort by context position)
const sortedPlayers = useMemo(
() => [...players].sort((a, b) => b.position - a.position),
[players]
() => [...players].sort((a, b) => {
const posA = playerPositions?.[a.name] || 0
const posB = playerPositions?.[b.name] || 0
return posB - posA
}),
[players, playerPositions]
)
// Handle dice roll
@@ -437,35 +751,45 @@ const GameScreen = () => {
return (
<div className="p-4 bg-gradient-to-br from-gray-900 via-gray-800 to-teal-900 min-h-screen flex items-center justify-center">
<div className="w-full">
{/* Connection Status Indicator */}
<div className="fixed top-4 right-4 z-50">
<div className={`px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 ${
isConnected
? 'bg-green-600 text-white'
: 'bg-red-600 text-white'
}`}>
<div className={`w-3 h-3 rounded-full ${
isConnected ? 'bg-green-300 animate-pulse' : 'bg-red-300'
}`}></div>
<span className="text-sm font-medium">
{isConnected ? '🟢 Csatlakozva' : '🔴 Kapcsolódás...'}
</span>
</div>
{error && !error.includes('Game not found') && !error.includes('token invalid') && (
<div className="mt-2 px-4 py-2 rounded-lg shadow-lg bg-red-600 text-white text-xs">
{error}
</div>
)}
</div> {/* Game Info Bar */}
{/* Exit Game Button - Top Right Corner */}
<div className="fixed top-4 right-4 z-50">
<button
onClick={() => {
if (window.confirm('Biztosan ki szeretnél lépni a játékból?')) {
leaveGame()
window.location.href = '/'
}
}}
className="bg-red-600 hover:bg-red-700 text-white font-semibold py-2 px-4 rounded-lg shadow-lg transition-colors duration-200 flex items-center gap-2 cursor-pointer"
title="Kilépés a játékból"
>
🚪 Kilépés
</button>
</div>
{/* Game Info Bar */}
{gameState && (
<div className="fixed top-4 left-4 z-50">
<div className="bg-gray-800 border border-teal-700 px-4 py-2 rounded-lg shadow-lg">
<div className="text-teal-300 text-sm font-medium">
🎮 Játék kód: <span className="font-bold text-white">{gameState.gameCode || 'N/A'}</span>
</div>
<div className="flex items-center gap-2 mt-1">
<div className={`w-2 h-2 rounded-full ${
isConnected ? 'bg-green-400 animate-pulse' : 'bg-red-400'
}`}></div>
<span className={`text-xs ${
isConnected ? 'text-green-400' : 'text-red-400'
}`}>
{isConnected ? 'Csatlakozva' : 'Kapcsolódás...'}
</span>
</div>
{currentTurn && (
<div className="text-gray-400 text-xs mt-1">
🎯 Köron: <span className="text-white">{players.find(p => p.id === currentTurn)?.name || 'Betöltés...'}</span>
🎯 Köron: <span className={`font-bold ${isMyTurn ? 'text-green-400' : 'text-white'}`}>
{currentTurnName || players.find(p => p.id === currentTurn || p.playerName === currentTurn || p.name === currentTurn)?.name || currentTurn || 'Betöltés...'}
</span>
{isMyTurn && <span className="ml-2 text-green-400 animate-pulse"> Te vagy!</span>}
</div>
)}
</div>
@@ -513,18 +837,28 @@ const GameScreen = () => {
))}
{/* Player tokens */}
{players.map((player) => (
<div
key={player.id}
className={`absolute w-6 h-6 ${player.color} rounded-full border-2 border-white shadow-lg flex items-center justify-center text-white text-xs font-bold z-10 animate-bounce`}
style={{
...getPlayerPosition(player.position),
transform: "translate(17px, 17px)",
}}
>
{player.emoji}
</div>
))}
{players.map((player) => {
// ALWAYS read position from context playerPositions, not local state
// Backend uses 1-based indexing (1-100)
const contextPosition = playerPositions?.[player.name] ?? 1
// Use animated position if player is currently animating, otherwise use context position
const displayPosition = animatedPositions[player.id] ?? contextPosition
const isAnimating = animatingPlayers[player.id] !== undefined
return (
<div
key={player.id}
className={`absolute w-6 h-6 ${player.color} rounded-full border-2 border-white shadow-lg flex items-center justify-center text-white text-xs font-bold z-10 transition-transform ${isAnimating ? 'scale-110' : ''}`}
style={{
...getPlayerPosition(displayPosition),
transform: "translate(17px, 17px)",
transition: isAnimating ? 'none' : 'all 0.3s ease'
}}
>
{player.emoji}
</div>
)
})}
</div>
</div>
@@ -591,7 +925,7 @@ const GameScreen = () => {
</span>
</div>
<div className="text-xs text-gray-500">
Pozíció: {player.position} Pontszám: {player.score}
Pozíció: {playerPositions?.[player.name] ?? 1} Pontszám: {player.score}
</div>
</div>
</div>
@@ -601,11 +935,23 @@ const GameScreen = () => {
{/* Dice Container */}
<div className="bg-gray-800 rounded-xl p-4 shadow-lg border border-teal-700 text-center">
<h2 className="text-xl font-semibold mb-3 text-teal-300">Dobókocka</h2>
<p className="text-gray-300 text-sm mb-4">
Kattints a kockára dobáshoz!
</p>
<Dice onRoll={handleDiceRoll} />
{isMyTurn ? (
<>
<p className="text-green-400 text-sm mb-4 font-bold animate-pulse">
🎯 A te köröd! Kattints a kockára dobáshoz!
</p>
<Dice onRoll={handleDiceRoll} />
</>
) : (
<>
<p className="text-gray-500 text-sm mb-4">
Várd meg a köröd...
</p>
<div className="opacity-50 pointer-events-none">
<Dice onRoll={handleDiceRoll} />
</div>
</>
)}
{/* Connection warning */}
{!isConnected && (
@@ -624,7 +970,9 @@ const GameScreen = () => {
<div>🎮 Game Code: {gameState?.gameCode || 'N/A'}</div>
<div>👥 Players: {backendPlayers?.length || 0}</div>
<div>🎲 Board Fields: {boardData?.fields?.length || 0}</div>
<div>🏁 Current Turn: {currentTurn || 'N/A'}</div>
<div>🏁 Current Turn: {currentTurnName || currentTurn || 'N/A'}</div>
<div>🆔 My ID: {playerIdentifier || 'N/A'}</div>
<div> Is My Turn: {isMyTurn ? 'YES' : 'NO'}</div>
{/* <div>🔑 Token: {gameToken ? '✅' : '❌'}</div> */}
</div>
</div>
@@ -650,20 +998,85 @@ const GameScreen = () => {
onClose={() => setIsCardModalOpen(false)}
card={currentCard}
onSubmitAnswer={handleSubmitAnswer}
isMyTurn={isMyCardTurn}
/>
{/* Consequence Modal - következmények megjelenítése */}
<ConsequenceModal
isOpen={isConsequenceModalOpen}
onClose={() => setIsConsequenceModalOpen(false)}
consequence={currentConsequence}
onClose={handleConsequenceClose}
isCorrect={currentConsequence?.isCorrect}
consequenceType={currentConsequence?.consequenceType}
consequenceValue={currentConsequence?.consequenceValue}
playerAnswer={currentConsequence?.playerAnswer}
correctAnswer={currentConsequence?.correctAnswer}
explanation={currentConsequence?.explanation}
/>
{/* End Game Modal */}
{isEndGameModalOpen && endGameData && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/90">
<div className="relative bg-gradient-to-br from-yellow-900 via-yellow-800 to-yellow-900 rounded-2xl shadow-2xl border-4 border-yellow-500 max-w-2xl w-full p-8 text-center">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-pulse" />
<div className="relative">
<div className="text-8xl mb-4 animate-bounce">🏆</div>
<h1 className="text-5xl font-bold text-white mb-4">
Játék vége!
</h1>
<div className="bg-black/30 rounded-xl p-6 mb-6">
<p className="text-6xl font-bold text-yellow-300 mb-2">
{endGameData.winnerName}
</p>
<p className="text-2xl text-yellow-100">
🎉 Nyert! 🎉
</p>
</div>
{endGameData.finalPositions && endGameData.finalPositions.length > 0 && (
<div className="bg-black/30 rounded-xl p-4 mb-6">
<h3 className="text-xl font-semibold text-yellow-300 mb-3">Végső állás:</h3>
<div className="space-y-2">
{endGameData.finalPositions
.sort((a, b) => b.boardPosition - a.boardPosition)
.map((player, index) => (
<div key={player.playerId} className="flex items-center justify-between bg-yellow-900/30 rounded-lg p-3">
<div className="flex items-center gap-3">
<span className="text-2xl">
{index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🎮'}
</span>
<span className="text-white font-semibold">{player.playerName}</span>
</div>
<span className="text-yellow-300 font-bold">Pozíció: {player.boardPosition}</span>
</div>
))}
</div>
</div>
)}
<button
onClick={() => window.location.href = '/'}
className="bg-gradient-to-r from-yellow-600 to-orange-600 hover:from-yellow-500 hover:to-orange-500 text-white font-bold py-4 px-8 rounded-xl shadow-lg transform transition-all duration-200 hover:scale-105 text-xl"
>
Vissza a főoldalra
</button>
</div>
</div>
</div>
)}
{/* Step Prediction Modal - pozíció tippelés */}
<StepPredictionModal
isOpen={isPredictionModalOpen}
onClose={() => setIsPredictionModalOpen(false)}
predictionData={currentPredictionData}
currentPosition={currentPredictionData?.currentPosition || 0}
diceRoll={currentPredictionData?.diceRoll || 0}
fieldStepValue={currentPredictionData?.fieldStepValue || 0}
patternModifier={currentPredictionData?.patternModifier || 0}
cardText={currentPredictionData?.cardText || "Tippeld meg, melyik pozícióra fogsz lépni!"}
hints={currentPredictionData?.hints || []}
timeLimit={currentPredictionData?.timeLimit || 30}
isJoker={currentPredictionData?.isJoker || false}
onSubmitPrediction={handleSubmitPrediction}
/>
</div>
@@ -1,4 +1,4 @@
import React, { useState } from "react"
import React, { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"
/**
@@ -12,6 +12,7 @@ import { motion, AnimatePresence } from "framer-motion"
* @param {Function} props.onReject - Elutasítás callback
* @param {string} props.playerName - Játékos neve
* @param {string} props.playerEmoji - Játékos emoji
* @param {number} props.timeLimit - Időkorlát másodpercben (default: 120)
*/
const JokerApprovalModal = ({
isOpen,
@@ -20,9 +21,49 @@ const JokerApprovalModal = ({
onApprove,
onReject,
playerName,
playerEmoji = "🎭"
playerEmoji = "🎭",
timeLimit = 120
}) => {
const [isProcessing, setIsProcessing] = useState(false)
const [timeLeft, setTimeLeft] = useState(timeLimit)
// Timer countdown
useEffect(() => {
if (!isOpen) return
setTimeLeft(timeLimit)
const timer = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) {
clearInterval(timer)
handleTimeout()
return 0
}
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [isOpen, timeLimit])
const handleTimeout = () => {
// Auto-reject on timeout
if (onReject && !isProcessing) {
handleReject()
}
}
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}:${secs.toString().padStart(2, '0')}`
}
const getTimeColor = () => {
if (timeLeft > 60) return "text-green-400"
if (timeLeft > 30) return "text-yellow-400"
return "text-red-400 animate-pulse"
}
const handleApprove = async () => {
setIsProcessing(true)
@@ -82,13 +123,21 @@ const JokerApprovalModal = ({
<p className="text-purple-100 text-sm">Gamemaster jóváhagyás szükséges</p>
</div>
</div>
<button
onClick={onClose}
className="text-white/80 hover:text-white transition-colors text-2xl"
disabled={isProcessing}
>
</button>
<div className="flex items-center gap-3">
{/* Timer */}
<div className="bg-black/30 rounded-lg px-4 py-2">
<div className={`text-2xl font-bold ${getTimeColor()}`}>
{formatTime(timeLeft)}
</div>
</div>
<button
onClick={onClose}
className="text-white/80 hover:text-white transition-colors text-2xl"
disabled={isProcessing}
>
</button>
</div>
</div>
</div>
@@ -17,7 +17,7 @@ const Lobby = () => {
const navigate = useNavigate()
const location = useLocation()
const [user, setUser] = useRequireAuth()
const [user, setUser] = useRequireAuth({ redirect: false })
// Get game code from location state or WebSocket
const gameCodeFromState = location.state?.gameCode
@@ -75,6 +75,7 @@ const Lobby = () => {
// Auto-navigate when game starts
useEffect(() => {
console.log("🎮 Lobby: gameStarted changed to:", gameStarted)
if (gameStarted) {
console.log("🎮 Game started, navigating to /game")
goGame()
@@ -42,7 +42,9 @@ const StepPredictionModal = ({
useEffect(() => {
if (!isOpen) return
// Reset time when modal opens
setTimeLeft(timeLimit)
const timer = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) {
@@ -55,10 +57,10 @@ const StepPredictionModal = ({
}, 1000)
return () => clearInterval(timer)
}, [isOpen, timeLimit])
}, [isOpen]) // Remove timeLimit from dependencies to prevent timer reset
const handleTimeout = () => {
if (onSubmitPrediction) {
if (onSubmitPrediction && !isProcessing) {
onSubmitPrediction(null) // null = timeout
}
}
@@ -89,7 +91,9 @@ const StepPredictionModal = ({
}, [isOpen])
// Számított végső pozíció (helyes válasz)
const calculatedPosition = currentPosition + diceRoll + fieldStepValue + patternModifier
// Backend formula: currentPosition + (stepValue × dice) + patternModifier
const movement = fieldStepValue * diceRoll
const calculatedPosition = currentPosition + movement + patternModifier
const formatTime = (seconds) => {
return `0:${seconds.toString().padStart(2, '0')}`
@@ -158,40 +162,58 @@ const StepPredictionModal = ({
</div>
</div>
{/* Calculation Info */}
<div className="bg-gradient-to-br from-blue-900/30 to-purple-900/30 rounded-xl p-3 border border-blue-500/30">
<h3 className="text-blue-300 font-semibold mb-2 text-center text-sm">📊 Számítási Adatok</h3>
<div className="grid grid-cols-2 gap-2 text-sm">
<div className="bg-black/30 rounded-lg p-2">
<p className="text-gray-400 text-xs mb-1">Jelenlegi pozíció</p>
<p className="text-white font-bold text-lg">{currentPosition}</p>
{/* Step-by-Step Calculation Helper */}
<div className="bg-gradient-to-br from-blue-900/30 to-purple-900/30 rounded-xl p-4 border border-blue-500/30">
<h3 className="text-blue-300 font-semibold mb-3 text-center">🧮 Számítási Adatok</h3>
{/* Visual calculation steps */}
<div className="space-y-2 mb-3">
<div className="flex items-center justify-between bg-black/40 rounded-lg p-3">
<span className="text-gray-300">Kezdő pozíció:</span>
<span className="text-white font-bold text-xl">{currentPosition}</span>
</div>
<div className="bg-black/30 rounded-lg p-2">
<p className="text-gray-400 text-xs mb-1">Dobás (kocka)</p>
<p className="text-white font-bold text-lg">+{diceRoll}</p>
<div className="flex items-center justify-between bg-black/40 rounded-lg p-3">
<span className="text-gray-300">Mező érték:</span>
<span className="text-yellow-400 font-bold text-xl">{fieldStepValue}</span>
</div>
<div className="bg-black/30 rounded-lg p-2">
<p className="text-gray-400 text-xs mb-1">Mező lépés</p>
<p className="text-white font-bold text-lg">+{fieldStepValue}</p>
</div>
<div className="bg-black/30 rounded-lg p-2">
<p className="text-gray-400 text-xs mb-1">Zóna módosító</p>
<p className={`font-bold text-lg ${patternModifier >= 0 ? 'text-green-400' : 'text-red-400'}`}>
{patternModifier >= 0 ? '+' : ''}{patternModifier}
</p>
<div className="flex items-center justify-between bg-black/40 rounded-lg p-3">
<span className="text-gray-300">Dobás:</span>
<span className="text-blue-400 font-bold text-xl">{diceRoll}</span>
</div>
</div>
<div className="mt-2 bg-yellow-900/30 rounded-lg p-2 border border-yellow-500/30">
<p className="text-yellow-300 text-center text-xs">
<span className="font-semibold">Számítsd ki:</span> {currentPosition} + {diceRoll} + {fieldStepValue} {patternModifier >= 0 ? '+' : ''} {patternModifier} = ?
{/* Pattern Modifier Info Box */}
<div className="bg-indigo-900/30 rounded-lg p-4 border border-indigo-500/30">
<div className="flex items-start gap-2 mb-2">
<span className="text-2xl"></span>
<div className="flex-1">
<h4 className="text-indigo-300 font-semibold mb-1">Zóna módosító szabályok:</h4>
<ul className="text-gray-300 text-sm space-y-1">
<li> <strong>0-ra végződik</strong> (10, 20...): nincs módosító</li>
<li> <strong>5-re végződik</strong> (15, 25...): ±3 módosító</li>
<li> <strong>3-mal osztható</strong> (9, 12, 21...): ±2 módosító</li>
<li> <strong>Páratlan</strong> (1, 7, 11...): ±1 módosító</li>
<li> <strong>Egyéb páros</strong>: nincs módosító</li>
</ul>
<p className="text-indigo-200 text-xs mt-2">A módosító előjele a mező típusától függ (+ pozitív, - negatív mező)</p>
</div>
</div>
</div>
{/* Formula hint without answer */}
<div className="bg-yellow-900/20 rounded-lg p-3 border border-yellow-500/30 mt-3">
<p className="text-yellow-200 text-xs text-center">
💡 <strong>Képlet:</strong> Kezdő + (Mező × Dobás) + Zóna módosító
</p>
</div>
</div>
{/* Position Input */}
<div className="space-y-2">
<h3 className="text-yellow-300 font-semibold text-center text-sm">
Írd be a tippelt pozíciót:
<h3 className="text-yellow-300 font-semibold text-center">
Írd be a tippelt pozíciót:
</h3>
<input
type="number"
@@ -199,19 +221,19 @@ const StepPredictionModal = ({
onChange={(e) => setPrediction(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSubmit()}
disabled={isProcessing}
placeholder="Pl: 28"
placeholder={`Pl.: ${Math.floor(currentPosition + diceRoll * 1.5)}`}
className="w-full bg-gray-800 border-2 border-yellow-600 rounded-xl px-4 py-3 text-white text-center text-2xl font-bold focus:border-yellow-400 focus:outline-none disabled:opacity-50"
min={currentPosition}
min={0}
max={100}
/>
</div>
{/* Prediction Info */}
{/* Show entered prediction */}
{prediction && prediction !== "" && (
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="bg-yellow-900/20 rounded-xl p-2 border border-yellow-500/30 text-center"
className="bg-yellow-900/20 rounded-xl p-3 border border-yellow-500/30 text-center"
>
<p className="text-yellow-300 text-sm">
A tipped: <span className="font-bold text-xl text-white">{prediction}</span> pozíció
@@ -17,30 +17,21 @@ export default function Home() {
const [isJoining, setIsJoining] = useState(false)
// Join game handler - csatlakozás játékhoz kóddal
const handleJoinGame = async (code) => {
if (!user) {
alert('Kérlek először jelentkezz be!')
goLogin()
return
const handleJoinGame = async (code, playerName) => {
// playerName is now passed from PlayMenu (either logged in user or guest name)
if (!playerName) {
alert('Név megadása kötelező a játékhoz való csatlakozáshoz!');
return;
}
console.log('=== JOIN GAME DEBUG ===')
console.log('Current user:', user)
console.log('Game code:', code)
console.log('LocalStorage username:', localStorage.getItem('username'))
console.log('LocalStorage authLevel:', localStorage.getItem('authLevel'))
console.log('======================')
setIsJoining(true)
try {
const joinData = {
gameCode: code.toUpperCase(),
playerName: user || 'Player',
playerName: playerName,
}
console.log('Sending join request with:', joinData)
const response = await joinGame(joinData)
console.log('Joined game:', response)
// Backend returns game object directly
if (response.gameToken) {
@@ -51,8 +42,6 @@ export default function Home() {
} catch (err) {
const errorMsg = err.response?.data?.error || err.response?.data?.message || 'Nem sikerült csatlakozni a játékhoz'
alert(errorMsg)
console.error('Join game error:', err)
console.error('Error details:', err.response?.data)
} finally {
setIsJoining(false)
}
+11 -1
View File
@@ -4,7 +4,7 @@ import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss(),],
plugins: [react(), tailwindcss()],
server: {
host: '0.0.0.0',
port: 5173,
@@ -29,6 +29,16 @@ export default defineConfig({
preview: {
host: '0.0.0.0',
port: 5173,
},
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // Remove all console statements in production
drop_debugger: true, // Remove debugger statements
pure_funcs: ['console.log', 'console.warn', 'console.debug'] // Specifically target these
}
}
}
})