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