Files
SerpentRace/SerpentRace_Frontend/src/components/DeckCreator/TaskCardEditor.jsx
T

643 lines
29 KiB
React
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// src/components/DeckCreator/TaskCardEditor.jsx
// Feladat kártya szerkesztő (Quiz, Igaz/Hamis, Párosítás, Szöveges)
import React from "react"
import { FaPlus, FaTrash, FaCheck, FaTimes } from "react-icons/fa"
const taskSubTypes = [
{ value: 'quiz', label: '📋 Quiz (A/B/C/D)', description: 'Feleletválasztós kérdés' },
{ value: 'truefalse', label: '✅ Igaz/Hamis', description: 'Igaz vagy hamis állítás' },
{ value: 'matching', label: '🔗 Párosítás', description: 'Elemek összekapcsolása' },
{ value: 'text', label: '✏️ Szöveges válasz', description: 'Szabadszöveges válasz' }
]
const timeLimits = [
{ value: 15, label: '15 másodperc' },
{ value: 30, label: '30 másodperc' },
{ value: 45, label: '45 másodperc' },
{ value: 60, label: '1 perc' },
{ value: 90, label: '1.5 perc' },
{ value: 120, label: '2 perc' }
]
const consequenceTypes = [
{ value: 0, label: '⬆️ Előre lépés', description: 'A játékos előre lép X mezőt' },
{ value: 1, label: '⬇️ Hátra lépés', description: 'A játékos hátra lép X mezőt' },
{ value: 2, label: '⏸️ Kör kihagyás', description: 'A játékos kihagy egy kört' },
{ value: 3, label: '⏩ Extra kör', description: 'A játékos kap egy extra kört' },
{ value: 5, label: '🏁 Vissza a starthoz', description: 'A játékos visszakerül a starthoz' }
]
export default function TaskCardEditor({ card, onChange }) {
const updateField = (field, value) => {
onChange({ [field]: value })
}
const updateOption = (index, value) => {
const newOptions = [...card.options]
newOptions[index] = value
onChange({ options: newOptions })
}
const addMatchingPair = () => {
const newLeft = [...(card.leftItems || []), '']
const newRight = [...(card.rightItems || []), '']
const newCorrectPairs = { ...(card.correctPairs || {}), [newLeft.length - 1]: newRight.length - 1 }
onChange({
leftItems: newLeft,
rightItems: newRight,
correctPairs: newCorrectPairs
})
}
const removeMatchingPair = (index) => {
const newLeft = card.leftItems.filter((_, i) => i !== index)
const newRight = card.rightItems.filter((_, i) => i !== index)
const newCorrectPairs = {}
// Újraszámozás
Object.entries(card.correctPairs).forEach(([leftIdx, rightIdx]) => {
const newLeftIdx = parseInt(leftIdx) > index ? parseInt(leftIdx) - 1 : parseInt(leftIdx)
const newRightIdx = parseInt(rightIdx) > index ? parseInt(rightIdx) - 1 : parseInt(rightIdx)
if (newLeftIdx !== index && newRightIdx !== index) {
newCorrectPairs[newLeftIdx] = newRightIdx
}
})
onChange({
leftItems: newLeft,
rightItems: newRight,
correctPairs: newCorrectPairs
})
}
const addAcceptedAnswer = () => {
const newAnswers = [...(card.acceptedAnswers || []), '']
onChange({ acceptedAnswers: newAnswers })
}
const updateAcceptedAnswer = (index, value) => {
const currentAnswers = card.acceptedAnswers || ['']
const newAnswers = [...currentAnswers]
newAnswers[index] = value
onChange({ acceptedAnswers: newAnswers })
}
const removeAcceptedAnswer = (index) => {
const currentAnswers = card.acceptedAnswers || ['']
if (currentAnswers.length <= 1) return // Legalább egy válasz maradjon
const newAnswers = currentAnswers.filter((_, i) => i !== index)
onChange({ acceptedAnswers: newAnswers })
}
const updateConsequence = (field, value) => {
const currentConsequence = card.consequence || { type: 0, value: 1 }
onChange({
consequence: {
...currentConsequence,
[field]: value
}
})
}
const updateWrongConsequence = (field, value) => {
const currentWrongConsequence = card.wrongConsequence || { type: 1, value: 1 }
onChange({
wrongConsequence: {
...currentWrongConsequence,
[field]: value
}
})
}
return (
<div className="max-w-4xl mx-auto space-y-6">
{/* Feladat típus választó */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
🎯 Feladat típusa
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{taskSubTypes.map(type => (
<button
key={type.value}
onClick={() => updateField('subType', type.value)}
className={`
p-4 rounded-xl border text-left transition-all duration-200 hover:scale-105
${card.subType === type.value
? 'bg-[color:var(--color-success)]/10 border-[color:var(--color-success)] shadow-lg'
: 'bg-[color:var(--color-background)] border-[color:var(--color-surface-selected)] hover:bg-[color:var(--color-surface-selected)]'
}
`}
>
<div className="font-semibold text-[color:var(--color-text)]">
{type.label}
</div>
<div className="text-sm text-[color:var(--color-text-muted)] mt-1">
{type.description}
</div>
</button>
))}
</div>
</div>
{/* Quiz típus szerkesztő */}
{card.subType === 'quiz' && (
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
📋 Quiz kérdés
</h3>
{/* Kérdés */}
<div className="mb-6">
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Kérdés
</label>
<textarea
value={card.question || ''}
onChange={(e) => updateField('question', 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..."
/>
</div>
{/* Válaszlehetőségek */}
<div className="space-y-3">
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium">
Válaszlehetőségek
</label>
{['A', 'B', 'C', 'D'].map((letter, index) => (
<div key={index} className="flex items-center gap-3">
<div className="flex items-center gap-2">
<button
onClick={() => updateField('correctAnswer', index)}
className={`
w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-all duration-200
${card.correctAnswer === index
? 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)]'
: 'bg-[color:var(--color-background)] text-[color:var(--color-text)] border border-[color:var(--color-surface-selected)]'
}
`}
>
{letter}
</button>
{card.correctAnswer === index && (
<FaCheck className="text-[color:var(--color-success)] text-sm" />
)}
</div>
<input
type="text"
value={card.options?.[index] || ''}
onChange={(e) => updateOption(index, e.target.value)}
className="flex-1 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={`${letter} válasz...`}
/>
</div>
))}
</div>
</div>
)}
{/* Igaz/Hamis típus szerkesztő */}
{card.subType === 'truefalse' && (
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
Igaz/Hamis állítás
</h3>
{/* Állítás */}
<div className="mb-6">
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Állítás
</label>
<textarea
value={card.statement || ''}
onChange={(e) => updateField('statement', 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..."
/>
</div>
{/* Helyes válasz */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-3">
Helyes válasz
</label>
<div className="flex gap-4">
<button
onClick={() => updateField('isTrue', true)}
className={`
flex items-center gap-3 px-6 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105
${card.isTrue === true
? 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] shadow-lg'
: 'bg-[color:var(--color-background)] text-[color:var(--color-text)] border border-[color:var(--color-surface-selected)]'
}
`}
>
<FaCheck />
IGAZ
</button>
<button
onClick={() => updateField('isTrue', false)}
className={`
flex items-center gap-3 px-6 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105
${card.isTrue === false
? 'bg-[color:var(--color-error)] text-[color:var(--color-text-inverse)] shadow-lg'
: 'bg-[color:var(--color-background)] text-[color:var(--color-text)] border border-[color:var(--color-surface-selected)]'
}
`}
>
<FaTimes />
HAMIS
</button>
</div>
</div>
</div>
)}
{/* Párosítás típus szerkesztő */}
{card.subType === 'matching' && (
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
🔗 Párosítás feladat
</h3>
{/* Feladat leírás */}
<div className="mb-6">
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Feladat leírása
</label>
<input
type="text"
value={card.taskDescription || ''}
onChange={(e) => updateField('taskDescription', 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"
/>
</div>
{/* Párosítások */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<label className="text-[color:var(--color-text-muted)] text-sm font-medium">
Párosítások
</label>
<button
onClick={addMatchingPair}
className="flex items-center gap-2 px-3 py-1 rounded-lg bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] text-sm font-medium hover:bg-[color:var(--color-success)]/80 transition-all duration-200"
>
<FaPlus />
Új pár
</button>
</div>
{(card.leftItems || []).map((leftItem, index) => (
<div key={index} className="grid grid-cols-5 gap-3 items-center">
<input
type="text"
value={leftItem}
onChange={(e) => {
const newLeft = [...card.leftItems]
newLeft[index] = e.target.value
onChange({ leftItems: newLeft })
}}
className="col-span-2 px-3 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 text-sm"
placeholder="Bal oldal..."
/>
<div className="text-center text-[color:var(--color-text-muted)]"></div>
<input
type="text"
value={card.rightItems?.[index] || ''}
onChange={(e) => {
const newRight = [...(card.rightItems || [])]
newRight[index] = e.target.value
onChange({ rightItems: newRight })
}}
className="px-3 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 text-sm"
placeholder="Jobb oldal..."
/>
<button
onClick={() => removeMatchingPair(index)}
className="p-2 rounded-lg bg-[color:var(--color-error)]/10 text-[color:var(--color-error)] hover:bg-[color:var(--color-error)]/20 transition-all duration-200"
>
<FaTrash className="text-xs" />
</button>
</div>
))}
</div>
</div>
)}
{/* Szöveges válasz típus szerkesztő */}
{card.subType === 'text' && (
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
Szöveges válasz
</h3>
{/* Kérdés */}
<div className="mb-6">
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Kérdés
</label>
<textarea
value={card.question || ''}
onChange={(e) => updateField('question', 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..."
/>
</div>
{/* Elfogadott válaszok */}
<div className="mb-6">
<div className="flex justify-between items-center mb-3">
<label className="text-[color:var(--color-text-muted)] text-sm font-medium">
Elfogadott válaszok
</label>
<button
onClick={addAcceptedAnswer}
className="flex items-center gap-2 px-3 py-1 rounded-lg bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] text-sm font-medium hover:bg-[color:var(--color-success)]/80 transition-all duration-200"
>
<FaPlus />
Új válasz
</button>
</div>
<div className="space-y-2">
{(card.acceptedAnswers || ['']).map((answer, index) => (
<div key={index} className="flex gap-2">
<input
type="text"
value={answer}
onChange={(e) => updateAcceptedAnswer(index, e.target.value)}
className="flex-1 px-3 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 text-sm"
placeholder={`Elfogadott válasz ${index + 1}...`}
/>
{(card.acceptedAnswers?.length || 1) > 1 && (
<button
onClick={() => removeAcceptedAnswer(index)}
className="p-2 rounded-lg bg-[color:var(--color-error)]/10 text-[color:var(--color-error)] hover:bg-[color:var(--color-error)]/20 transition-all duration-200"
>
<FaTrash className="text-xs" />
</button>
)}
</div>
))}
</div>
<div className="text-xs text-[color:var(--color-text-muted)] mt-2">
Vesszővel elválasztva is megadhatsz több elfogadott választ egy mezőben
</div>
</div>
{/* Beállítások - Később elérhető */}
<div className="relative">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 opacity-50 pointer-events-none blur-[1px]">
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={false}
disabled
className="w-4 h-4 text-[color:var(--color-success)] border-[color:var(--color-surface-selected)] rounded focus:ring-[color:var(--color-success)]"
/>
<span className="text-[color:var(--color-text)]">Kis/nagy betű érzékeny</span>
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={false}
disabled
className="w-4 h-4 text-[color:var(--color-success)] border-[color:var(--color-surface-selected)] rounded focus:ring-[color:var(--color-success)]"
/>
<span className="text-[color:var(--color-text)]">Pontos egyezés</span>
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={false}
disabled
className="w-4 h-4 text-[color:var(--color-success)] border-[color:var(--color-surface-selected)] rounded focus:ring-[color:var(--color-success)]"
/>
<span className="text-[color:var(--color-text)]">Részleges elfogadás</span>
</label>
</div>
{/* Hamarosan elérhető felület */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="bg-[color:var(--color-warning)]/20 backdrop-blur-sm border-2 border-[color:var(--color-warning)] rounded-lg px-4 py-2">
<span className="text-[color:var(--color-warning)] font-semibold text-sm flex items-center gap-2">
🚧 Hamarosan elérhető
</span>
</div>
</div>
</div>
{/* Tipp */}
<div className="mt-4">
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Tipp (opcionális)
</label>
<input
type="text"
value={card.hint || ''}
onChange={(e) => updateField('hint', 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="Segítő tipp a játékosoknak..."
/>
</div>
</div>
)}
{/* Közös beállítások - Később elérhető */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6 relative">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
Beállítások
</h3>
<div className="relative">
<div className="opacity-50 pointer-events-none blur-[1px]">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Pontszám */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
💰 Pontszám
</label>
<input
type="number"
value={10}
disabled
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"
min="0"
max="1000"
/>
</div>
{/* Időlimit */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Időlimit
</label>
<select
disabled
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"
>
<option>30 másodperc</option>
</select>
</div>
{/* Karakterlimit (csak szöveges válasznál) */}
{card.subType === 'text' && (
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
📝 Karakterlimit
</label>
<input
type="number"
value={100}
disabled
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"
min="10"
max="500"
/>
</div>
)}
</div>
{/* Magyarázat */}
<div className="mt-6">
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
💡 Magyarázat (opcionális)
</label>
<textarea
disabled
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="Magyarázat a helyes válaszhoz..."
/>
</div>
</div>
{/* Hamarosan elérhető felület */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="bg-[color:var(--color-warning)]/20 backdrop-blur-sm border-2 border-[color:var(--color-warning)] rounded-lg px-4 py-2">
<span className="text-[color:var(--color-warning)] font-semibold text-sm flex items-center gap-2">
🚧 Hamarosan elérhető
</span>
</div>
</div>
</div>
</div>
{/* Következmények (helyes válasz esetén) */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
🎯 Következmények (helyes válasz esetén)
</h3>
<div className="space-y-4">
{/* Consequence Type */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Hatás típusa
</label>
<select
value={card.consequence?.type ?? 0}
onChange={(e) => updateConsequence('type', parseInt(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"
>
{consequenceTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
<div className="text-xs text-[color:var(--color-text-muted)] mt-1">
{consequenceTypes.find(t => t.value === (card.consequence?.type ?? 0))?.description}
</div>
</div>
{/* Consequence Value - csak ha előre/hátra lépés */}
{(card.consequence?.type === 0 || card.consequence?.type === 1) && (
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Mezők száma
</label>
<input
type="number"
value={card.consequence?.value ?? 1}
onChange={(e) => updateConsequence('value', parseInt(e.target.value) || 1)}
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"
min="1"
max="10"
/>
</div>
)}
</div>
</div>
{/* Következmények (rossz válasz esetén) */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
Következmények (rossz válasz esetén)
</h3>
<div className="space-y-4">
{/* Wrong Consequence Type */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Hatás típusa
</label>
<select
value={card.wrongConsequence?.type ?? 1}
onChange={(e) => updateWrongConsequence('type', parseInt(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-danger)] focus:border-transparent outline-none transition-all duration-200"
>
{consequenceTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
<div className="text-xs text-[color:var(--color-text-muted)] mt-1">
{consequenceTypes.find(t => t.value === (card.wrongConsequence?.type ?? 1))?.description}
</div>
</div>
{/* Wrong Consequence Value - csak ha előre/hátra lépés */}
{(card.wrongConsequence?.type === 0 || card.wrongConsequence?.type === 1) && (
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Mezők száma
</label>
<input
type="number"
value={card.wrongConsequence?.value ?? 1}
onChange={(e) => updateWrongConsequence('value', parseInt(e.target.value) || 1)}
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-danger)] focus:border-transparent outline-none transition-all duration-200"
min="1"
max="10"
/>
</div>
)}
</div>
</div>
</div>
)
}