diff --git a/SerpentRace_Docker/docker-compose.watch.yml b/SerpentRace_Docker/docker-compose.watch.yml index b7e523ca..8d026a06 100644 --- a/SerpentRace_Docker/docker-compose.watch.yml +++ b/SerpentRace_Docker/docker-compose.watch.yml @@ -75,7 +75,9 @@ services: environment: - NODE_ENV=development - VITE_API_URL=http://localhost:3000 - volumes: [] + volumes: + - ../SerpentRace_Frontend:/app + - /app/node_modules develop: watch: - action: sync diff --git a/SerpentRace_Frontend/package-lock.json b/SerpentRace_Frontend/package-lock.json index e8e4cd88..02104eac 100644 --- a/SerpentRace_Frontend/package-lock.json +++ b/SerpentRace_Frontend/package-lock.json @@ -15,6 +15,7 @@ "react-dom": "^19.1.0", "react-icons": "^5.5.0", "react-router-dom": "^7.6.0", + "react-toastify": "^11.0.5", "tailwindcss": "^4.1.7" }, "devDependencies": { @@ -1816,6 +1817,15 @@ "node": ">=18" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3363,6 +3373,19 @@ "node": ">=18" } }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/SerpentRace_Frontend/package.json b/SerpentRace_Frontend/package.json index 5bb119a6..d06aa2d6 100644 --- a/SerpentRace_Frontend/package.json +++ b/SerpentRace_Frontend/package.json @@ -17,6 +17,7 @@ "react-dom": "^19.1.0", "react-icons": "^5.5.0", "react-router-dom": "^7.6.0", + "react-toastify": "^11.0.5", "tailwindcss": "^4.1.7" }, "devDependencies": { diff --git a/SerpentRace_Frontend/src/components/DeckCreator/CardEditor.jsx b/SerpentRace_Frontend/src/components/DeckCreator/CardEditor.jsx index 8fb712f7..2fc5cbe3 100644 --- a/SerpentRace_Frontend/src/components/DeckCreator/CardEditor.jsx +++ b/SerpentRace_Frontend/src/components/DeckCreator/CardEditor.jsx @@ -20,29 +20,32 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance id: null, type: type, points: 10, - timeLimit: 30 + timeLimit: 30, + consequence: { type: 0, value: 1 } } switch (type) { - case 'task': + case 'QUESTION': return { ...baseData, subType: 'quiz', question: '', options: ['', '', '', ''], correctAnswer: 0, - explanation: '' + explanation: '', + wrongConsequence: { type: 1, value: 1 } } - case 'joker': + case 'JOKER': return { ...baseData, title: '', description: '', effect: '', actionType: 'skip', - usage: 'once' + usage: 'once', + wrongConsequence: { type: 1, value: 1 } } - case 'luck': + case 'LUCK': return { ...baseData, event: '', @@ -58,38 +61,55 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance // Kártya adatok inicializálása useEffect(() => { - if (isCreating && cardType) { - setCardData(getDefaultCardData(cardType)) - } else if (card) { - setCardData({ ...card }) - } else { + try { + if (isCreating && cardType) { + const defaultData = getDefaultCardData(cardType) + setCardData(defaultData) + } else if (card) { + setCardData({ ...card }) + } else { + setCardData(null) + } + } catch (error) { + console.error('Kártya inicializálási hiba:', error) setCardData(null) } }, [card, isCreating, cardType]) const validateCard = (data) => { - if (data.type === 'task') { - if (!data.question && !data.statement) { - notifyError("Kérdés vagy állítás megadása kötelező!") + try { + if (!data || !data.type) { + notifyError("Érvénytelen kártya adatok!") return false } - if (data.subType === 'quiz' && data.options.some(opt => !opt.trim())) { - notifyError("Minden válaszlehetőséget ki kell tölteni!") - return false - } - } else if (data.type === 'joker') { - if (!data.text || !data.text.trim()) { - notifyError("Joker kártya szövege nem lehet üres!") - return false - } - } else if (data.type === 'luck') { - if (!data.text || !data.text.trim()) { - notifyError("Szerencse kártya szövege nem lehet üres!") - return false + + if (data.type === 'QUESTION') { + if (!data.question && !data.statement) { + notifyError("Kérdés vagy állítás megadása kötelező!") + return false + } + if (data.subType === 'quiz' && data.options && data.options.some(opt => !opt.trim())) { + notifyError("Minden válaszlehetőséget ki kell tölteni!") + return false + } + } else if (data.type === 'JOKER') { + if (!data.text || !data.text.trim()) { + notifyError("Joker kártya szövege nem lehet üres!") + return false + } + } else if (data.type === 'LUCK') { + if (!data.text || !data.text.trim()) { + notifyError("Szerencse kártya szövege nem lehet üres!") + return false + } } + + return true + } catch (error) { + console.error('Validálási hiba:', error) + notifyError("Hiba történt a kártya ellenőrzése során") + return false } - - return true } const updateCardData = (updates) => { @@ -97,16 +117,14 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance } const handleSave = () => { - if (!cardData) return + if (!cardData) { + notifyError("Nincs mentendő kártya adat!") + return + } if (!validateCard(cardData)) return - try { - onSave(cardData) - notifySuccess('Kártya sikeresen mentve!') - } catch (error) { - notifyError('Hiba történt a kártya mentése során: ' + (error?.message || String(error))) - } + onSave(cardData) } // Ha nincs kiválasztott kártya vagy új kártya létrehozás @@ -129,24 +147,47 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance return (
+ {/* Type Mismatch Warning */} + {cardData?.type && cardType && cardData.type !== cardType && !isCreating && ( +
+
+
⚠️
+
+
+ Figyelmeztetés: Nem megfelelő kártya típus +
+
+ {`Ez egy ${ + cardData.type === 'QUESTION' ? 'Feladat' : + cardData.type === 'JOKER' ? 'Joker' : 'Szerencse' + } kártya, de a pakli típusa ${ + cardType === 'QUESTION' ? 'Feladat' : + cardType === 'JOKER' ? 'Joker' : 'Szerencse' + }.`} +
+
+
+
+ )} + {/* Header */}
- {cardData.type === 'task' && '📋'} - {cardData.type === 'joker' && '🃏'} - {cardData.type === 'luck' && '🎲'} + {cardData.type === 'QUESTION' && '📋'} + {cardData.type === 'JOKER' && '🃏'} + {cardData.type === 'LUCK' && '🎲'}

