Files
SerpentRace/SerpentRace_Frontend/src/pages/Game/CardDisplayModal.jsx
T
2025-11-24 23:28:57 +01:00

565 lines
24 KiB
React
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { CardType } from "../../constants/CardTypes"
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
import { arrayMove, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
/**
* Draggable item for sentence pairing
*/
const DraggableItem = ({ id, text, disabled }) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id, disabled })
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
}
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className={`bg-gray-800 border-2 border-purple-500 rounded-lg p-3 text-white ${
disabled ? 'cursor-default' : 'cursor-grab active:cursor-grabbing'
} ${isDragging ? 'shadow-lg' : ''}`}
>
{text}
</div>
)
}
/**
* CardDisplayModal - Kártya megjelenítése a játékos számára
*
* @param {Object} props
* @param {boolean} props.isOpen - Modal megjelenítése
* @param {Function} props.onClose - Modal bezárása
* @param {Object} props.card - Kártya adatok
* @param {string} props.cardType - Kártya típusa (QUESTION, LUCK, JOKER)
* @param {Function} props.onSubmitAnswer - Válasz beküldése (csak QUESTION típusnál)
* @param {number} props.timeLimit - Időkorlát másodpercben (default: 60)
* @param {boolean} props.isMyTurn - Whether this is the active player (can answer) or spectator (read-only)
* @param {string} props.submittedAnswer - For spectators, shows what the active player answered
*/
const CardDisplayModal = ({
isOpen,
onClose,
card,
cardType = "QUESTION",
onSubmitAnswer,
timeLimit = 60,
isMyTurn = true,
submittedAnswer = null
}) => {
const [playerAnswer, setPlayerAnswer] = useState("")
const [selectedOption, setSelectedOption] = useState(null)
const [timeLeft, setTimeLeft] = useState(timeLimit)
const [isProcessing, setIsProcessing] = useState(false)
// For sentence pairing drag and drop
const [rightItems, setRightItems] = useState([])
const sensors = useSensors(useSensor(PointerSensor))
// Timer countdown (only for active player)
useEffect(() => {
if (!isOpen || cardType !== "QUESTION" || !isMyTurn) return
setTimeLeft(timeLimit)
const timer = setInterval(() => {
setTimeLeft(prev => {
if (prev <= 1) {
clearInterval(timer)
handleTimeout()
return 0
}
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [isOpen, timeLimit, isMyTurn])
// Reset state when modal opens
useEffect(() => {
if (isOpen) {
setPlayerAnswer("")
setSelectedOption(null)
setIsProcessing(false)
// Initialize sentence pairing right items
if (card?.sentencePairs && card.sentencePairs.length > 0) {
setRightItems(card.sentencePairs.map(p => ({ id: p.id, text: p.right })))
}
}
}, [isOpen, card])
const handleTimeout = () => {
if (onSubmitAnswer) {
onSubmitAnswer(null) // null = timeout
}
}
const handleSubmit = async () => {
if (isProcessing || !isMyTurn) return
let answer = null
// Different answer formats based on card type
if (card?.type === CardType.SENTENCE_PAIRING && card?.sentencePairs) {
// Answer is array of pairs
answer = card.sentencePairs.map((leftPair, index) => ({
pairId: leftPair.id,
leftText: leftPair.left,
rightText: rightItems[index].text
}))
} else if (selectedOption !== null) {
answer = selectedOption
} else {
answer = playerAnswer.trim()
}
if (!answer || (Array.isArray(answer) && answer.length === 0)) {
console.warn('⚠️ No answer provided')
return
}
console.log('📝 Submitting answer:', answer)
setIsProcessing(true)
try {
await onSubmitAnswer(answer)
} catch (error) {
console.error("❌ Válasz küldési hiba:", error)
setIsProcessing(false)
}
}
const handleDragEnd = (event) => {
const { active, over } = event
if (active.id !== over.id) {
setRightItems((items) => {
const oldIndex = items.findIndex(item => item.id === active.id)
const newIndex = items.findIndex(item => item.id === over.id)
return arrayMove(items, oldIndex, newIndex)
})
}
}
const getCardIcon = () => {
switch (cardType) {
case "QUESTION": return "❓"
case "LUCK": return "🍀"
case "JOKER": return "🃏"
default: return "📝"
}
}
const getCardTitle = () => {
switch (cardType) {
case "QUESTION": return "Feladat Kártya"
case "LUCK": return "Szerencse Kártya"
case "JOKER": return "Joker Kártya Feladat"
default: return "Kártya"
}
}
const getCardBgGradient = () => {
switch (cardType) {
case "QUESTION": return "from-blue-600 via-purple-600 to-blue-600"
case "LUCK": return "from-green-600 via-teal-600 to-green-600"
case "JOKER": return "from-purple-600 via-pink-600 to-purple-600"
default: return "from-gray-600 via-gray-700 to-gray-600"
}
}
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}:${secs.toString().padStart(2, '0')}`
}
const getTimeColor = () => {
if (timeLeft > 30) return "text-green-400"
if (timeLeft > 10) return "text-yellow-400"
return "text-red-400 animate-pulse"
}
if (!isOpen || !card) return null
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
/>
{/* Modal Content */}
<motion.div
initial={{ scale: 0.9, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.9, opacity: 0, y: 20 }}
transition={{ type: "spring", duration: 0.5 }}
className="relative bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 rounded-2xl shadow-2xl border-2 border-purple-500/30 max-w-2xl w-full overflow-hidden max-h-[90vh] overflow-y-auto"
>
{/* Header */}
<div className={`bg-gradient-to-r ${getCardBgGradient()} p-6 relative overflow-hidden`}>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-pulse" />
<div className="relative flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="text-5xl animate-bounce">{getCardIcon()}</div>
<div>
<h2 className="text-2xl font-bold text-white">{getCardTitle()}</h2>
{cardType === "QUESTION" && (
<p className="text-white/80 text-sm">
{isMyTurn ? "Válaszolj a kérdésre!" : "Néző mód - Várakozás a játékosra"}
</p>
)}
{cardType === "JOKER" && (
<p className="text-purple-100 text-sm">
{isMyTurn ? "Gamemaster jóváhagyás szükséges" : "Várakozás a Gamemaster döntésére"}
</p>
)}
</div>
</div>
{/* Timer - csak QUESTION típusnál és aktív játékosnál */}
{cardType === "QUESTION" && isMyTurn && (
<div className="bg-black/30 rounded-lg px-4 py-2">
<div className={`text-2xl font-bold ${getTimeColor()}`}>
{formatTime(timeLeft)}
</div>
</div>
)}
</div>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{/* JOKER CARD SPECIFIC LAYOUT */}
{cardType === "JOKER" ? (
<>
{/* Player Info */}
<div className="bg-gray-800/50 rounded-xl p-4 border border-gray-700">
<div className="flex items-center gap-3">
<div className="text-4xl">{card.playerEmoji || "🎭"}</div>
<div>
<p className="text-gray-400 text-sm">Játékos</p>
<p className="text-white font-semibold text-lg">{card.playerName}</p>
</div>
</div>
</div>
{/* Joker Card Details */}
<div className="bg-gradient-to-br from-purple-900/30 to-pink-900/30 rounded-xl p-5 border border-purple-500/30">
<div className="flex items-start gap-3 mb-3">
<div className="text-3xl">🎯</div>
<div className="flex-1">
<h3 className="text-purple-300 font-semibold mb-2">Feladat címe</h3>
<p className="text-white text-lg font-medium">
{card.question || "Joker Kártya Feladat"}
</p>
</div>
</div>
{card.consequence && (
<div className="flex items-start gap-3">
<div className="text-2xl">📝</div>
<div className="flex-1">
<h3 className="text-purple-300 font-semibold mb-2">Feladat leírása</h3>
<p className="text-gray-300 leading-relaxed">
{card.consequence}
</p>
</div>
</div>
)}
{/* Points Info (if available) */}
{card.points && (
<div className="mt-4 pt-4 border-t border-purple-500/20">
<div className="flex items-center gap-2">
<span className="text-2xl"></span>
<span className="text-yellow-400 font-bold text-lg">
{card.points} pont
</span>
<span className="text-gray-400 text-sm">járható érte</span>
</div>
</div>
)}
</div>
{/* Waiting for gamemaster message */}
{card.waitingForGamemaster && (
<div className="bg-yellow-900/20 rounded-xl p-4 border border-yellow-500/30">
<div className="flex items-start gap-3">
<div className="text-2xl"></div>
<div className="flex-1">
<p className="text-yellow-200 text-sm">
<strong>Várakozás:</strong> A Gamemaster döntése szükséges a folytatáshoz.
</p>
</div>
</div>
</div>
)}
</>
) : (
<>
{/* REGULAR CARD LAYOUT */}
{/* Spectator Notice */}
{!isMyTurn && (
<div className="bg-blue-900/30 rounded-xl p-4 border border-blue-500/50 text-center">
<p className="text-blue-300 font-semibold">👀 Néző módban vagy</p>
<p className="text-gray-300 text-sm mt-1">Várakozás a játékos válaszára...</p>
</div>
)}
{/* Question/Text */}
<div className="bg-gray-800/50 rounded-xl p-5 border border-gray-700">
<div className="flex items-start gap-3">
<div className="text-3xl">📝</div>
<div className="flex-1">
<p className="text-white text-lg leading-relaxed">
{card.question || card.text || card.statement}
</p>
</div>
</div>
</div>
</>
)}
{/* QUIZ TYPE - Four Buttons (A, B, C, D) */}
{cardType === "QUESTION" && card.type === CardType.QUIZ && card.answerOptions && card.answerOptions.length > 0 && (
<div className="space-y-3">
<h3 className="text-purple-300 font-semibold">
{isMyTurn ? "Válaszd ki a helyes választ:" : "Válasz lehetőségek:"}
</h3>
<div className="grid grid-cols-1 gap-3">
{card.answerOptions.map((option, index) => (
<button
key={index}
onClick={() => isMyTurn && setSelectedOption(option.answer)}
disabled={!isMyTurn || isProcessing}
className={`w-full text-left p-4 rounded-lg border-2 transition-all ${
selectedOption === option.answer
? "bg-purple-600 border-purple-400 text-white scale-105"
: submittedAnswer === option.answer
? "bg-blue-600 border-blue-400 text-white"
: "bg-gray-800 border-gray-600 text-gray-300"
} ${isMyTurn && !isProcessing ? "hover:border-purple-500 cursor-pointer" : "cursor-default"} ${
!isMyTurn ? "opacity-75" : ""
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl font-bold bg-gray-900/50 rounded-full w-10 h-10 flex items-center justify-center">
{option.answer}
</span>
<span className="flex-1">{option.text}</span>
</div>
</button>
))}
</div>
{!isMyTurn && submittedAnswer && (
<p className="text-blue-300 text-center text-sm">
A játékos választása: <span className="font-bold">{submittedAnswer}</span>
</p>
)}
</div>
)}
{/* TRUE/FALSE TYPE - Two Buttons */}
{cardType === "QUESTION" && card.type === CardType.TRUE_FALSE && (
<div className="space-y-3">
<h3 className="text-purple-300 font-semibold text-center">
{isMyTurn ? "Igaz vagy Hamis?" : "Válasz lehetőségek:"}
</h3>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => isMyTurn && setSelectedOption("Igaz")}
disabled={!isMyTurn || isProcessing}
className={`p-6 rounded-xl border-2 transition-all ${
selectedOption === "Igaz"
? "bg-green-600 border-green-400 text-white scale-105"
: submittedAnswer === "Igaz"
? "bg-blue-600 border-blue-400 text-white"
: "bg-gray-800 border-gray-600 text-gray-300"
} ${isMyTurn && !isProcessing ? "hover:border-green-500 cursor-pointer" : "cursor-default"}`}
>
<div className="text-center">
<div className="text-4xl mb-2"></div>
<div className="text-xl font-bold">IGAZ</div>
</div>
</button>
<button
onClick={() => isMyTurn && setSelectedOption("Hamis")}
disabled={!isMyTurn || isProcessing}
className={`p-6 rounded-xl border-2 transition-all ${
selectedOption === "Hamis"
? "bg-red-600 border-red-400 text-white scale-105"
: submittedAnswer === "Hamis"
? "bg-blue-600 border-blue-400 text-white"
: "bg-gray-800 border-gray-600 text-gray-300"
} ${isMyTurn && !isProcessing ? "hover:border-red-500 cursor-pointer" : "cursor-default"}`}
>
<div className="text-center">
<div className="text-4xl mb-2"></div>
<div className="text-xl font-bold">HAMIS</div>
</div>
</button>
</div>
{!isMyTurn && submittedAnswer && (
<p className="text-blue-300 text-center text-sm">
A játékos választása: <span className="font-bold">{submittedAnswer}</span>
</p>
)}
</div>
)}
{/* SENTENCE_PAIRING TYPE - Drag and Drop */}
{cardType === "QUESTION" && card.type === CardType.SENTENCE_PAIRING && card.sentencePairs && card.sentencePairs.length > 0 && (
<div className="space-y-4">
<h3 className="text-purple-300 font-semibold text-center">
{isMyTurn ? "Párosítsd a mondatokat! (Húzd a jobb oldali elemeket)" : "Mondatpárosítás:"}
</h3>
<div className="grid grid-cols-2 gap-4">
{/* Left column - fixed */}
<div className="space-y-3">
<p className="text-gray-400 text-sm text-center font-semibold">Bal oldal</p>
{card.sentencePairs.map((pair, index) => (
<div
key={`left-${pair.id}`}
className="bg-gray-800 border-2 border-blue-500 rounded-lg p-3 text-white"
>
{pair.left}
</div>
))}
</div>
{/* Right column - draggable */}
<div className="space-y-3">
<p className="text-gray-400 text-sm text-center font-semibold">
{isMyTurn ? "Jobb oldal (húzható)" : "Jobb oldal"}
</p>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={rightItems.map(item => item.id)}
strategy={verticalListSortingStrategy}
disabled={!isMyTurn}
>
{rightItems.map((item) => (
<DraggableItem
key={item.id}
id={item.id}
text={item.text}
disabled={!isMyTurn}
/>
))}
</SortableContext>
</DndContext>
</div>
</div>
{!isMyTurn && (
<p className="text-blue-300 text-center text-sm">
Várakozás a játékos párosítására...
</p>
)}
</div>
)}
{/* OWN_ANSWER and CLOSER - Text Input */}
{cardType === "QUESTION" && (card.type === CardType.OWN_ANSWER || card.type === CardType.CLOSER) && (
<div className="space-y-3">
<h3 className="text-purple-300 font-semibold">
{isMyTurn ? "Írd be a választ:" : "A játékos válasza:"}
</h3>
<input
type="text"
value={isMyTurn ? playerAnswer : submittedAnswer || "Várakozás..."}
onChange={(e) => isMyTurn && setPlayerAnswer(e.target.value)}
onKeyPress={(e) => isMyTurn && e.key === 'Enter' && !isProcessing && playerAnswer.trim() && handleSubmit()}
disabled={!isMyTurn || isProcessing}
placeholder={card.type === CardType.CLOSER ? "Számot adj meg" : "Válaszod..."}
className={`w-full bg-gray-800 border-2 border-gray-600 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none disabled:opacity-50 ${
!isMyTurn ? "cursor-default" : ""
}`}
/>
</div>
)}
{/* Hint (if available) */}
{card.hint && isMyTurn && (
<div className="bg-yellow-900/20 rounded-xl p-4 border border-yellow-500/30">
<div className="flex items-start gap-3">
<div className="text-2xl">💡</div>
<div className="flex-1">
<h3 className="text-yellow-300 font-semibold mb-2">Segítség</h3>
<p className="text-gray-300 text-sm">{card.hint}</p>
</div>
</div>
</div>
)}
{/* Submit Button - csak QUESTION típusnál és aktív játékosnál */}
{cardType === "QUESTION" && isMyTurn && (
<button
onClick={handleSubmit}
disabled={isProcessing || (!playerAnswer.trim() && selectedOption === null && card.type !== CardType.SENTENCE_PAIRING)}
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500
text-white font-bold py-4 px-6 rounded-xl shadow-lg
transform transition-all duration-200 hover:scale-105 active:scale-95
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100
border border-purple-500/50"
>
<div className="flex items-center justify-center gap-2">
<span className="text-2xl"></span>
<span className="text-lg">
{isProcessing ? "Feldolgozás..." : "Válasz beküldése"}
</span>
</div>
</button>
)}
{/* Close Button - LUCK és JOKER típusnál vagy nézőknél */}
{((cardType === "LUCK" || cardType === "JOKER") || !isMyTurn) && (
<button
onClick={onClose}
className="w-full bg-gradient-to-r from-green-600 to-teal-600 hover:from-green-500 hover:to-teal-500
text-white font-bold py-4 px-6 rounded-xl shadow-lg
transform transition-all duration-200 hover:scale-105 active:scale-95
border border-green-500/50"
>
<div className="flex items-center justify-center gap-2">
<span className="text-2xl">👍</span>
<span className="text-lg">Rendben</span>
</div>
</button>
)}
</div>
</motion.div>
</div>
)}
</AnimatePresence>
)
}
export default CardDisplayModal