500 lines
22 KiB
React
500 lines
22 KiB
React
// 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' }
|
||
]
|
||
|
||
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 newAnswers = [...card.acceptedAnswers]
|
||
newAnswers[index] = value
|
||
onChange({ acceptedAnswers: newAnswers })
|
||
}
|
||
|
||
const removeAcceptedAnswer = (index) => {
|
||
const newAnswers = card.acceptedAnswers.filter((_, i) => i !== index)
|
||
onChange({ acceptedAnswers: newAnswers })
|
||
}
|
||
|
||
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 || 0) > 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 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<label className="flex items-center gap-2 text-sm">
|
||
<input
|
||
type="checkbox"
|
||
checked={card.caseSensitive || false}
|
||
onChange={(e) => updateField('caseSensitive', e.target.checked)}
|
||
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={card.exactMatch || false}
|
||
onChange={(e) => updateField('exactMatch', e.target.checked)}
|
||
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={card.partialAccepted || true}
|
||
onChange={(e) => updateField('partialAccepted', e.target.checked)}
|
||
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>
|
||
|
||
{/* 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 */}
|
||
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
|
||
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
|
||
⚙️ Beállítások
|
||
</h3>
|
||
|
||
<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={card.points || 10}
|
||
onChange={(e) => updateField('points', parseInt(e.target.value) || 0)}
|
||
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
|
||
value={card.timeLimit || 30}
|
||
onChange={(e) => updateField('timeLimit', 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"
|
||
>
|
||
{timeLimits.map(time => (
|
||
<option key={time.value} value={time.value}>
|
||
{time.label}
|
||
</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={card.characterLimit || 100}
|
||
onChange={(e) => updateField('characterLimit', parseInt(e.target.value) || 0)}
|
||
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
|
||
value={card.explanation || ''}
|
||
onChange={(e) => updateField('explanation', 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="Magyarázat a helyes válaszhoz..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
} |