- {isCreating ? 'Új' : 'Szerkesztés'}{' '} - {cardData.type === 'task' && 'Feladat kártya'} - {cardData.type === 'joker' && 'Joker kártya'} - {cardData.type === 'luck' && 'Szerencse kártya'} + {isCreating ? 'Új' : 'Szerkesztés'} {' '} + {(isCreating ? cardType : cardData.type) === 'QUESTION' && 'Feladat kártya'} + {(isCreating ? cardType : cardData.type) === 'JOKER' && 'Joker kártya'} + {(isCreating ? cardType : cardData.type) === 'LUCK' && 'Szerencse kártya'}

- {cardData.type === 'task' && cardData.subType && ( + {cardData.type === 'QUESTION' && cardData.subType && ( <> {cardData.subType === 'quiz' && 'Quiz (A/B/C/D)'} {cardData.subType === 'truefalse' && 'Igaz/Hamis'} @@ -203,21 +244,21 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
) : (
- {cardData.type === 'task' && ( + {cardData.type === 'QUESTION' && ( )} - {cardData.type === 'joker' && ( + {cardData.type === 'JOKER' && ( )} - {cardData.type === 'luck' && ( + {cardData.type === 'LUCK' && ( { - if (card.type === "task") { - return card.question || card.statement || "Új feladat kártya" + if (card.type === 'QUESTION') { + return card.question || card.statement || 'Új feladat kártya' } - if (card.type === "joker") { - return card.text || "Új joker kártya" + if (card.type === 'JOKER') { + return card.text || 'Új joker kártya' } - if (card.type === "luck") { - return card.text || "Új szerencse kártya" + if (card.type === 'LUCK') { + return card.text || 'Új szerencse kártya' } return "Ismeretlen kártya" } const getCardTypeLabel = (card) => { - if (card.type === "task") { + if (card.type === 'QUESTION') { if (card.subType) { return cardSubTypeLabels[card.subType] || "Feladat" } return "Feladat" } - if (card.type === "joker") { - return "Joker" + if (card.type === 'JOKER') { + return 'Joker' } - if (card.type === "luck") { - return "Szerencse" + if (card.type === 'LUCK') { + return 'Szerencse' } return "Ismeretlen" } @@ -87,40 +88,22 @@ export default function CardsList({ 🃏 Kártyák - {/* New Card Dropdown */} -
- - - {/* Dropdown Menu */} -
- - - - - -
-
+ {/* New Card Button */} +
{/* Cards List */} @@ -138,7 +121,7 @@ export default function CardsList({ )}
- Új {newCardType === "task" ? "feladat" : newCardType === "joker" ? "joker" : "szerencse"} kártya + Új {newCardType === "QUESTION" ? "feladat" : newCardType === "JOKER" ? "joker" : "szerencse"} kártya
Szerkesztés folyamatban... @@ -158,14 +141,24 @@ export default function CardsList({ key={card.id} onClick={() => onSelectCard(card)} className={` - p-4 rounded-xl border cursor-pointer transition-all duration-200 hover:scale-105 group + p-4 rounded-xl border cursor-pointer transition-all duration-200 hover:scale-105 group relative ${ isSelected ? "bg-[color:var(--color-success)]/10 border-[color:var(--color-success)] shadow-lg" : "bg-[color:var(--color-background)]/50 border-[color:var(--color-surface-selected)] hover:bg-[color:var(--color-background)]/80" } + ${card.type !== deckType ? "opacity-70" : ""} `} > + {card.type !== deckType && ( +
+
+
+ + ⚠️ Nem megfelelő típus + +
+ )} {/* Card Header */}
@@ -270,14 +263,6 @@ export default function CardsList({
📊 Összesen: {cards.length} kártya
- - {cards.length > 0 && ( -
- 📋 {cards.filter((c) => c.type === "task").length} - 🃏 {cards.filter((c) => c.type === "joker").length} - 🎲 {cards.filter((c) => c.type === "luck").length} -
- )}
diff --git a/SerpentRace_Frontend/src/components/DeckCreator/DeckHeader.jsx b/SerpentRace_Frontend/src/components/DeckCreator/DeckHeader.jsx index 2aaa8626..b285dd5e 100644 --- a/SerpentRace_Frontend/src/components/DeckCreator/DeckHeader.jsx +++ b/SerpentRace_Frontend/src/components/DeckCreator/DeckHeader.jsx @@ -1,13 +1,13 @@ // src/components/DeckCreator/DeckHeader.jsx // Deck alapadatok szerkesztése és mentés -import React from "react" +import React, { useState, useRef, useEffect } from "react" import { FaSave, FaArrowLeft, FaGlobe, FaLock, FaQuestionCircle, FaDice, FaLaughBeam } from "react-icons/fa" const deckTypes = [ - { value: "Question", label: "Kérdés", icon: FaQuestionCircle, color: "var(--color-question)" }, - { value: "Luck", label: "Szerencse", icon: FaDice, color: "var(--color-luck)" }, - { value: "Fun", label: "Szórakozás", icon: FaLaughBeam, color: "var(--color-fun)" } + { value: "QUESTION", label: "Kérdés", icon: FaQuestionCircle, color: "var(--color-question)" }, + { value: "LUCK", label: "Szerencse", icon: FaDice, color: "var(--color-luck)" }, + { value: "JOKER", label: "Joker", icon: FaLaughBeam, color: "var(--color-fun)" } ] const privacyOptions = [ @@ -16,17 +16,35 @@ const privacyOptions = [ ] export default function DeckHeader({ deck, onUpdate, onSave, onBack }) { + const [isTypeDropdownOpen, setIsTypeDropdownOpen] = useState(false); + const [isPrivacyDropdownOpen, setIsPrivacyDropdownOpen] = useState(false); + const typeDropdownRef = useRef(null); + const privacyDropdownRef = useRef(null); + const currentDeckType = deckTypes.find(type => type.value === deck.type) || deckTypes[0] const currentPrivacy = privacyOptions.find(option => option.value === deck.privacy) || privacyOptions[0] + useEffect(() => { + function handleClickOutside(event) { + if (typeDropdownRef.current && !typeDropdownRef.current.contains(event.target)) { + setIsTypeDropdownOpen(false); + } + if (privacyDropdownRef.current && !privacyDropdownRef.current.contains(event.target)) { + setIsPrivacyDropdownOpen(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + const handleInputChange = (field, value) => { onUpdate({ [field]: value }) } - const cardsCount = deck.cards?.length || 0 - const taskCards = deck.cards?.filter(card => card.type === 'task')?.length || 0 - const jokerCards = deck.cards?.filter(card => card.type === 'joker')?.length || 0 - const luckCards = deck.cards?.filter(card => card.type === 'luck')?.length || 0 + // Remove unused card count variables return (
@@ -42,7 +60,7 @@ export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {

- 📝 Deck Szerkesztés + 📝 Pakli Szerkesztés

@@ -56,104 +74,146 @@ export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {
{/* Main Content Row */} -
- {/* Deck Basic Info */} -
- {/* Deck Name */} -
+
+ {/* Two Column Layout */} +
+ {/* Deck Name - Takes up 2 columns */} +
handleInputChange('name', e.target.value)} className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200" - placeholder="Add meg a deck nevét..." + placeholder="Add meg a pakli nevét..." />
- - {/* Type and Privacy Row */} -
- {/* Deck Type */} -
- - -
- - {/* Privacy */} -
- - -
- - {/* Description */} -
- - handleInputChange('description', e.target.value)} - className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200" - placeholder="Rövid leírás..." - /> -
-
+ + {/* Empty space for visual balance */} +
- {/* Stats Panel */} -
-

- 📊 Statisztikák -

- -
-
- Összes kártya: - {cardsCount} + {/* Type, Privacy and Description Row */} +
+ {/* Deck Type */} +
+ +
+ + + {isTypeDropdownOpen && ( +
+ {deckTypes.map(type => ( + + ))} +
+ )}
- -
- 📋 Feladat: - {taskCards} -
- -
- 🃏 Joker: - {jokerCards} -
- -
- 🎲 Szerencse: - {luckCards} +
+ + {/* Privacy */} +
+ +
+ + + {isPrivacyDropdownOpen && ( +
+ {privacyOptions.map(option => ( + + ))} +
+ )}
+ + {/* Description */} +
+ + handleInputChange('description', e.target.value)} + className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200" + placeholder="Rövid leírás..." + /> +
diff --git a/SerpentRace_Frontend/src/components/DeckCreator/DeckManager.jsx b/SerpentRace_Frontend/src/components/DeckCreator/DeckManager.jsx index 0b1eddac..6aa60f09 100644 --- a/SerpentRace_Frontend/src/components/DeckCreator/DeckManager.jsx +++ b/SerpentRace_Frontend/src/components/DeckCreator/DeckManager.jsx @@ -17,7 +17,7 @@ import DeckInfoPopUp from "../PopUp/DeckInfoPopUp" const deckTypes = [ { label: "Luck", color: "var(--color-luck)" }, { label: "Question", color: "var(--color-question)" }, - { label: "Fun", color: "var(--color-fun)" }, + { label: "Joker", color: "var(--color-fun)" }, ] // initial state will be fetched from backend @@ -163,8 +163,8 @@ const DeckManager = () => { ? "Szerencse" : type.label === "Question" ? "Kérdés" - : type.label === "Fun" - ? "Szórakozás" + : type.label === "Joker" + ? "Joker" : type.label} ))} @@ -303,7 +303,7 @@ const DeckManager = () => { : deck.type === "Question" ? "Kérdés" : deck.type === "Fun" - ? "Szórakozás" + ? "Joker" : deck.type}

diff --git a/SerpentRace_Frontend/src/components/DeckCreator/JokerCardEditor.jsx b/SerpentRace_Frontend/src/components/DeckCreator/JokerCardEditor.jsx index c902cade..5f8118e6 100644 --- a/SerpentRace_Frontend/src/components/DeckCreator/JokerCardEditor.jsx +++ b/SerpentRace_Frontend/src/components/DeckCreator/JokerCardEditor.jsx @@ -4,17 +4,29 @@ import React, { useState, useEffect } from 'react' import { FaTheaterMasks, FaInfoCircle, FaUsers } from 'react-icons/fa' +const consequenceTypes = [ + { value: 0, label: '⬆️ Előre lépés', description: 'A játékos előre lép X mezőt' }, + { value: 1, label: '⬇️ Hátra lépés', description: 'A játékos hátra lép X mezőt' }, + { value: 2, label: '⏸️ Kör kihagyás', description: 'A játékos kihagy egy kört' }, + { value: 3, label: '⏩ Extra kör', description: 'A játékos kap egy extra kört' }, + { value: 5, label: '🏁 Vissza a starthoz', description: 'A játékos visszakerül a starthoz' } +] + export default function JokerCardEditor({ card, onChange }) { const [cardData, setCardData] = useState({ - type: 'joker', - text: '' + type: 'JOKER', + text: '', + consequence: { type: 0, value: 1 }, + wrongConsequence: { type: 1, value: 1 } }) useEffect(() => { if (card) { setCardData({ - type: 'joker', - text: card.text || '' + type: 'JOKER', + text: card.text || '', + consequence: card.consequence || { type: 0, value: 1 }, + wrongConsequence: card.wrongConsequence || { type: 1, value: 1 } }) } }, [card]) @@ -31,6 +43,36 @@ export default function JokerCardEditor({ card, onChange }) { } } + const updateConsequence = (field, value) => { + const newCardData = { + ...cardData, + consequence: { + ...cardData.consequence, + [field]: value + } + } + setCardData(newCardData) + + if (onChange) { + onChange(newCardData) + } + } + + const updateWrongConsequence = (field, value) => { + const newCardData = { + ...cardData, + wrongConsequence: { + ...cardData.wrongConsequence, + [field]: value + } + } + setCardData(newCardData) + + if (onChange) { + onChange(newCardData) + } + } + // Példa joker kártyák const exampleCards = [ "Felelsz vagy mersz? (Az előző játékos kérdez)", @@ -57,18 +99,10 @@ export default function JokerCardEditor({ card, onChange }) { } return ( -
+
+ {/* Info box */}
- {/* Header */} -
- -

- Joker Kártya Szerkesztő -

-
- - {/* Info box */} -
+
@@ -86,28 +120,33 @@ export default function JokerCardEditor({ card, onChange }) {
+
- {/* Card text input */} -
-
- -