@@ -0,0 +1,643 @@
import React , { useState , useEffect } from "react"
import { useParams , useNavigate } from "react-router-dom"
import {
FaArrowLeft ,
FaFilter ,
FaArrowUp ,
FaArrowDown ,
FaSortAlphaDown ,
FaSortAlphaUp ,
FaQuestionCircle ,
FaChevronLeft ,
FaChevronRight ,
} from "react-icons/fa"
import Navbar from "../../components/Navbar/Navbar"
import SearchBox from "../../components/Search/SearchBox"
import PopUp from "../../components/PopUp/PopUp"
import { getDeckById } from "../../api/deckApi"
const Card _display = ( ) => {
const { deckId } = useParams ( )
const navigate = useNavigate ( )
const [ deck , setDeck ] = useState ( null )
const [ cards , setCards ] = useState ( [ ] )
const [ loading , setLoading ] = useState ( true )
const [ error , setError ] = useState ( null )
const [ search , setSearch ] = useState ( "" )
const [ sortBy , setSortBy ] = useState ( "index" )
const [ showSortHelp , setShowSortHelp ] = useState ( false )
const [ itemsPerPage , setItemsPerPage ] = useState ( 20 )
const [ currentPage , setCurrentPage ] = useState ( 1 )
const [ flippedCards , setFlippedCards ] = useState ( new Set ( ) ) // Track which cards are flipped
// Load deck and parse cards
useEffect ( ( ) => {
let mounted = true
const load = async ( ) => {
setLoading ( true )
setError ( null )
try {
const result = await getDeckById ( deckId )
if ( ! mounted ) return
console . log ( 'Loaded deck:' , result )
setDeck ( result )
// Parse cards from JSON if it's a string
let parsedCards = [ ]
if ( result . cards ) {
if ( typeof result . cards === 'string' ) {
try {
parsedCards = JSON . parse ( result . cards )
} catch ( e ) {
console . error ( 'Failed to parse cards JSON:' , e )
}
} else if ( Array . isArray ( result . cards ) ) {
parsedCards = result . cards
}
}
console . log ( 'Parsed cards:' , parsedCards )
console . log ( 'First card structure:' , parsedCards [ 0 ] )
setCards ( parsedCards )
} catch ( err ) {
console . error ( 'Failed to load deck' , err )
if ( ! mounted ) return
setError ( err . message || 'Hiba történt a pakli betöltése közben.' )
} finally {
if ( mounted ) setLoading ( false )
}
}
load ( )
return ( ) => { mounted = false }
} , [ deckId ] )
// Filter logic
let filteredCards = cards . filter ( ( card ) => {
if ( ! search ) return true
const searchLower = search . toLowerCase ( )
// Check question, statement, and options
const questionText = 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 ) )
: Array . isArray ( card . answers )
? card . answers . some ( a => a && a . toLowerCase ( ) . includes ( searchLower ) )
: false
return questionMatch || answersMatch
} )
// Sort logic
filteredCards = [ ... filteredCards ] . sort ( ( a , b ) => {
if ( sortBy === "index" ) {
// Keep original order
return 0
} else if ( sortBy === "question-asc" ) {
const aText = a . question || a . statement || ''
const bText = 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 || ''
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
const bCount = Array . isArray ( b . options ) ? b . options . length : Array . isArray ( b . answers ) ? b . answers . length : 0
return aCount - bCount
} else if ( sortBy === "answers-desc" ) {
const aCount = Array . isArray ( a . options ) ? a . options . length : Array . isArray ( a . answers ) ? a . answers . length : 0
const bCount = Array . isArray ( b . options ) ? b . options . length : Array . isArray ( b . answers ) ? b . answers . length : 0
return bCount - aCount
}
return 0
} )
// Pagination logic
const totalCards = filteredCards . length
const totalPages = Math . ceil ( totalCards / itemsPerPage )
const startIndex = ( currentPage - 1 ) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const paginatedCards = filteredCards . slice ( startIndex , endIndex )
// Reset to page 1 when filters or items per page change
useEffect ( ( ) => {
setCurrentPage ( 1 )
} , [ search , sortBy , itemsPerPage ] )
const deckTypes = {
0 : { label : "Szerencse" , color : "var(--color-luck)" } ,
1 : { label : "Joker" , color : "var(--color-fun)" } ,
2 : { label : "Kérdés" , color : "var(--color-question)" } ,
}
// Card subtype Hungarian labels - UPDATED based on actual data
const cardSubTypeLabels = {
// String types (from DeckCreator)
"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" ,
// 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" ,
2 : "Szöveges válasz" ,
3 : "Igaz/Hamis" , // type 3 detected
4 : "Sorbarendezés" ,
5 : "Párosítás" ,
6 : "Kiegészítés"
}
const currentDeckType = deck ? ( deckTypes [ deck . type ] || { label : "Ismeretlen" , color : "var(--color-success)" } ) : null
const toggleCardFlip = ( cardId ) => {
setFlippedCards ( prev => {
const newSet = new Set ( prev )
if ( newSet . has ( cardId ) ) {
newSet . delete ( cardId )
} else {
newSet . add ( cardId )
}
return newSet
} )
}
return (
< div className = "w-full min-h-screen bg-[color:var(--color-background)] flex flex-col" >
< Navbar / >
< main className = "flex-1 w-full max-w-[1200px] mx-auto px-4 py-10" >
{ /* Header with back button */ }
< div className = "flex items-center gap-4 mb-6" >
< button
onClick = { ( ) => navigate ( '/decks' ) }
className = "flex items-center gap-2 px-4 py-2 rounded-xl bg-[color:var(--color-surface)] text-[color:var(--color-text)] hover:bg-[color:var(--color-surface-selected)] transition-all duration-200 shadow"
>
< FaArrowLeft / >
Vissza a paklikhoz
< / button >
{ deck && (
< div className = "flex items-center gap-3" >
< h1 className = "text-2xl font-bold text-[color:var(--color-text)]" > { deck . name } < / h1 >
{ currentDeckType && (
< span
className = "inline-block px-3 py-1 rounded-full text-sm font-bold"
style = { {
background : currentDeckType . color ,
color : "var(--color-text-inverse)" ,
} }
>
{ currentDeckType . label }
< / span >
) }
< / div >
) }
< / div >
{ /* Loading / Error states */ }
{ loading && (
< div className = "text-center text-[color:var(--color-text-muted)] py-10" >
Betöltés ...
< / div >
) }
{ error && (
< div className = "text-center text-[color:var(--color-error)] py-10" >
{ error }
< / div >
) }
{ /* Filters and controls */ }
{ ! loading && ! error && (
< >
< div className = "flex flex-col md:flex-row gap-3 justify-between items-center mb-6 bg-[color:var(--color-surface)]/80 backdrop-blur-lg rounded-2xl px-6 py-4 shadow-lg" >
< div className = "flex gap-2 items-center w-full md:w-auto" >
< SearchBox
value = { search }
onChange = { ( e ) => setSearch ( e . target . value ) }
width = { 300 }
placeholder = "Keresés kérdésben vagy válaszokban..."
className = "mr-4"
/ >
< span className = "text-[color:var(--color-text)] font-semibold mr-2 ml-2 flex items-center gap-1" >
Rendezés :
< button
type = "button"
className = "ml-1 text-[color:var(--color-success)] hover:text-[color:var(--color-text)] focus:outline-none"
onClick = { ( ) => setShowSortHelp ( true ) }
aria - label = "Rendezési magyarázat megnyitása"
style = { { fontSize : 18 , lineHeight : 1 } }
>
< FaQuestionCircle / >
< / button >
< / span >
< select
className = "px-3 py-1 rounded-lg bg-[color:var(--color-success)]/10 hover:bg-[color:var(--color-success)]/30 text-[color:var(--color-text)] border-none focus:ring-2 focus:ring-[color:var(--color-success)] outline-none"
style = { { backgroundColor : "rgba(0, 255, 0, 0.10)" } }
value = { sortBy }
onChange = { ( e ) => setSortBy ( e . target . value ) }
aria - label = "Rendezés"
>
< option
value = "index"
style = { { backgroundColor : "var(--color-surface)" , color : "var(--color-text)" } }
>
Eredeti sorrend
< / option >
< option
value = "question-asc"
style = { { backgroundColor : "var(--color-surface)" , color : "var(--color-text)" } }
>
Kérdés A → Z
< / option >
< option
value = "question-desc"
style = { { backgroundColor : "var(--color-surface)" , color : "var(--color-text)" } }
>
Kérdés Z → A
< / option >
< option
value = "answers-asc"
style = { { backgroundColor : "var(--color-surface)" , color : "var(--color-text)" } }
>
Válaszok száma ↑
< / option >
< option
value = "answers-desc"
style = { { backgroundColor : "var(--color-surface)" , color : "var(--color-text)" } }
>
Válaszok száma ↓
< / option >
< / select >
< / div >
< / div >
{ showSortHelp && (
< PopUp onClose = { ( ) => setShowSortHelp ( false ) } >
< h2 className = "text-lg font-bold mb-4" > Rendezési lehetőségek magyarázata < / h2 >
< ul className = "space-y-2 text-[color:var(--color-night)]" >
< li >
< span className = "font-bold" > Eredeti sorrend < / span > – A kártyák eredeti sorrendben jelennek meg
< / li >
< li >
< span className = "font-bold" > Kérdés A → Z < / span > – Kérdések ABC sorrendben ( A - tól Z - ig )
< / li >
< li >
< span className = "font-bold" > Kérdés Z → A < / span > – Kérdések fordított ABC sorrendben ( Z - től A - ig )
< / li >
< li >
< span className = "font-bold" > Válaszok száma ↑ < / span > – Kevesebb választól a több válasz felé
< / li >
< li >
< span className = "font-bold" > Válaszok száma ↓ < / span > – Több választól a kevesebb válasz felé
< / li >
< / ul >
< button
className = "mt-6 px-4 py-2 rounded-lg bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] font-semibold hover:bg-[color:var(--color-success)]/80 transition"
onClick = { ( ) => setShowSortHelp ( false ) }
>
Bezárás
< / button >
< / PopUp >
) }
{ /* 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 = { 10 } > 10 < / option >
< option value = { 20 } > 20 < / option >
< option value = { 30 } > 30 < / option >
< option value = { 50 } > 50 < / option >
< / select >
< / div >
< div className = "text-[color:var(--color-text-muted)] text-sm" >
{ totalCards > 0 ? (
< >
{ startIndex + 1 } - { Math . min ( endIndex , totalCards ) } / { totalCards } kártya
< / >
) : (
< > 0 kártya < / >
) }
< / div >
< / div >
{ /* Cards Grid */ }
< div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" >
{ totalCards === 0 && (
< div className = "col-span-full text-center text-[color:var(--color-text-muted)] py-10" >
Nincsenek kártyák ebben a pakliban .
< / div >
) }
{ paginatedCards . map ( ( card , idx ) => {
const cardIndex = startIndex + idx + 1
const questionText = card . question || card . statement || 'Kérdés hiányzik'
// Get answers based on card type
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 by fields if subType is missing
let detectedType = subType
if ( subType === 'undefined' || subType === 'null' ) {
// Check by numeric type field first
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
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'
}
}
if ( detectedType === 'truefalse' || detectedType === '0' ) {
// 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
const pairs = [ ]
for ( const [ leftIdx , rightIdx ] of Object . entries ( card . correctPairs ) ) {
const left = card . leftItems [ parseInt ( leftIdx ) ]
const right = card . rightItems [ parseInt ( rightIdx ) ]
if ( left && right ) {
pairs . push ( ` ${ left } → ${ right } ` )
}
}
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 ( card . options && Array . isArray ( card . options ) ) {
// Other types with options
answerOptions = card . options . filter ( opt => opt && opt . trim ( ) )
} else if ( card . answers && Array . isArray ( card . answers ) ) {
// Other card types with answers array
answerOptions = card . answers . filter ( opt => opt && opt . trim ( ) )
} else if ( card . acceptedAnswers && Array . isArray ( card . acceptedAnswers ) ) {
// Fallback for accepted answers
answerOptions = card . acceptedAnswers
correctAnswerIndex = - 1
}
const answerCount = answerOptions . length
const cardId = card . id || idx
const isFlipped = flippedCards . has ( cardId )
return (
< div
key = { cardId }
className = "relative h-80 cursor-pointer"
style = { { perspective : "1000px" } }
onClick = { ( ) => toggleCardFlip ( cardId ) }
>
< div
className = { ` relative w-full h-full transition-transform duration-500 ` }
style = { {
transformStyle : "preserve-3d" ,
transform : isFlipped ? "rotateY(180deg)" : "rotateY(0deg)"
} }
>
{ /* Front side - Question */ }
< div
className = "absolute w-full h-full bg-[color:var(--color-card)] rounded-2xl p-6 shadow-lg border-l-4"
style = { {
borderLeftColor : currentDeckType ? . color || "var(--color-success)" ,
backfaceVisibility : "hidden"
} }
>
< div className = "flex items-center justify-between mb-3" >
< span className = "text-[color:var(--color-text-muted)] text-sm font-medium" >
Kártya # { cardIndex }
< / span >
< span
className = "inline-block px-2 py-1 rounded-full text-xs font-bold"
style = { {
background : currentDeckType ? . color || "var(--color-success)" ,
color : "var(--color-text-inverse)" ,
} }
>
{ answerCount } válasz
< / span >
< / div >
< h3 className = "text-lg font-bold text-[color:var(--color-text)] mb-3" >
{ questionText }
< / h3 >
{ /* Type info only */ }
< div className = "absolute bottom-6 left-6 right-6 pt-3 border-t border-[color:var(--color-surface-selected)] text-xs text-[color:var(--color-text-muted)]" >
< div > Típus : < span className = "font-semibold" >
{ cardSubTypeLabels [ detectedType ] || cardSubTypeLabels [ card . subType ] || cardSubTypeLabels [ card . type ] || detectedType || 'Ismeretlen' }
< / span > < / div >
< div className = "text-center mt-3 text-[color:var(--color-text-muted)] italic" >
Kattints a megoldáshoz →
< / div >
< / div >
< / div >
{ /* Back side - Answer */ }
< div
className = "absolute w-full h-full bg-[color:var(--color-card)] rounded-2xl p-6 shadow-lg border-l-4 overflow-y-auto"
style = { {
borderLeftColor : currentDeckType ? . color || "var(--color-success)" ,
backfaceVisibility : "hidden" ,
transform : "rotateY(180deg)"
} }
>
< div className = "flex items-center justify-between mb-3" >
< span className = "text-[color:var(--color-text-muted)] text-sm font-medium" >
Megoldás
< / span >
< span
className = "inline-block px-2 py-1 rounded-full text-xs font-bold"
style = { {
background : currentDeckType ? . color || "var(--color-success)" ,
color : "var(--color-text-inverse)" ,
} }
>
{ answerCount } válasz
< / span >
< / div >
{ answerCount > 0 ? (
< div className = "space-y-2" >
< div className = "text-[color:var(--color-text-muted)] text-sm font-medium mb-2" >
Helyes válasz :
< / div >
{ detectedType === 'truefalse' || detectedType === '0' ? (
// 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' ? (
// Matching - show all correct pairs
< ul className = "space-y-2" >
{ answerOptions . map ( ( pair , idx ) => (
< li
key = { idx }
className = "text-[color:var(--color-text)] text-sm bg-[color:var(--color-success)]/20 rounded-lg px-3 py-2 border-l-2 border-[color:var(--color-success)] font-semibold"
>
✓ { pair }
< / li >
) ) }
< / ul >
) : ( detectedType === 'text' || detectedType === '2' ) && card . acceptedAnswers && Array . isArray ( card . acceptedAnswers ) ? (
// Text answers - show all accepted answers
< ul className = "space-y-1" >
{ answerOptions . map ( ( answer , ansIdx ) => (
< li
key = { ansIdx }
className = "text-[color:var(--color-text)] text-sm bg-[color:var(--color-success)]/20 rounded-lg px-3 py-2 border-l-2 border-[color:var(--color-success)] font-semibold"
>
✓ { answer }
< / li >
) ) }
< / ul >
) : (
// Multiple choice - 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 ] }
< / div >
) : (
< div className = "text-center text-[color:var(--color-text-muted)] py-4" >
Nincs megadva helyes válasz
< / div >
)
) }
< / div >
) : (
< div className = "text-center text-[color:var(--color-text-muted)] py-10" >
Nincs elérhető válasz
< / div >
) }
< div className = "absolute bottom-6 left-6 right-6 text-center text-xs text-[color:var(--color-text-muted)] italic" >
Kattints a kérdéshez ←
< / div >
< / div >
< / div >
< / 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
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 >
) }
< / >
) }
< / main >
< / div >
)
}
export default Card _display