diff --git a/SerpentRace_Frontend/src/App.jsx b/SerpentRace_Frontend/src/App.jsx
index 80aa0a79..ed9188d4 100644
--- a/SerpentRace_Frontend/src/App.jsx
+++ b/SerpentRace_Frontend/src/App.jsx
@@ -8,6 +8,7 @@ import ResetPassword from "./pages/Auth/ResetPassword"
import Landingpage from "./pages/Landing/Landingpage"
import Home from "./pages/Landing/Home"
import DeckManagerPage from "./pages/Decks/DeckManagerPage"
+import Card_display from "./pages/Decks/Card_display"
import DeckCreator from "./pages/DeckCreator/DeckCreator"
import CompanyHub from "./pages/Contacts/Contacts"
import About from "./pages/About/About"
@@ -61,6 +62,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/SerpentRace_Frontend/src/components/PopUp/DeckInfoPopUp.jsx b/SerpentRace_Frontend/src/components/PopUp/DeckInfoPopUp.jsx
index 888a37cd..1342de73 100644
--- a/SerpentRace_Frontend/src/components/PopUp/DeckInfoPopUp.jsx
+++ b/SerpentRace_Frontend/src/components/PopUp/DeckInfoPopUp.jsx
@@ -127,8 +127,19 @@ export default function DeckInfoPopUp({ deck, onClose }) {
}
const handleOpenDeck = () => {
- // TODO: Megnyitás funkció - később implementálható
- alert("⚠️ A pakli megnyitás funkció még fejlesztés alatt áll!")
+ // Get the deck ID from raw data
+ const deckId = rawData.id || deck.id
+
+ if (!deckId) {
+ alert("⚠️ Hiba: A pakli azonosítója nem található!")
+ return
+ }
+
+ // Navigate to card display page
+ navigate(`/deck/${deckId}`)
+
+ // Close the popup
+ onClose()
}
const handleEditDeck = () => {
diff --git a/SerpentRace_Frontend/src/pages/Decks/Card_display.jsx b/SerpentRace_Frontend/src/pages/Decks/Card_display.jsx
new file mode 100644
index 00000000..175aed1d
--- /dev/null
+++ b/SerpentRace_Frontend/src/pages/Decks/Card_display.jsx
@@ -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 (
+
+
+
+
+ {/* Header with back button */}
+
+
+ {deck && (
+
+
{deck.name}
+ {currentDeckType && (
+
+ {currentDeckType.label}
+
+ )}
+
+ )}
+
+
+ {/* Loading / Error states */}
+ {loading && (
+
+ Betöltés...
+
+ )}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Filters and controls */}
+ {!loading && !error && (
+ <>
+
+
+ setSearch(e.target.value)}
+ width={300}
+ placeholder="Keresés kérdésben vagy válaszokban..."
+ className="mr-4"
+ />
+
+ Rendezés:
+
+
+
+
+
+
+ {showSortHelp && (
+ setShowSortHelp(false)}>
+ Rendezési lehetőségek magyarázata
+
+ -
+ Eredeti sorrend – A kártyák eredeti sorrendben jelennek meg
+
+ -
+ Kérdés A→Z – Kérdések ABC sorrendben (A-tól Z-ig)
+
+ -
+ Kérdés Z→A – Kérdések fordított ABC sorrendben (Z-től A-ig)
+
+ -
+ Válaszok száma ↑ – Kevesebb választól a több válasz felé
+
+ -
+ Válaszok száma ↓ – Több választól a kevesebb válasz felé
+
+
+
+
+ )}
+
+ {/* Items per page selector and pagination info */}
+
+
+
+ Elemek oldalanként:
+
+
+
+
+
+ {totalCards > 0 ? (
+ <>
+ {startIndex + 1}-{Math.min(endIndex, totalCards)} / {totalCards} kártya
+ >
+ ) : (
+ <>0 kártya>
+ )}
+
+
+
+ {/* Cards Grid */}
+
+ {totalCards === 0 && (
+
+ Nincsenek kártyák ebben a pakliban.
+
+ )}
+ {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 (
+
toggleCardFlip(cardId)}
+ >
+
+ {/* Front side - Question */}
+
+
+
+ Kártya #{cardIndex}
+
+
+ {answerCount} válasz
+
+
+
+
+ {questionText}
+
+
+ {/* Type info only */}
+
+
Típus:
+ {cardSubTypeLabels[detectedType] || cardSubTypeLabels[card.subType] || cardSubTypeLabels[card.type] || detectedType || 'Ismeretlen'}
+
+
+ Kattints a megoldáshoz →
+
+
+
+
+ {/* Back side - Answer */}
+
+
+
+ Megoldás
+
+
+ {answerCount} válasz
+
+
+
+ {answerCount > 0 ? (
+
+
+ Helyes válasz:
+
+ {detectedType === 'truefalse' || detectedType === '0' ? (
+ // True/False - show only the correct answer
+
+ ✓ {card.isTrue ? 'Igaz' : 'Hamis'}
+
+ ) : detectedType === 'matching' || detectedType === '5' ? (
+ // Matching - show all correct pairs
+
+ {answerOptions.map((pair, idx) => (
+ -
+ ✓ {pair}
+
+ ))}
+
+ ) : (detectedType === 'text' || detectedType === '2') && card.acceptedAnswers && Array.isArray(card.acceptedAnswers) ? (
+ // Text answers - show all accepted answers
+
+ {answerOptions.map((answer, ansIdx) => (
+ -
+ ✓ {answer}
+
+ ))}
+
+ ) : (
+ // Multiple choice - show only the correct answer
+ correctAnswerIndex !== undefined && correctAnswerIndex !== -1 && answerOptions[correctAnswerIndex] ? (
+
+ ✓ {answerOptions[correctAnswerIndex]}
+
+ ) : (
+
+ Nincs megadva helyes válasz
+
+ )
+ )}
+
+ ) : (
+
+ Nincs elérhető válasz
+
+ )}
+
+
+ Kattints a kérdéshez ←
+
+
+
+
+ )
+ })}
+
+
+ {/* Pagination Controls */}
+ {totalPages > 1 && (
+
+
+
+
+ {[...Array(totalPages)].map((_, index) => {
+ const pageNum = index + 1
+ if (
+ pageNum === 1 ||
+ pageNum === totalPages ||
+ (pageNum >= currentPage - 1 && pageNum <= currentPage + 1)
+ ) {
+ return (
+
+ )
+ } else if (
+ pageNum === currentPage - 2 ||
+ pageNum === currentPage + 2
+ ) {
+ return (
+
+ ...
+
+ )
+ }
+ return null
+ })}
+
+
+
+
+ )}
+ >
+ )}
+
+
+ )
+}
+
+export default Card_display