10.23 zsola hibák + Deckek listázása megoldva #63

Merged
Donat merged 1 commits from 1023zsolahibak into main 2025-10-23 20:20:07 +02:00
4 changed files with 244 additions and 47 deletions
Showing only changes of commit b73d1528c4 - Show all commits
@@ -33,6 +33,19 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
options: ['', '', '', ''], options: ['', '', '', ''],
correctAnswer: 0, correctAnswer: 0,
explanation: '', explanation: '',
acceptedAnswers: [''],
wrongConsequence: { type: 1, value: 1 }
}
case 'PAIRING':
case 'MATCHING':
return {
...baseData,
type: 'QUESTION',
subType: 'matching',
taskDescription: '',
leftItems: ['', ''],
rightItems: ['', ''],
correctPairs: { 0: 0, 1: 1 },
wrongConsequence: { type: 1, value: 1 } wrongConsequence: { type: 1, value: 1 }
} }
case 'JOKER': case 'JOKER':
@@ -84,13 +97,60 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
} }
if (data.type === 'QUESTION') { if (data.type === 'QUESTION') {
if (!data.question && !data.statement) { // Quiz típus validálás
notifyError("Kérdés vagy állítás megadása kötelező!") if (data.subType === 'quiz') {
return false if (!data.question || !data.question.trim()) {
notifyError("Kérdés megadása kötelező!")
return false
}
if (data.options && data.options.some(opt => !opt.trim())) {
notifyError("Minden válaszlehetőséget ki kell tölteni!")
return false
}
} }
if (data.subType === 'quiz' && data.options && data.options.some(opt => !opt.trim())) { // Igaz/Hamis típus validálás
notifyError("Minden válaszlehetőséget ki kell tölteni!") else if (data.subType === 'truefalse') {
return false if (!data.statement || !data.statement.trim()) {
notifyError("Állítás megadása kötelező!")
return false
}
if (data.isTrue === undefined || data.isTrue === null) {
notifyError("Válaszd ki, hogy az állítás igaz vagy hamis!")
return false
}
}
// Párosítás típus validálás
else if (data.subType === 'matching') {
if (!data.taskDescription || !data.taskDescription.trim()) {
notifyError("Feladat leírása kötelező!")
return false
}
if (!data.leftItems || data.leftItems.length === 0) {
notifyError("Legalább egy párosítást meg kell adni!")
return false
}
if (data.leftItems.some(item => !item.trim()) || data.rightItems.some(item => !item.trim())) {
notifyError("Minden párosítási elemet ki kell tölteni!")
return false
}
}
// Szöveges válasz típus validálás
else if (data.subType === 'text') {
if (!data.question || !data.question.trim()) {
notifyError("Kérdés megadása kötelező!")
return false
}
if (!data.acceptedAnswers || data.acceptedAnswers.length === 0 || data.acceptedAnswers.every(ans => !ans.trim())) {
notifyError("Legalább egy elfogadott választ meg kell adni!")
return false
}
}
// Általános validálás (ha nincs subType megadva)
else {
if (!data.question && !data.statement) {
notifyError("Kérdés vagy állítás megadása kötelező!")
return false
}
} }
} else if (data.type === 'JOKER') { } else if (data.type === 'JOKER') {
if (!data.text || !data.text.trim()) { if (!data.text || !data.text.trim()) {
@@ -9,6 +9,8 @@ import {
FaSortAlphaDown, FaSortAlphaDown,
FaSortAlphaUp, FaSortAlphaUp,
FaQuestionCircle, FaQuestionCircle,
FaChevronLeft,
FaChevronRight,
} from "react-icons/fa" } from "react-icons/fa"
import SearchBox from "../Search/SearchBox" import SearchBox from "../Search/SearchBox"
import PopUp from "../PopUp/PopUp" import PopUp from "../PopUp/PopUp"
@@ -70,18 +72,25 @@ const DeckManager = () => {
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [showSortHelp, setShowSortHelp] = useState(false) const [showSortHelp, setShowSortHelp] = useState(false)
const [selectedDeck, setSelectedDeck] = useState(null) const [selectedDeck, setSelectedDeck] = useState(null)
const [decks, setDecks] = useState([]) const [allDecks, setAllDecks] = useState([]) // Összes pakli
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [itemsPerPage, setItemsPerPage] = useState(20)
const [currentPage, setCurrentPage] = useState(1)
// Load all decks once
useEffect(() => { useEffect(() => {
let mounted = true let mounted = true
const load = async () => { const load = async () => {
setLoading(true) setLoading(true)
try { try {
const result = await import('../../api/deckApi').then(m => m.getDecksPage(0, 49)) // Load all decks (0-99 is the max limit = 100 decks)
const result = await import('../../api/deckApi').then(m => m.getDecksPage(0, 99))
if (!mounted) return if (!mounted) return
// map backend deck shape to UI shape
const mapped = result.decks.map(d => ({ console.log('Loaded decks:', result) // Debug
// Map backend deck shape to UI shape
const mapped = (result.decks || []).map(d => ({
id: d.id, id: d.id,
name: d.name, name: d.name,
type: d.type === 2 ? 'Question' : d.type === 1 ? 'Joker' : 'Luck', type: d.type === 2 ? 'Question' : d.type === 1 ? 'Joker' : 'Luck',
@@ -89,7 +98,9 @@ const DeckManager = () => {
origin: d.ctype === 2 ? 'Vállalati' : d.ctype === 0 ? 'Mind' : 'Saját', origin: d.ctype === 2 ? 'Vállalati' : d.ctype === 0 ? 'Mind' : 'Saját',
raw: d raw: d
})) }))
setDecks(mapped)
console.log('Mapped decks:', mapped) // Debug
setAllDecks(mapped)
} catch (err) { } catch (err) {
console.error('Failed to load decks', err) console.error('Failed to load decks', err)
} finally { } finally {
@@ -101,8 +112,7 @@ const DeckManager = () => {
}, []) }, [])
// Filter logic // Filter logic
const sourceDecks = decks let filteredDecks = allDecks.filter((deck) => {
let filteredDecks = sourceDecks.filter((deck) => {
const typeMatch = selectedType === "All" || deck.type === selectedType const typeMatch = selectedType === "All" || deck.type === selectedType
const originMatch = selectedOrigin === "Mind" || deck.origin === selectedOrigin const originMatch = selectedOrigin === "Mind" || deck.origin === selectedOrigin
const searchMatch = !search || deck.name.toLowerCase().includes(search.toLowerCase()) const searchMatch = !search || deck.name.toLowerCase().includes(search.toLowerCase())
@@ -123,6 +133,18 @@ const DeckManager = () => {
return 0 return 0
}) })
// Pagination logic - frontend only
const totalDecks = filteredDecks.length
const totalPages = Math.ceil(totalDecks / itemsPerPage)
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const paginatedDecks = filteredDecks.slice(startIndex, endIndex)
// Reset to page 1 when filters or items per page change
useEffect(() => {
setCurrentPage(1)
}, [selectedType, selectedOrigin, search, sortBy, itemsPerPage])
return ( return (
<div className="w-full flex flex-col bg-[color:var(--color-background)]"> <div className="w-full flex flex-col bg-[color:var(--color-background)]">
<div className="w-full max-w-[1200px] mx-auto px-4 py-10"> <div className="w-full max-w-[1200px] mx-auto px-4 py-10">
@@ -263,6 +285,36 @@ const DeckManager = () => {
)} )}
</div> </div>
</div> </div>
{/* Items per page selector and pagination info */}
<div className="flex flex-col md:flex-row gap-4 justify-between items-center mb-6 bg-[color:var(--color-surface)]/60 backdrop-blur-lg rounded-xl px-6 py-3 shadow">
<div className="flex items-center gap-3">
<span className="text-[color:var(--color-text-muted)] text-sm font-medium">
Elemek oldalanként:
</span>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="px-3 py-1.5 rounded-lg bg-[color:var(--color-background)] text-[color:var(--color-text)] border border-[color:var(--color-surface-selected)] focus:ring-2 focus:ring-[color:var(--color-success)] outline-none transition-all duration-200"
>
<option value={20}>20</option>
<option value={30}>30</option>
<option value={40}>40</option>
<option value={50}>50</option>
</select>
</div>
<div className="text-[color:var(--color-text-muted)] text-sm">
{totalDecks > 0 ? (
<>
{startIndex + 1}-{Math.min(endIndex, totalDecks)} / {totalDecks} pakli
</>
) : (
<>0 pakli</>
)}
</div>
</div>
{/* Decks Grid */} {/* Decks Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-8 mt-8"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-8 mt-8">
{/* Create New Deck (Mockup) */} {/* Create New Deck (Mockup) */}
@@ -280,7 +332,7 @@ const DeckManager = () => {
{!loading && filteredDecks.length === 0 && ( {!loading && filteredDecks.length === 0 && (
<div className="col-span-full text-center text-[color:var(--color-text-muted)]">Nincsenek mentett paklik.</div> <div className="col-span-full text-center text-[color:var(--color-text-muted)]">Nincsenek mentett paklik.</div>
)} )}
{!loading && filteredDecks.map((deck) => { {!loading && paginatedDecks.map((deck) => {
const deckType = deckTypes.find((t) => t.label === deck.type) const deckType = deckTypes.find((t) => t.label === deck.type)
const borderColor = deckType ? deckType.color : "var(--color-success)" const borderColor = deckType ? deckType.color : "var(--color-success)"
return ( return (
@@ -317,6 +369,73 @@ const DeckManager = () => {
) )
})} })}
</div> </div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="flex justify-center items-center gap-2 mt-8">
<button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className={`px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 ${
currentPage === 1
? 'bg-[color:var(--color-surface)] text-[color:var(--color-text-muted)] cursor-not-allowed'
: 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] hover:bg-[color:var(--color-success)]/80 hover:scale-105'
}`}
>
<FaChevronLeft />
Előző
</button>
<div className="flex items-center gap-2">
{[...Array(totalPages)].map((_, index) => {
const pageNum = index + 1
// Show first page, last page, current page and neighbors
if (
pageNum === 1 ||
pageNum === totalPages ||
(pageNum >= currentPage - 1 && pageNum <= currentPage + 1)
) {
return (
<button
key={pageNum}
onClick={() => setCurrentPage(pageNum)}
className={`w-10 h-10 rounded-lg font-medium transition-all duration-200 ${
currentPage === pageNum
? 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] scale-110 shadow-lg'
: 'bg-[color:var(--color-surface)] text-[color:var(--color-text)] hover:bg-[color:var(--color-surface-selected)]'
}`}
>
{pageNum}
</button>
)
} else if (
pageNum === currentPage - 2 ||
pageNum === currentPage + 2
) {
return (
<span key={pageNum} className="text-[color:var(--color-text-muted)]">
...
</span>
)
}
return null
})}
</div>
<button
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className={`px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 ${
currentPage === totalPages
? 'bg-[color:var(--color-surface)] text-[color:var(--color-text-muted)] cursor-not-allowed'
: 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] hover:bg-[color:var(--color-success)]/80 hover:scale-105'
}`}
>
Következő
<FaChevronRight />
</button>
</div>
)}
</div> </div>
{/* Deck Info Popup */} {/* Deck Info Popup */}
@@ -79,13 +79,16 @@ export default function TaskCardEditor({ card, onChange }) {
} }
const updateAcceptedAnswer = (index, value) => { const updateAcceptedAnswer = (index, value) => {
const newAnswers = [...card.acceptedAnswers] const currentAnswers = card.acceptedAnswers || ['']
const newAnswers = [...currentAnswers]
newAnswers[index] = value newAnswers[index] = value
onChange({ acceptedAnswers: newAnswers }) onChange({ acceptedAnswers: newAnswers })
} }
const removeAcceptedAnswer = (index) => { const removeAcceptedAnswer = (index) => {
const newAnswers = card.acceptedAnswers.filter((_, i) => i !== 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 }) onChange({ acceptedAnswers: newAnswers })
} }
@@ -375,7 +378,7 @@ export default function TaskCardEditor({ card, onChange }) {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{(card.acceptedAnswers || ['', '', '']).map((answer, index) => ( {(card.acceptedAnswers || ['']).map((answer, index) => (
<div key={index} className="flex gap-2"> <div key={index} className="flex gap-2">
<input <input
type="text" type="text"
@@ -385,7 +388,7 @@ export default function TaskCardEditor({ card, onChange }) {
placeholder={`Elfogadott válasz ${index + 1}...`} placeholder={`Elfogadott válasz ${index + 1}...`}
/> />
{(card.acceptedAnswers?.length || 0) > 1 && ( {(card.acceptedAnswers?.length || 1) > 1 && (
<button <button
onClick={() => removeAcceptedAnswer(index)} 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" 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"
@@ -11,15 +11,15 @@ import { createDeck } from '../../api/deckApi'
import { notifySuccess, notifyError, notifyWarning } from "../../components/Toastify/toastifyServices" import { notifySuccess, notifyError, notifyWarning } from "../../components/Toastify/toastifyServices"
export default function DeckCreator() { export default function DeckCreator() {
const { deckId } = useParams() // URL-ből deck ID (új deck esetén undefined) const { deckId } = useParams()
const navigate = useNavigate() const navigate = useNavigate()
// Deck alapadatok // Deck alapadatok
const [deck, setDeck] = useState({ const [deck, setDeck] = useState({
id: null, id: null,
name: "Új Pakli", name: "Új Pakli",
type: "QUESTION", // QUESTION, LUCK, JOKER - backend formátum type: "QUESTION",
privacy: "private", // private, public privacy: "private",
description: "", description: "",
cards: [] cards: []
}) })
@@ -45,13 +45,26 @@ export default function DeckCreator() {
} }
}, [deckId]) }, [deckId])
const loadDeck = async (id) => {
// Mock adatok később backendből jön majd
const mockDeck = {
id,
name: "Demo Deck",
type: "QUESTION",
privacy: "public",
description: "Ez egy teszt deck",
cards: []
}
setDeck(mockDeck)
}
const handleDeckUpdate = (updates) => { const handleDeckUpdate = (updates) => {
setDeck(prev => ({ ...prev, ...updates })) setDeck(prev => ({ ...prev, ...updates }))
} }
const handleSaveDeck = async () => { const handleSaveDeck = async () => {
try { try {
// Konvertálás: Frontend string -> Backend enum number // Típus konverzió backendhez
const typeMapping = { const typeMapping = {
'LUCK': 0, 'LUCK': 0,
'JOKER': 1, 'JOKER': 1,
@@ -59,11 +72,11 @@ export default function DeckCreator() {
} }
const payload = { const payload = {
name: deck.name, name: deck.name?.trim() || "Névtelen pakli",
type: typeMapping[deck.type] ?? 2, // Alapértelmezett: QUESTION type: typeMapping[deck.type] ?? 2,
ctype: deck.privacy === 'public' ? 'PUBLIC' : 'PRIVATE', ctype: deck.privacy === 'public' ? 'PUBLIC' : 'PRIVATE',
description: deck.description || '', description: deck.description || "",
cards: deck.cards cards: deck.cards || []
} }
const saved = await createDeck(payload) const saved = await createDeck(payload)
@@ -75,16 +88,14 @@ export default function DeckCreator() {
})) }))
console.log('Deck saved (backend):', saved) console.log('Deck saved (backend):', saved)
notifySuccess('Deck sikeresen mentve!') notifySuccess('Pakli sikeresen elmentve!')
} catch (error) { } catch (error) {
console.error('Mentési hiba:', error) console.error('Mentési hiba:', error)
notifyError('Hiba történt a mentés során: ' + (error?.response?.data?.error || error.message || String(error))) notifyError('Hiba történt a mentés során: ' + (error?.response?.data?.error || error.message))
} }
} }
// 🔧 Itt korábban volt confirm(), de most eltávolítottuk
const handleBack = () => { const handleBack = () => {
// Egyszerű visszalépés — ha akarsz, később adhatunk hozzá saját modalt
navigate("/decks") navigate("/decks")
} }
@@ -100,9 +111,14 @@ export default function DeckCreator() {
setNewCardType(null) setNewCardType(null)
} }
// 💡 Demo verzió: beállítások szekció kihagyva
const handleSaveCard = (cardData) => { const handleSaveCard = (cardData) => {
try { try {
// Biztosítjuk az alapértelmezett consequence értékeket if (cardData.section === "settings") {
console.log("Beállítások szekció kihagyva (demo verzió)")
return
}
const defaultConsequence = { type: 0, value: 1 } const defaultConsequence = { type: 0, value: 1 }
const defaultWrongConsequence = { type: 1, value: 1 } const defaultWrongConsequence = { type: 1, value: 1 }
@@ -110,8 +126,7 @@ export default function DeckCreator() {
...cardData, ...cardData,
id: isCreatingCard ? Date.now() : cardData.id, id: isCreatingCard ? Date.now() : cardData.id,
consequence: cardData.consequence || defaultConsequence, consequence: cardData.consequence || defaultConsequence,
// wrongConsequence csak QUESTION és JOKER típusoknál ...(cardData.type === 'QUESTION' || cardData.type === 'JOKER' || cardData.type === 'PAIRING'
...(cardData.type === 'QUESTION' || cardData.type === 'JOKER'
? { wrongConsequence: cardData.wrongConsequence || defaultWrongConsequence } ? { wrongConsequence: cardData.wrongConsequence || defaultWrongConsequence }
: {} : {}
) )
@@ -120,13 +135,10 @@ export default function DeckCreator() {
let wasInvalidCardDeleted = false let wasInvalidCardDeleted = false
setDeck(prev => { setDeck(prev => {
// Ellenőrizzük, vannak-e nem megfelelő típusú kártyák
const invalidCards = prev.cards.filter(card => card.type !== prev.type) const invalidCards = prev.cards.filter(card => card.type !== prev.type)
// Ha új kártyát mentünk megfelelő típussal és vannak nem megfelelők
if (isCreatingCard && cardData.type === prev.type && invalidCards.length > 0) { if (isCreatingCard && cardData.type === prev.type && invalidCards.length > 0) {
wasInvalidCardDeleted = true wasInvalidCardDeleted = true
return { return {
...prev, ...prev,
cards: [ cards: [
@@ -136,7 +148,6 @@ export default function DeckCreator() {
} }
} }
// Alap mentési logika
return { return {
...prev, ...prev,
cards: isCreatingCard cards: isCreatingCard
@@ -149,7 +160,6 @@ export default function DeckCreator() {
setIsCreatingCard(false) setIsCreatingCard(false)
setNewCardType(null) setNewCardType(null)
// Csak egy értesítés
if (wasInvalidCardDeleted) { if (wasInvalidCardDeleted) {
const invalidCount = deck.cards.filter(card => card.type !== deck.type).length const invalidCount = deck.cards.filter(card => card.type !== deck.type).length
notifyWarning(`Kártya mentve! ${invalidCount} db nem megfelelő típusú kártya törlésre került.`) notifyWarning(`Kártya mentve! ${invalidCount} db nem megfelelő típusú kártya törlésre került.`)
@@ -162,8 +172,11 @@ export default function DeckCreator() {
} }
} }
// 🔧 Itt is confirm() volt — most a CardsList popupja kezeli a megerősítést // 💬 Felugró ablak törlés előtt
const handleDeleteCard = (cardId) => { const handleDeleteCard = (cardId) => {
const confirmDelete = window.confirm("Biztosan törölni szeretnéd ezt a kártyát?")
if (!confirmDelete) return
setDeck(prev => ({ setDeck(prev => ({
...prev, ...prev,
cards: prev.cards.filter(card => card.id !== cardId) cards: prev.cards.filter(card => card.id !== cardId)
@@ -172,6 +185,8 @@ export default function DeckCreator() {
if (selectedCard?.id === cardId) { if (selectedCard?.id === cardId) {
setSelectedCard(null) setSelectedCard(null)
} }
notifySuccess("Kártya törölve a pakliból!")
} }
return ( return (