62 Commits

Author SHA1 Message Date
zsola03 ab35f73158 userdetails,resetpass müködöképes lett 2025-10-26 19:46:13 +01:00
Donat 18110ba410 Merge pull request 'Email verification Backend' (#72) from Backend_Fix into main
Reviewed-on: #72
2025-10-24 23:34:35 +00:00
magdo f746cfd23f Email verification Backend 2025-10-25 01:33:21 +02:00
Donat 44645bb3fc Merge pull request 'Email verification Backend' (#71) from Backend_Fix into main
Reviewed-on: #71
2025-10-24 19:17:09 +00:00
magdo 7a9a676fc0 Email verification Backend 2025-10-24 21:16:23 +02:00
Donat 1ca0f54032 Merge pull request '[#122] Email verifikáció https://project.mdnd-it.cc/work_packages/122 #70' (#70) from task/122-email-verifik-ci into main
Reviewed-on: #70
2025-10-24 19:08:16 +00:00
Buus d90f92c91c [#122] Email verifikációhttps://project.mdnd-it.cc/work_packages/122 2025-10-24 21:06:18 +02:00
Donat 1ad4af5864 Merge pull request 'Deck szerkesztese' (#69) from 1024zsola into main
Reviewed-on: #69
2025-10-24 18:40:17 +00:00
magdo 6867cb2b72 Merge branch 'main' of https://git.mdnd-it.cc/Donat/SerpentRace into 1024zsola 2025-10-24 20:39:51 +02:00
zsola03 cea9062f91 Deck szerkesztese 2025-10-24 20:34:43 +02:00
Donat e3f752ce8a Merge pull request 'Auth Check For Decks' (#68) from Backend_Fix into main
Reviewed-on: #68
2025-10-24 18:29:05 +00:00
magdo b9fedb3601 Auth Check For Decks 2025-10-24 20:28:45 +02:00
Donat 0ae66b3307 Merge pull request 'navbar meg fooldal navigációk illetve companies -> contacts' (#67) from barni into main
Reviewed-on: #67
2025-10-24 18:01:37 +00:00
magdo 630283e922 Merge branch 'main' of https://git.mdnd-it.cc/Donat/SerpentRace into barni 2025-10-24 20:01:32 +02:00
Barni 0ed75beb3f navbar meg fooldal navigációk illetve companies -> contacts 2025-10-24 19:55:37 +02:00
Donat 8ff8e80e31 Merge pull request 'To The Top' (#66) from Backend_Fix into main
Reviewed-on: #66
2025-10-24 17:55:32 +00:00
magdo 5722846da3 To The Top 2025-10-24 19:52:11 +02:00
Donat a64829f8cb Merge pull request 'To The Top' (#65) from Backend_Fix into main
Reviewed-on: #65
2025-10-24 17:37:42 +00:00
magdo a5f38f791d To The Top 2025-10-24 19:37:13 +02:00
Donat 8960bd9dce Merge pull request 'fix' (#64) from backend_update into main
Reviewed-on: #64
2025-10-23 19:19:54 +00:00
magdon df75095651 fix 2025-10-23 21:16:04 +02:00
Donat 94cdf54b83 Merge pull request '10.23 zsola hibák + Deckek listázása megoldva' (#63) from 1023zsolahibak into main
Reviewed-on: #63
2025-10-23 18:20:06 +00:00
zsola03 b73d1528c4 10.23 zsola hibák + Deckek listázása megoldva 2025-10-23 20:18:52 +02:00
Donat 387ebbc64d Merge pull request 'deckcreate-oldal-javitas' (#62) from deckcreate-oldal-javitas into main
Reviewed-on: #62
2025-10-23 15:29:44 +00:00
GitG0r0 3bbd3f1e8a Feature: Consequence rendszer implementálása minden kártya típushoz
- TaskCardEditor: Consequence és wrongConsequence kezelés hozzáadva
- JokerCardEditor: Teljesítés és nem teljesítés consequence-ek
- LuckCardEditor: Szerencse kártyák consequence kezelése
- CardEditor: Alapértelmezett consequence értékek az új kártyákhoz
- DeckCreator: Consequence mezők biztosítása mentéskor
- CardsList: Következmény típusok megjelenítése
- UI javítás: Mind a három editor külön szekciókba rendezve (info, szöveg, következmények)
- Egységes struktúra és design az összes kártya szerkesztőnél
2025-10-23 00:31:33 +02:00
GitG0r0 f2a54154f5 UI: Beállítások szekció letiltása a feladat kártyáknál
- Pontszám, Időlimit és Karakterlimit mezők letiltva
- Magyarázat mező is letiltva
- Szöveges válasz típusnál a beállítások (kis/nagy betű, pontos egyezés, stb.) letiltva
- Egységes 'Hamarosan elérhető' effekt az összes letiltott szekción
- Tipp mező továbbra is opcionális és használható
- Ezek a beállítások nem kötelezőek a kártya mentéséhez
2025-10-22 23:34:34 +02:00
GitG0r0 edca8f84cd Fix: Kártya típus kezelés javítása és joker kártyák megjelenítése
- Hozzáadva react-toastify a notifyWarning használatához
- Javítva a CardEditor fejléc hogy helyesen jelenítse meg az új kártya típusát
- Javítva a CardsList 'szerkesztés folyamatban' rész hogy QUESTION/JOKER/LUCK értékeket használjon
- Implementálva az automatikus nem megfelelő típusú kártyák törlése új kártya mentésekor
- Hozzáadva hibakezelés a kártya mentési logikához
- Joker típus címke változtatva 'Szórakozás'-ról 'Joker'-re
- Joker kártya szín változtatva citromsárgára (#FFD700)
- Docker watch mode volume konfiguráció javítása a hot reload-hoz
2025-10-22 23:21:19 +02:00
Donat 4501257a15 Merge pull request 'creator, creation date on deck' (#61) from Backend_Fix into main
Reviewed-on: #61
2025-10-22 20:04:30 +00:00
magdo 38a2aeb58a creator, creation date on deck 2025-10-22 22:03:50 +02:00
GitG0r0 0ca0e95540 Merge: Konfliktusok feloldása és toastify integráció
- Megtartva az új kártya típusok (QUESTION, LUCK, JOKER)
- Hozzáadva toastify notifikációk
- Egyszerűsített új kártya létrehozás
2025-10-22 21:31:16 +02:00
GitG0r0 ec001fb39f Refactor: DeckCreator komponensek típus kezelésének egységesítése
- Frissítve a DeckHeader típusai a backend formátumra (QUESTION, LUCK, JOKER)
- Frissítve a CardsList és Editor komponensek típus kezelése
- Egyszerűsítve a kártya létrehozás és mentés logika
- Az új kártya gomb mindig a pakli típusának megfelelő kártyát hozza létre
2025-10-22 21:23:16 +02:00
GitG0r0 00b13de70c fix(deck-creator): alapértelmezett név magyarítása
- 'Új Deck' helyett 'Új Pakli' az alapértelmezett név
- Mind a kezdeti állapotban, mind az új pakli létrehozásakor
2025-10-22 20:10:47 +02:00
GitG0r0 83efb91f52 style(deck-creator): pakli név mező szélességének optimalizálása
- Pakli név mező mostantól csak 2/3 szélességű
- Jobb vizuális egyensúly a form elemek között
- Reszponzív elrendezés megtartva
2025-10-22 20:01:54 +02:00
Donat 9673d564a0 Merge pull request 'hibak.txt feladatai' (#59) from zsolahibatxt into main
Reviewed-on: #59
2025-10-22 17:57:16 +00:00
magdo 5ba043cff8 Merge branch 'main' into zsolahibatxt 2025-10-22 19:56:32 +02:00
GitG0r0 46ad6caefd refactor(deck-creator): statisztika panel eltávolítása és layout optimalizálása
- Statisztika panel eltávolítva
- Grid elrendezés egyszerűsítve
- Felesleges kód eltávolítva
- Jobb helykihasználás az űrlap elemeknek
2025-10-22 19:52:45 +02:00
GitG0r0 f56ebbf2c3 fix(deck-creator): angol szövegek magyarítása
- 'Deck' szó cseréje 'Pakli'-ra
- Placeholder szövegek magyarítása
2025-10-22 19:43:31 +02:00
GitG0r0 c207fa5961 feat(deck-creator): dropdown menük fejlesztése
- Típus és láthatóság dropdown menük átalakítása
- Ikonok hozzáadása mindkét dropdown menühöz
- Szöveg színek javítása a jobb láthatóságért
- Hover és kijelölési állapotok hozzáadása
- Dropdown menük egységes stílusának kialakítása
2025-10-22 19:35:08 +02:00
Walke 0a811741c7 Merge pull request 'Navbar,landing, meg a többi frontend javítás' (#60) from ujbarni into main
Reviewed-on: #60
HIHETETLEEEENÜL SZIPI SZUPER
2025-10-22 13:22:00 +00:00
Barni d16d481d86 Navbar,landing, meg a többi frontend javítás 2025-10-22 15:15:20 +02:00
zsola03 3ad9ba3e3f kartyatorles popup 2025-10-22 14:24:24 +02:00
zsola03 825e9d1a08 hibak.txt feladatai 2025-10-22 09:30:09 +02:00
Walke ad5f13a8e1 Merge pull request 'guessName+Fixes' (#58) from frontendFix+Guess into main
Reviewed-on: #58

VAOOO UGUYES VAGYYY
2025-10-21 13:09:27 +00:00
Walke 237378c208 guessName+Fixes 2025-10-21 15:08:28 +02:00
Walke a1cf327837 Merge pull request 'Ha be van jelenkezve a user akkor a /# en nem irja ki neki a belepest meg a regisztraciot VAMOOOS' (#57) from landingFlowFix into main
Reviewed-on: #57
szerintem is :D:D:D:D
2025-10-21 12:14:13 +00:00
Walke c31bf9d4fb Ha be van jelenkezve a user akkor a /# en nem irja ki neki a belepest meg a regisztraciot VAMOOOS 2025-10-21 14:13:16 +02:00
Walke ef0b1916f2 Merge pull request 'dobokocka mukodik :O' (#56) from dice into main
Reviewed-on: #56
kocka kocka kocka kocka kocka kocka kocka
2025-10-20 17:40:13 +00:00
Walke 1c01e4ce24 Merge pull request 'nagyon meno lett a tabon logo SerpentRace minden' (#55) from tab into main
sztem jo xd
2025-10-20 17:39:25 +00:00
Walke 8b5cf2c1e5 nagyon meno lett a tabon logo SerpentRace minden 2025-10-20 19:38:21 +02:00
Walke 023219e41b dobokocka mukodik :O 2025-10-20 19:20:49 +02:00
magdo 2d7778f7d1 test removed 2025-10-20 19:17:43 +02:00
magdo aa3587b60a deck card count added 2025-10-20 19:13:50 +02:00
Walke 99fa7ebd98 Merge pull request 'registracional jol navigal a loginra' (#54) from regnavigationfix into main
Reviewed-on: #54
:D
2025-10-20 16:57:15 +00:00
Walke 23c4b838d4 registracional jol navigal a loginra 2025-10-20 18:56:38 +02:00
Walke bfe977d35b Merge remote-tracking branch 'origin/deck_kezeles' 2025-10-20 18:01:41 +02:00
Walke 8d24e8ffa6 Merge pull request 'Lobby' (#50) from barni1020 into main
Reviewed-on: #50
2025-10-20 15:22:13 +00:00
Barni 1bf3253128 Lobby 2025-10-20 17:14:37 +02:00
Walke 96487fb065 Merge pull request 'Filter bar fix' (#49) from deckmanagerfrontendfix into main
Reviewed-on: #49
2025-10-18 15:52:30 +00:00
Walke 9ef83f7963 Filter bar fix 2025-10-18 17:50:39 +02:00
Walke 27fc028bad navbarban jol le vannak kezelve a redirect es letre lett hozva egy hook amivel automatikusan berakja a usernamet es ha meg nem akkor redirectel 2025-10-15 19:08:31 +02:00
Walke d1b4141e63 Merge pull request 'redirect fix' (#48) from authlocalstorage into main
Reviewed-on: #48
2025-10-15 16:40:22 +00:00
Walke 76fa204ae8 redirect fix 2025-10-15 18:39:43 +02:00
73 changed files with 4419 additions and 1251 deletions
+62
View File
@@ -0,0 +1,62 @@
Javitás
Deckeck:
- Következmény csak szerencse kártyánál
- Egy fajta következmény (/lap, automatikusan kerül végrehajtásra)
- Hibás kártya pakli mentésekor is törlödjön
- extra kör, kimarad bármennyi 1-től 5-ig
- megnyitás, szerkesztés, adatok betöltése
- Mentési ADATOK Csekkolása | ZSOLA
- Closer option
navbar:
- tegnapiak
TEGNAPI HIBÁK JAVÍTÁSA:
- kapcs fel routing
- navbar széthúz
- footer kapcsolat
- navabar gomboksorrend
- vagy kontat vagy kapcsolat
- navbar bejelent
- navbar layout finomít
- palki info get
GET /ap/decks/page/:from/:to (0-49) 50db (50-99) 50db ... (0-29) 30db => (30-59) 30db
- from: (oldalsz-1)*dbsz (pl: (1-1)*30=0; (2-1)*30=30)
- to: (oldalsz*dbsz) - 1 (pl: (1*30)-1=29; (2*30)-1 =59)
email verifikáció:
- verify-email/:code => Email címe hitelesítés alatt: stb
- ha sikeres => login => toastify => email címe hitelesítve
- ha sikertelen => home/register => toastify/pushup => sikertelen vegye fel velünk a kapcsolatot
- POST api/users/verify-email/:code <= BACKEND URI
HOLNAP ESTE 19:00 => Jó lenne, ha ezek megvannak
HOLNAPTÓL => JÁTÉK => SOCKET IO működése
Mobil nézet:
- landing page
- navbar
- footer
- pakli fő nézet => bar
- pakli összerakás és szerkesztés
- bejelentkezés
- regisztráció
User felület:
- Saját adatok lekérése
- Saját adatok módosítása:
- email-cím
- telefonszám
- jelszó
- felhasználó név
- Saját profil törlése
- Elfelelejtett jelszó
- Kérése => email-cím alapján => POST /api/users/forgot-password
- password-reset/:token => POST /api/users/reset-password
Binary file not shown.

After

Width:  |  Height:  |  Size: 981 KiB

+582 -41
View File
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -50,25 +50,25 @@
"nodemailer": "^7.0.5", "nodemailer": "^7.0.5",
"pg": "^8.16.3", "pg": "^8.16.3",
"redis": "^5.8.1", "redis": "^5.8.1",
"sharp": "^0.34.4",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typeorm": "^0.3.26", "typeorm": "^0.3.26",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"winston": "^3.17.0", "winston": "^3.17.0"
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/multer": "^2.0.0",
"@types/nodemailer": "^7.0.1",
"@types/uuid": "^10.0.0",
"@jest/globals": "^30.0.5", "@jest/globals": "^30.0.5",
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
"@types/cookie-parser": "^1.4.9", "@types/cookie-parser": "^1.4.9",
"@types/express": "^5.0.3", "@types/express": "^5.0.3",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.0.0",
"@types/node": "^24.3.3", "@types/node": "^24.3.3",
"@types/nodemailer": "^7.0.1",
"@types/pg": "^8.15.5", "@types/pg": "^8.15.5",
"@types/redis": "^4.0.10", "@types/redis": "^4.0.10",
"@types/socket.io": "^3.0.1", "@types/socket.io": "^3.0.1",
@@ -76,6 +76,7 @@
"@types/supertest": "^6.0.3", "@types/supertest": "^6.0.3",
"@types/swagger-jsdoc": "^6.0.4", "@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8", "@types/swagger-ui-express": "^4.1.8",
"@types/uuid": "^10.0.0",
"jest": "^30.0.5", "jest": "^30.0.5",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"rimraf": "^5.0.10", "rimraf": "^5.0.10",
@@ -385,10 +385,11 @@ router.patch('/decks/:id', adminRequired, async (req: Request, res: Response) =>
// Hard delete deck (admin only) // Hard delete deck (admin only)
router.delete('/decks/:id/hard', adminRequired, async (req: Request, res: Response) => { router.delete('/decks/:id/hard', adminRequired, async (req: Request, res: Response) => {
try { try {
const adminUserId = (req as any).user.userId;
const deckId = req.params.id; const deckId = req.params.id;
logRequest('Admin hard delete deck endpoint accessed', req, res, { deckId }); logRequest('Admin hard delete deck endpoint accessed', req, res, { deckId });
const result = await container.deleteDeckCommandHandler.execute({ id: deckId, soft: false }); const result = await container.deleteDeckCommandHandler.execute({ userid: adminUserId, authLevel: 1, id: deckId, soft: false });
logRequest('Admin deck hard delete successful', req, res, { deckId, success: result }); logRequest('Admin deck hard delete successful', req, res, { deckId, success: result });
res.json({ success: result }); res.json({ success: result });
@@ -199,12 +199,13 @@ deckRouter.patch('/:id', authRequired, async (req, res) => {
try { try {
const deckId = req.params.id; const deckId = req.params.id;
const userId = (req as any).user.userId; const userId = (req as any).user.userId;
const authLevel = (req as any).user.authLevel;
logRequest('Update deck endpoint accessed', req, res, { deckId, userId, updateFields: Object.keys(req.body) }); logRequest('Update deck endpoint accessed', req, res, { deckId, userId, updateFields: Object.keys(req.body) });
// Convert string enum values to integers // Convert string enum values to integers
const updateData = convertEnumValues(req.body); const updateData = convertEnumValues(req.body);
const result = await container.updateDeckCommandHandler.execute({ id: deckId, ...updateData }); const result = await container.updateDeckCommandHandler.execute({ userid: userId, authLevel: authLevel, id: deckId, ...updateData });
logRequest('Deck updated successfully', req, res, { deckId, userId }); logRequest('Deck updated successfully', req, res, { deckId, userId });
res.json(result); res.json(result);
@@ -244,9 +245,10 @@ deckRouter.delete('/:id', authRequired, async (req, res) => {
try { try {
const deckId = req.params.id; const deckId = req.params.id;
const userId = (req as any).user.userId; const userId = (req as any).user.userId;
const authLevel = (req as any).user.authLevel;
logRequest('Soft delete deck endpoint accessed', req, res, { deckId, userId }); logRequest('Soft delete deck endpoint accessed', req, res, { deckId, userId });
const result = await container.deleteDeckCommandHandler.execute({ id: deckId, soft: true }); const result = await container.deleteDeckCommandHandler.execute({ userid: userId, authLevel: authLevel, id: deckId, soft: true });
logRequest('Deck soft delete successful', req, res, { deckId, userId, success: result }); logRequest('Deck soft delete successful', req, res, { deckId, userId, success: result });
res.json({ success: result }); res.json({ success: result });
@@ -1,66 +0,0 @@
import e, { Router } from 'express';
import { container, DIContainer } from '../../Application/Services/DIContainer';
import { ErrorResponseService } from '../../Application/Services/ErrorResponseService';
import { logRequest, logError, logAuth, logWarning, logOther } from '../../Application/Services/Logger';
import { GenerateBoardCommand } from '../../Application/Game/commands/GenerateBoardCommand';
const router = Router();
//function to test the search service
async function triggerAsyncBoardGeneration(gameId: string): Promise<boolean> {
try {
// Calculate default field counts based on game configuration
// For now, use reasonable defaults - this should be configurable by host in the future
const maxSpecialFieldsPercentage = parseInt(process.env.MAX_SPECIAL_FIELDS_PERCENTAGE || '67');
const maxSpecialFields = Math.floor((100 * maxSpecialFieldsPercentage) / 100);
// Default distribution: 60% positive, 25% negative, 15% luck
const positiveFieldCount = Math.floor(maxSpecialFields * 0.6);
const negativeFieldCount = Math.floor(maxSpecialFields * 0.25);
const luckFieldCount = Math.floor(maxSpecialFields * 0.15);
const command: GenerateBoardCommand = {
gameId,
positiveFieldCount,
negativeFieldCount,
luckFieldCount
};
logOther(`Triggering async board generation for game ${gameId}`, {
positiveFieldCount,
negativeFieldCount,
luckFieldCount,
totalSpecialFields: positiveFieldCount + negativeFieldCount + luckFieldCount
});
// Execute board generation in background
await DIContainer.getInstance().generateBoardCommandHandler.execute(command);
return true;
} catch (error) {
logError(`Async board generation failed for game ${gameId}`, error as Error);
// Don't propagate error - board generation failure shouldn't affect game creation
return false;
}
}
// Game board generation endpoint
router.post('/gameBoardGeneration', async (req, res) => {
try {
logRequest('Game board generation endpoint accessed', req, res);
const result = await triggerAsyncBoardGeneration("######-#####-#####-######");
if (result) {
logOther('Game board generation triggered successfully', result);
return res.json({ message: 'Game board generation triggered successfully' });
} else {
throw new Error('Game board generation failed to trigger');
}
} catch (error : any) {
logError('Error in game board generation endpoint', error);
return ErrorResponseService.sendInternalServerError(res);
}
});
export default router;
@@ -225,7 +225,7 @@ userRouter.post('/refresh-token', async (req, res) => {
}); });
// Email verification endpoint // Email verification endpoint
userRouter.get('/verify-email/:token', async (req, res) => { userRouter.post('/verify-email/:token', async (req, res) => {
try { try {
const { token } = req.params; const { token } = req.params;
@@ -15,6 +15,9 @@ export interface ShortDeckDto {
type: number; type: number;
playedNumber: number; playedNumber: number;
ctype: number; ctype: number;
cardsCount: number;
creator: string;
creationdate: Date;
} }
export interface DetailDeckDto { export interface DetailDeckDto {
@@ -1,4 +1,5 @@
import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate'; import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate';
import { UserAggregate } from '../../../Domain/User/UserAggregate';
import { CreateDeckDto, UpdateDeckDto, ShortDeckDto, DetailDeckDto } from '../DeckDto'; import { CreateDeckDto, UpdateDeckDto, ShortDeckDto, DetailDeckDto } from '../DeckDto';
export class DeckMapper { export class DeckMapper {
@@ -9,6 +10,9 @@ export class DeckMapper {
type: deck.type, type: deck.type,
playedNumber: deck.playedNumber, playedNumber: deck.playedNumber,
ctype: deck.ctype, ctype: deck.ctype,
cardsCount: deck.cards.length,
creator: deck.user?.username || 'Unknown',
creationdate: deck.creationdate
}; };
} }
@@ -26,6 +30,15 @@ export class DeckMapper {
} }
static toShortDtoList(decks: DeckAggregate[]): ShortDeckDto[] { static toShortDtoList(decks: DeckAggregate[]): ShortDeckDto[] {
return decks.map(this.toShortDto); return decks.map(deck => ({
id: deck.id,
name: deck.name,
type: deck.type,
playedNumber: deck.playedNumber,
ctype: deck.ctype,
cardsCount: deck.cards.length,
creator: deck.user?.username || 'Unknown',
creationdate: deck.creationdate
}));
} }
} }
@@ -1,4 +1,6 @@
export interface DeleteDeckCommand { export interface DeleteDeckCommand {
userid: string;
authLevel: number;
id: string; id: string;
soft?: boolean; soft?: boolean;
} }
@@ -1,10 +1,24 @@
import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository'; import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository';
import { logAuth, logError } from '../../Services/Logger';
import { DeleteDeckCommand } from './DeleteDeckCommand'; import { DeleteDeckCommand } from './DeleteDeckCommand';
export class DeleteDeckCommandHandler { export class DeleteDeckCommandHandler {
constructor(private readonly deckRepo: IDeckRepository) {} constructor(private readonly deckRepo: IDeckRepository) {}
async execute(cmd: DeleteDeckCommand): Promise<boolean> { async execute(cmd: DeleteDeckCommand): Promise<boolean> {
//get decks userid
const deck = await this.deckRepo.findById(cmd.id);
if (!deck) {
logError(`Deck not found with ID: ${cmd.id}`);
throw new Error('Deck not found');
}
if(cmd.authLevel !==1 && deck.userid !== cmd.userid) {
logAuth(`Unauthorized access attempt to deck with ID: ${cmd.id}, UserID: ${cmd.userid}`);
throw new Error('Unauthorized');
}
if (cmd.soft) { if (cmd.soft) {
await this.deckRepo.softDelete(cmd.id); await this.deckRepo.softDelete(cmd.id);
} else { } else {
@@ -1,11 +1,10 @@
import { n } from "framer-motion/dist/types.d-D0HXPxHm";
export interface UpdateDeckCommand { export interface UpdateDeckCommand {
userid: string;
authLevel: number;
id: string; id: string;
userstate?: number; userstate?: number;
name?: string; name?: string;
type?: number; type?: number;
userid?: string;
cards?: any[]; cards?: any[];
ctype?: number; ctype?: number;
state?: number; state?: number;
@@ -3,7 +3,7 @@ import { UpdateDeckCommand } from './UpdateDeckCommand';
import { ShortDeckDto } from '../../DTOs/DeckDto'; import { ShortDeckDto } from '../../DTOs/DeckDto';
import { DeckMapper } from '../../DTOs/Mappers/DeckMapper'; import { DeckMapper } from '../../DTOs/Mappers/DeckMapper';
import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate'; import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate';
import { logError } from '../../Services/Logger'; import { logAuth, logError } from '../../Services/Logger';
export class UpdateDeckCommandHandler { export class UpdateDeckCommandHandler {
constructor(private readonly deckRepo: IDeckRepository) {} constructor(private readonly deckRepo: IDeckRepository) {}
@@ -24,6 +24,11 @@ export class UpdateDeckCommandHandler {
throw new Error('Deck not found'); throw new Error('Deck not found');
} }
if(cmd.authLevel !==1 && existingDeck.userid !== cmd.userid) {
logAuth(`Unauthorized access attempt to deck with ID: ${cmd.id}, UserID: ${cmd.userid}`);
throw new Error('Unauthorized');
}
const for_update: Partial<DeckAggregate> = {}; const for_update: Partial<DeckAggregate> = {};
if(cmd.name !== undefined) for_update.name = cmd.name; if(cmd.name !== undefined) for_update.name = cmd.name;
if(cmd.type !== undefined) for_update.type = cmd.type; if(cmd.type !== undefined) for_update.type = cmd.type;
@@ -1,6 +1,7 @@
import * as nodemailer from 'nodemailer'; import * as nodemailer from 'nodemailer';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import sharp from 'sharp';
import { logError, logAuth, logStartup } from './Logger'; import { logError, logAuth, logStartup } from './Logger';
import { EmailTemplateHelper, LocalizedSubjects } from './EmailTemplateHelper'; import { EmailTemplateHelper, LocalizedSubjects } from './EmailTemplateHelper';
@@ -28,9 +29,14 @@ export class EmailService {
private transporter!: nodemailer.Transporter; private transporter!: nodemailer.Transporter;
private config: EmailConfig; private config: EmailConfig;
private templatesPath: string; private templatesPath: string;
private logoPath: string;
private resizedLogoBuffer?: Buffer;
constructor() { constructor() {
this.templatesPath = path.join(__dirname, '../../Templates'); this.templatesPath = path.join(__dirname, '../../Templates');
this.logoPath = path.join(__dirname, '../../../assets/Logo.png');
// Load logo asynchronously after initialization
this.loadLogoBase64().catch(err => console.error('[EmailService] Error loading logo:', err));
this.config = { this.config = {
host: process.env.EMAIL_HOST || 'smtp.gmail.com', host: process.env.EMAIL_HOST || 'smtp.gmail.com',
@@ -63,6 +69,32 @@ export class EmailService {
} }
} }
/**
* Load and resize logo for email attachments - 32x32 pixels
*/
private async loadLogoBase64(): Promise<void> {
try {
if (fs.existsSync(this.logoPath)) {
const logoBuffer = fs.readFileSync(this.logoPath);
// Resize to 60x60 pixels with high quality and centered
this.resizedLogoBuffer = await sharp(logoBuffer)
.resize(60, 60, {
fit: 'contain',
background: { r: 255, g: 255, b: 255, alpha: 1 },
position: 'center'
})
.png()
.toBuffer();
console.log(`[EmailService] ✅ Logo loaded and resized to 60x60`);
}
} catch (error) {
console.error(`[EmailService] ❌ Failed to load/resize logo:`, error);
logError('Failed to load logo for emails', error instanceof Error ? error : new Error(String(error)));
}
}
/** /**
* Send email with template * Send email with template
* @param options - Email options including template and data * @param options - Email options including template and data
@@ -73,19 +105,29 @@ export class EmailService {
let textContent = options.text; let textContent = options.text;
if (options.template) { if (options.template) {
const templateResult = await this.loadTemplate(options.template, options.templateData || {}); const templateResult = await this.loadTemplate(options.template, options.templateData);
htmlContent = templateResult.html; htmlContent = templateResult.html;
textContent = templateResult.text; textContent = templateResult.text;
} }
const mailOptions = { const mailOptions: any = {
from: this.config.from, from: this.config.from,
to: options.to, to: options.to,
subject: options.subject, subject: options.subject,
html: htmlContent, html: htmlContent,
text: textContent text: textContent,
attachments: []
}; };
// Add logo as CID attachment if available
if (this.resizedLogoBuffer) {
mailOptions.attachments.push({
filename: 'logo.png',
content: this.resizedLogoBuffer,
cid: 'logo@serpentrace' // Content-ID for referencing in HTML
});
}
const result = await this.transporter.sendMail(mailOptions); const result = await this.transporter.sendMail(mailOptions);
logAuth('Email sent successfully', undefined, { logAuth('Email sent successfully', undefined, {
messageId: result.messageId, messageId: result.messageId,
@@ -120,7 +120,7 @@ export class TokenService {
try { try {
// Remove trailing slash from baseUrl if present // Remove trailing slash from baseUrl if present
const cleanBaseUrl = baseUrl.replace(/\/$/, ''); const cleanBaseUrl = baseUrl.replace(/\/$/, '');
return `${cleanBaseUrl}/api/auth/verify-email?token=${encodeURIComponent(token)}`; return `${cleanBaseUrl}/verify-email?token=${encodeURIComponent(token)}`;
} catch (error) { } catch (error) {
logError('TokenService.generateVerificationUrl error', error instanceof Error ? error : new Error(String(error))); logError('TokenService.generateVerificationUrl error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to generate verification URL'); throw new Error('Failed to generate verification URL');
@@ -69,7 +69,7 @@ export class CreateUserCommandHandler {
private async sendVerificationEmailAsync(user: UserAggregate, token: string): Promise<void> { private async sendVerificationEmailAsync(user: UserAggregate, token: string): Promise<void> {
try { try {
const baseUrl = process.env.APP_BASE_URL || 'http://localhost:3000'; const baseUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
const verificationUrl = TokenService.generateVerificationUrl(baseUrl, token); const verificationUrl = TokenService.generateVerificationUrl(baseUrl, token);
const emailSent = await this.emailService.sendVerificationEmail( const emailSent = await this.emailService.sendVerificationEmail(
@@ -1,5 +1,6 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { OrganizationAggregate } from '../Organization/OrganizationAggregate'; import { OrganizationAggregate } from '../Organization/OrganizationAggregate';
import { UserAggregate } from '../User/UserAggregate';
export enum Type { export enum Type {
LUCK = 0, LUCK = 0,
@@ -81,4 +82,8 @@ export class DeckAggregate {
@ManyToOne(() => OrganizationAggregate, { nullable: true }) @ManyToOne(() => OrganizationAggregate, { nullable: true })
@JoinColumn({ name: 'organization_id' }) @JoinColumn({ name: 'organization_id' })
organization!: OrganizationAggregate | null; organization!: OrganizationAggregate | null;
@ManyToOne(() => UserAggregate, { eager: false })
@JoinColumn({ name: 'user_id' })
user!: UserAggregate | null;
} }
@@ -255,7 +255,7 @@ export class DeckRepository implements IDeckRepository {
const [decks, totalCount] = await this.repo.findAndCount({ const [decks, totalCount] = await this.repo.findAndCount({
where: { state: Not(State.SOFT_DELETE) }, where: { state: Not(State.SOFT_DELETE) },
relations: ['organization'], relations: ['organization', 'user'],
order: { creationdate: 'DESC' }, order: { creationdate: 'DESC' },
skip, skip,
take take
@@ -270,6 +270,7 @@ export class DeckRepository implements IDeckRepository {
// Regular user complex filtering // Regular user complex filtering
const queryBuilder = this.repo.createQueryBuilder('deck') const queryBuilder = this.repo.createQueryBuilder('deck')
.leftJoinAndSelect('deck.organization', 'org') .leftJoinAndSelect('deck.organization', 'org')
.leftJoinAndSelect('deck.user', 'user')
.where('deck.state != :deletedState', { deletedState: State.SOFT_DELETE }); .where('deck.state != :deletedState', { deletedState: State.SOFT_DELETE });
queryBuilder.andWhere('(' + queryBuilder.andWhere('(' +
@@ -22,18 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
padding-bottom: 20px;
}
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
} }
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #2c5aa0; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
font-size: 18px; font-size: 18px;
color: #666; color: #666;
margin-bottom: 20px; margin-bottom: 20px;
text-align: center;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.greeting { .greeting {
font-size: 16px; font-size: 16px;
@@ -98,9 +111,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo">🐍 {{companyName}}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Antwort auf Ihre {{contactTypeString}}</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">🐍 SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Antwort auf Ihre {{contactTypeString}}</div>
<div class="greeting"> <div class="greeting">
Hallo {{contactName}}, Hallo {{contactName}},
@@ -22,18 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
padding-bottom: 20px;
}
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
} }
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #2c5aa0; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
font-size: 18px; font-size: 18px;
color: #666; color: #666;
margin-bottom: 20px; margin-bottom: 20px;
text-align: center;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.greeting { .greeting {
font-size: 16px; font-size: 16px;
@@ -98,9 +111,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo">🐍 {{companyName}}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Válasz az Ön {{contactTypeString}} üzenetére</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">🐍 SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Válasz az ön {{contactTypeString}}</div>
<div class="greeting"> <div class="greeting">
Kedves {{contactName}}! Kedves {{contactName}}!
@@ -22,18 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
padding-bottom: 20px;
}
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
} }
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #2c5aa0; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
font-size: 18px; font-size: 18px;
color: #666; color: #666;
margin-bottom: 20px; margin-bottom: 20px;
text-align: center;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.greeting { .greeting {
font-size: 16px; font-size: 16px;
@@ -98,9 +111,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo">🐍 {{companyName}}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Response to Your {{contactTypeString}}</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">🐍 SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Response to Your {{contactTypeString}}</div>
<div class="greeting"> <div class="greeting">
Hello {{contactName}}, Hello {{contactName}},
@@ -22,19 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
border-bottom: 2px solid #FF9800;
padding-bottom: 20px; padding-bottom: 20px;
} }
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
}
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #E65100; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
color: #666; color: #666;
font-size: 16px; font-size: 16px;
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.content { .content {
margin-bottom: 30px; margin-bottom: 30px;
@@ -123,9 +135,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo">🐍 {{ companyName }}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Passwort zurücksetzen</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Passwort zurücksetzen</div>
<div class="content"> <div class="content">
<div class="greeting"> <div class="greeting">
@@ -22,19 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
border-bottom: 2px solid #FF9800;
padding-bottom: 20px; padding-bottom: 20px;
} }
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
}
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #E65100; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
color: #666; color: #666;
font-size: 16px; font-size: 16px;
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.content { .content {
margin-bottom: 30px; margin-bottom: 30px;
@@ -123,9 +135,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo">🐍 {{ companyName }}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Jelszó visszaállítás kérése</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Jelszó visszaállítás kérése</div>
<div class="content"> <div class="content">
<div class="greeting"> <div class="greeting">
@@ -22,19 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
border-bottom: 2px solid #FF9800;
padding-bottom: 20px; padding-bottom: 20px;
} }
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
}
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #E65100; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
color: #666; color: #666;
font-size: 16px; font-size: 16px;
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.content { .content {
margin-bottom: 30px; margin-bottom: 30px;
@@ -123,9 +135,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo">🐍 {{ companyName }}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Password Reset Request</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Password Reset Request</div>
<div class="content"> <div class="content">
<div class="greeting"> <div class="greeting">
@@ -22,19 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
border-bottom: 2px solid #4CAF50;
padding-bottom: 20px; padding-bottom: 20px;
} }
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
}
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #2E7D32; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
color: #666; color: #666;
font-size: 16px; font-size: 16px;
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.content { .content {
margin-bottom: 30px; margin-bottom: 30px;
@@ -115,9 +127,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo">🐍 {{ companyName }}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Konto verifizieren</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Konto verifizieren</div>
<div class="content"> <div class="content">
<div class="greeting"> <div class="greeting">
@@ -22,19 +22,31 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
border-bottom: 2px solid #4CAF50;
padding-bottom: 20px; padding-bottom: 20px;
} }
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
}
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #2E7D32; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
color: #666; color: #666;
font-size: 16px; font-size: 16px;
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.content { .content {
margin-bottom: 30px; margin-bottom: 30px;
@@ -115,9 +127,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo">🐍 {{ companyName }}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Fiók megerősítése</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Fiók megerősítése</div>
<div class="content"> <div class="content">
<div class="greeting"> <div class="greeting">
@@ -22,19 +22,32 @@
} }
.header { .header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 10px;
border-bottom: 2px solid #4CAF50;
padding-bottom: 20px; padding-bottom: 20px;
} }
.header table {
margin: 0 auto;
}
.header img {
width: 60px;
height: 60px;
display: block;
vertical-align: middle;
}
.logo { .logo {
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
color: #2E7D32; color: #2E7D32;
margin-bottom: 10px; vertical-align: middle;
padding-left: 10px;
} }
.subtitle { .subtitle {
color: #666; color: #666;
font-size: 16px; font-size: 16px;
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #4CAF50;
} }
.content { .content {
margin-bottom: 30px; margin-bottom: 30px;
@@ -115,9 +128,14 @@
<body> <body>
<div class="email-container"> <div class="email-container">
<div class="header"> <div class="header">
<div class="logo">🐍 {{ companyName }}</div> <table cellpadding="0" cellspacing="0" border="0">
<div class="subtitle">Account Verification</div> <tr>
<td><img src="cid:logo@serpentrace" alt="Logo"/></td>
<td class="logo">SerpentRace</td>
</tr>
</table>
</div> </div>
<div class="subtitle">Account Verification</div>
<div class="content"> <div class="content">
<div class="greeting"> <div class="greeting">
@@ -0,0 +1,206 @@
services:
# Backend service with hot reload
backend:
build:
context: ../SerpentRace_Backend
dockerfile: ../SerpentRace_Docker/Dockerfile_backend.dev
container_name: serpentrace-backend-dev
restart: unless-stopped
env_file:
- .env.dev
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- PORT=3000
- FRONTEND_URL=http://localhost:5173
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=serpentrace
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
- REDIS_URL=redis://redis:6379
- REDIS_HOST=redis
- REDIS_PORT=6379
- MINIO_ENDPOINT=minio
- MINIO_PORT=9000
- MINIO_ACCESS_KEY=serpentrace
- MINIO_SECRET_KEY=serpentrace123!
- MINIO_USE_SSL=false
volumes: [ ../SerpentRace_Backend/logs:/app/logs ]
develop:
watch:
- action: sync
path: ../SerpentRace_Backend/src
target: /app/src
ignore:
- node_modules/
- dist/
- "*.log"
- action: sync
path: ../SerpentRace_Backend/package.json
target: /app/package.json
- action: rebuild
path: ../SerpentRace_Backend/package-lock.json
- action: rebuild
path: ../SerpentRace_Backend/tsconfig.json
- action: rebuild
path: ./Dockerfile_backend.dev
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_healthy
network_mode: host
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Frontend service with hot reload
frontend:
build:
context: ../SerpentRace_Frontend
dockerfile: ../SerpentRace_Docker/Dockerfile_frontend.dev
container_name: serpentrace-frontend-dev
restart: unless-stopped
ports:
- "5173:5173"
environment:
- NODE_ENV=development
- VITE_API_URL=http://localhost:3000
volumes: []
develop:
watch:
- action: sync
path: ../SerpentRace_Frontend/src
target: /app/src
ignore:
- node_modules/
- dist/
- "*.log"
- action: sync
path: ../SerpentRace_Frontend/public
target: /app/public
- action: sync
path: ../SerpentRace_Frontend/package.json
target: /app/package.json
- action: rebuild
path: ../SerpentRace_Frontend/package-lock.json
- action: rebuild
path: ../SerpentRace_Frontend/vite.config.js
- action: rebuild
path: ./Dockerfile_frontend.dev
depends_on:
- backend
network_mode: host
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: serpentrace-postgres-dev
restart: unless-stopped
ports:
- "5432:5432"
environment:
POSTGRES_DB: serpentrace
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
volumes:
- postgres_dev_data:/var/lib/postgresql/data
- ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql:ro
network_mode: host
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# Redis Cache
redis:
image: redis:7-alpine
container_name: serpentrace-redis-dev
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_dev_data:/data
command: redis-server --appendonly yes
network_mode: host
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# MinIO Object Storage
minio:
image: minio/minio:latest
container_name: serpentrace-minio-dev
restart: unless-stopped
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: serpentrace
MINIO_ROOT_PASSWORD: serpentrace123!
volumes:
- minio_dev_data:/data
command: server /data --console-address ":9001"
network_mode: host
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 5
# Redis Commander for development debugging
redis-commander:
image: rediscommander/redis-commander:latest
container_name: serpentrace-redis-commander-dev
restart: unless-stopped
ports:
- "8081:8081"
environment:
- REDIS_HOSTS=local:redis:6379
depends_on:
redis:
condition: service_healthy
network_mode: host
# Database administration tool
pgadmin:
image: dpage/pgadmin4:latest
container_name: serpentrace-pgadmin-dev
restart: unless-stopped
ports:
- "8080:80"
environment:
PGADMIN_DEFAULT_EMAIL: admin@serpentrace.dev
PGADMIN_DEFAULT_PASSWORD: admin
PGADMIN_CONFIG_SERVER_MODE: 'False'
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
PGADMIN_CONFIG_WTF_CSRF_ENABLED: 'False'
volumes:
- pgadmin_dev_data:/var/lib/pgadmin
- ./pgadmin_servers.json:/pgadmin4/servers.json:ro
depends_on:
postgres:
condition: service_healthy
network_mode: host
volumes:
postgres_dev_data:
driver: local
redis_dev_data:
driver: local
minio_dev_data:
driver: local
pgadmin_dev_data:
driver: local
+2 -2
View File
@@ -13,7 +13,6 @@ services:
environment: environment:
- NODE_ENV=development - NODE_ENV=development
- PORT=3000 - PORT=3000
- FRONTEND_URL=http://localhost:5173
- DB_HOST=postgres - DB_HOST=postgres
- DB_PORT=5432 - DB_PORT=5432
- DB_NAME=serpentrace - DB_NAME=serpentrace
@@ -75,7 +74,8 @@ services:
environment: environment:
- NODE_ENV=development - NODE_ENV=development
- VITE_API_URL=http://localhost:3000 - VITE_API_URL=http://localhost:3000
volumes: [] volumes:
[]
develop: develop:
watch: watch:
- action: sync - action: sync
+3 -3
View File
@@ -1,10 +1,10 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/src/assets/pictures/Logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title> <title>SerpentRace</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+23
View File
@@ -15,6 +15,7 @@
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
"react-toastify": "^11.0.5",
"tailwindcss": "^4.1.7" "tailwindcss": "^4.1.7"
}, },
"devDependencies": { "devDependencies": {
@@ -1816,6 +1817,15 @@
"node": ">=18" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3363,6 +3373,19 @@
"node": ">=18" "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": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+1
View File
@@ -17,6 +17,7 @@
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
"react-toastify": "^11.0.5",
"tailwindcss": "^4.1.7" "tailwindcss": "^4.1.7"
}, },
"devDependencies": { "devDependencies": {
+18 -4
View File
@@ -6,15 +6,22 @@ import EmailVerification from "./pages/Auth/EmailVerification"
import Test from "./pages/Testing/Test" import Test from "./pages/Testing/Test"
import ForgotPassword from "./pages/Auth/ForgotPassword" import ForgotPassword from "./pages/Auth/ForgotPassword"
import ResetPassword from "./pages/Auth/ResetPassword" import ResetPassword from "./pages/Auth/ResetPassword"
import ResetPasswordRedirect from "./pages/Auth/ResetPasswordRedirect"
import Landingpage from "./pages/Landing/Landingpage" import Landingpage from "./pages/Landing/Landingpage"
import Home from "./pages/Landing/Home" import Home from "./pages/Landing/Home"
import DeckManagerPage from "./pages/Decks/DeckManagerPage" import DeckManagerPage from "./pages/Decks/DeckManagerPage"
import DeckCreator from "./pages/DeckCreator/DeckCreator" import DeckCreator from "./pages/DeckCreator/DeckCreator"
import CompanyHub from "./pages/Companies/Companies" import CompanyHub from "./pages/Contacts/Contacts"
import About from "./pages/About/About" import About from "./pages/About/About"
import ScrollToTop from "./components/ScrollToTop" import ScrollToTop from "./components/ScrollToTop"
import GameScreen from "./pages/Game/GameScreen" import GameScreen from "./pages/Game/GameScreen"
import Reports from "./pages/Report/Reports" import Reports from "./pages/Report/Reports"
import Lobby from "./pages/Lobby/Lobby"
import ProfileCard from "./components/Userdetails/Userdetails"
import { ToastConfig } from "./components/Toastify/toastifyServices" // fontos: named import, nem default!
import VerifyEmailPage from "./pages/Auth/VerifyEmailPage"
function App() { function App() {
const [isMobile, setIsMobile] = useState(false) const [isMobile, setIsMobile] = useState(false)
@@ -43,14 +50,19 @@ function App() {
// } // }
return ( return (
<>
<Router> <Router>
<Routes> <Routes>
<Route path="/api/auth/reset-password" element={<ResetPasswordRedirect />} />
<Route path="/verify-email" element={<VerifyEmailPage />} />
<Route path="/about" element={<About />} /> <Route path="/about" element={<About />} />
<Route path="/lobby" element={<Lobby />} />
<Route path="/register" element={<AuthRegister />} /> <Route path="/register" element={<AuthRegister />} />
<Route path="/login" element={<AuthLogin />} /> <Route path="/login" element={<AuthLogin />} />
<Route path="/verify-email" element={<EmailVerification />} /> <Route path="/verify-email" element={<EmailVerification />} />
<Route path="/forgot-password" element={<ForgotPassword />} /> <Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<ResetPassword />} /> <Route path="/reset-password" element={<ResetPassword />} />
<Route path="/profile" element={<ProfileCard />} />
<Route path="/test" element={<Test />} /> <Route path="/test" element={<Test />} />
<Route path="/" element={<Landingpage />} /> <Route path="/" element={<Landingpage />} />
<Route path="/home" element={<Home />} /> <Route path="/home" element={<Home />} />
@@ -58,12 +70,14 @@ function App() {
<Route path="/deck-creator" element={<DeckCreator />} /> <Route path="/deck-creator" element={<DeckCreator />} />
<Route path="/deck-creator/:deckId" element={<DeckCreator />} /> <Route path="/deck-creator/:deckId" element={<DeckCreator />} />
<Route path="/game" element={<GameScreen />} /> <Route path="/game" element={<GameScreen />} />
<Route path="/companies" element={<CompanyHub />} /> <Route path="/contacts" element={<CompanyHub />} />
<Route path="/report" element={<Reports />} /> <Route path="/report" element={<Reports />} />
{/* Add more routes as needed */}
</Routes> </Routes>
</Router> </Router>
{/* ✅ Toastify Container */}
<ToastConfig />
</>
) )
} }
+24 -2
View File
@@ -20,6 +20,28 @@ export const getDecksPage = async (from = 0, to = 49) => {
} }
} }
export default { // Get a specific deck by ID (authenticated)
createDeck export const getDeckById = async (deckId) => {
try {
const response = await apiClient.get(`/decks/${deckId}`)
return response.data
} catch (err) {
throw err
}
}
// Update an existing deck (authenticated)
export const updateDeck = async (deckId, deck) => {
try {
const response = await apiClient.patch(`/decks/${deckId}`, deck)
return response.data
} catch (err) {
throw err
}
}
export default {
createDeck,
getDeckById,
updateDeck
} }
+66 -14
View File
@@ -1,7 +1,7 @@
import axios from "axios" import axios from "axios"
export const API_CONFIG = { export const API_CONFIG = {
baseURL: (import.meta.env.VITE_API_URL ? import.meta.env.VITE_API_URL : '') + "/api", baseURL: (import.meta.env.VITE_API_URL ? import.meta.env.VITE_API_URL : "") + "/api",
wsURL: "http://localhost:3000", wsURL: "http://localhost:3000",
timeout: 10000, timeout: 10000,
retryAttempts: 3, retryAttempts: 3,
@@ -12,9 +12,9 @@ export const apiClient = axios.create({
timeout: API_CONFIG.timeout, timeout: API_CONFIG.timeout,
withCredentials: true, // Important for cookie-based auth withCredentials: true, // Important for cookie-based auth
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}); })
//login //login
export const login = async (username, password) => { export const login = async (username, password) => {
@@ -36,16 +36,6 @@ export const register = async (username, email, password, fname, lname, phone) =
} }
} }
//verify email
export const verifyEmail = async (token) => {
try {
const response = await apiClient.get(`/users/verify-email/${token}`)
return response.data
} catch (error) {
throw error
}
}
// Get current user's game statistics // Get current user's game statistics
export const getUserStats = async () => { export const getUserStats = async () => {
try { try {
@@ -55,3 +45,65 @@ export const getUserStats = async () => {
throw error throw error
} }
} }
// Email verification - POST
export const verifyEmail = async (token) => {
try {
const response = await apiClient.post(`/users/verify-email/${token}`);
return response;
} catch (error) {
throw error;
}
};
// Get current user profile
export const getUserProfile = async () => {
try {
const response = await apiClient.get("/users/profile");
return response.data;
} catch (error) {
throw error;
}
};
// Update current user profile
export const updateUserProfile = async (data) => {
try {
const response = await apiClient.patch("/users/profile", data);
return response.data;
} catch (error) {
throw error;
}
};
// Delete current user profile
export const deleteUserProfile = async () => {
try {
const response = await apiClient.delete("/users/profile");
return response.data;
} catch (error) {
throw error;
}
};
// Request password reset
export const forgotPassword = async (email) => {
try {
const response = await apiClient.post("/users/forgot-password", { email });
return response.data;
} catch (error) {
throw error;
}
};
// Reset password with token
export const resetPassword = async (token, newPassword) => {
try {
const response = await apiClient.post("/users/reset-password", { token, newPassword });
return response.data;
} catch (error) {
throw error;
}
};
@@ -7,6 +7,8 @@ import TaskCardEditor from "./TaskCardEditor.jsx"
import JokerCardEditor from "./JokerCardEditor.jsx" import JokerCardEditor from "./JokerCardEditor.jsx"
import LuckCardEditor from "./LuckCardEditor.jsx" import LuckCardEditor from "./LuckCardEditor.jsx"
import CardPreview from "./CardPreview.jsx" import CardPreview from "./CardPreview.jsx"
import { notifySuccess, notifyError,notifyWarning } from "../../components/Toastify/toastifyServices"
export default function CardEditor({ card, isCreating, cardType, onSave, onCancel }) { export default function CardEditor({ card, isCreating, cardType, onSave, onCancel }) {
const [cardData, setCardData] = useState(null) const [cardData, setCardData] = useState(null)
@@ -18,29 +20,45 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
id: null, id: null,
type: type, type: type,
points: 10, points: 10,
timeLimit: 30 timeLimit: 30,
consequence: { type: 0, value: 1 }
} }
switch (type) { switch (type) {
case 'task': case 'QUESTION':
return { return {
...baseData, ...baseData,
subType: 'quiz', subType: 'quiz',
question: '', question: '',
options: ['', '', '', ''], options: ['', '', '', ''],
correctAnswer: 0, correctAnswer: 0,
explanation: '' explanation: '',
acceptedAnswers: [''],
wrongConsequence: { type: 1, value: 1 }
} }
case 'joker': case 'PAIRING':
case 'MATCHING':
return {
...baseData,
type: 'QUESTION',
subType: 'matching',
taskDescription: '',
leftItems: ['', ''],
rightItems: ['', ''],
correctPairs: { 0: 0, 1: 1 },
wrongConsequence: { type: 1, value: 1 }
}
case 'JOKER':
return { return {
...baseData, ...baseData,
title: '', title: '',
description: '', description: '',
effect: '', effect: '',
actionType: 'skip', actionType: 'skip',
usage: 'once' usage: 'once',
wrongConsequence: { type: 1, value: 1 }
} }
case 'luck': case 'LUCK':
return { return {
...baseData, ...baseData,
event: '', event: '',
@@ -56,53 +74,119 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
// Kártya adatok inicializálása // Kártya adatok inicializálása
useEffect(() => { useEffect(() => {
try {
if (isCreating && cardType) { if (isCreating && cardType) {
setCardData(getDefaultCardData(cardType)) const defaultData = getDefaultCardData(cardType)
setCardData(defaultData)
} else if (card) { } else if (card) {
setCardData({ ...card }) setCardData({ ...card })
} else { } else {
setCardData(null) setCardData(null)
} }
} catch (error) {
console.error('Kártya inicializálási hiba:', error)
setCardData(null)
}
}, [card, isCreating, cardType]) }, [card, isCreating, cardType])
const handleSave = () => {
if (!cardData) return
// Validáció
if (!validateCard(cardData)) return
onSave(cardData)
}
const validateCard = (data) => { const validateCard = (data) => {
if (data.type === 'task') { try {
if (!data || !data.type) {
notifyError("Érvénytelen kártya adatok!")
return false
}
if (data.type === 'QUESTION') {
// Quiz típus validálás
if (data.subType === 'quiz') {
if (!data.question || !data.question.trim()) {
notifyError("Kérdés megadása kötelező!")
return false
}
if (data.options && data.options.some(opt => !opt.trim())) {
notifyError("Minden válaszlehetőséget ki kell tölteni!")
return false
}
}
// Igaz/Hamis típus validálás
else if (data.subType === 'truefalse') {
if (!data.statement || !data.statement.trim()) {
notifyError("Állítás megadása kötelező!")
return false
}
if (data.isTrue === undefined || data.isTrue === null) {
notifyError("Válaszd ki, hogy az állítás igaz vagy hamis!")
return false
}
}
// Párosítás típus validálás
else if (data.subType === 'matching') {
if (!data.taskDescription || !data.taskDescription.trim()) {
notifyError("Feladat leírása kötelező!")
return false
}
if (!data.leftItems || data.leftItems.length === 0) {
notifyError("Legalább egy párosítást meg kell adni!")
return false
}
if (data.leftItems.some(item => !item.trim()) || data.rightItems.some(item => !item.trim())) {
notifyError("Minden párosítási elemet ki kell tölteni!")
return false
}
}
// Szöveges válasz típus validálás
else if (data.subType === 'text') {
if (!data.question || !data.question.trim()) {
notifyError("Kérdés megadása kötelező!")
return false
}
if (!data.acceptedAnswers || data.acceptedAnswers.length === 0 || data.acceptedAnswers.every(ans => !ans.trim())) {
notifyError("Legalább egy elfogadott választ meg kell adni!")
return false
}
}
// Általános validálás (ha nincs subType megadva)
else {
if (!data.question && !data.statement) { if (!data.question && !data.statement) {
alert("Kérdés vagy állítás megadása kötelező!") notifyError("Kérdés vagy állítás megadása kötelező!")
return false return false
} }
if (data.subType === 'quiz' && data.options.some(opt => !opt.trim())) {
alert("❌ Minden válaszlehetőséget ki kell tölteni!")
return false
} }
} else if (data.type === 'joker') { } else if (data.type === 'JOKER') {
if (!data.text || !data.text.trim()) { if (!data.text || !data.text.trim()) {
alert("Joker kártya szövege nem lehet üres!") notifyError("Joker kártya szövege nem lehet üres!")
return false return false
} }
} else if (data.type === 'luck') { } else if (data.type === 'LUCK') {
if (!data.text || !data.text.trim()) { if (!data.text || !data.text.trim()) {
alert("Szerencse kártya szövege nem lehet üres!") notifyError("Szerencse kártya szövege nem lehet üres!")
return false return false
} }
} }
return true 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
}
} }
const updateCardData = (updates) => { const updateCardData = (updates) => {
setCardData(prev => prev ? { ...prev, ...updates } : null) setCardData(prev => prev ? { ...prev, ...updates } : null)
} }
const handleSave = () => {
if (!cardData) {
notifyError("Nincs mentendő kártya adat!")
return
}
if (!validateCard(cardData)) return
onSave(cardData)
}
// Ha nincs kiválasztott kártya vagy új kártya létrehozás // Ha nincs kiválasztott kártya vagy új kártya létrehozás
if (!cardData) { if (!cardData) {
return ( return (
@@ -123,24 +207,47 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
return ( return (
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
{/* Type Mismatch Warning */}
{cardData?.type && cardType && cardData.type !== cardType && !isCreating && (
<div className="bg-[color:var(--color-error)]/10 border-l-4 border-[color:var(--color-error)] px-6 py-4">
<div className="flex items-center gap-3">
<div className="text-[color:var(--color-error)] text-xl"></div>
<div>
<div className="text-[color:var(--color-error)] font-semibold">
Figyelmeztetés: Nem megfelelő kártya típus
</div>
<div className="text-[color:var(--color-error)]/80 text-sm">
{`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'
}.`}
</div>
</div>
</div>
</div>
)}
{/* Header */} {/* Header */}
<div className="bg-[color:var(--color-surface)] border-b border-[color:var(--color-surface-selected)] px-6 py-4"> <div className="bg-[color:var(--color-surface)] border-b border-[color:var(--color-surface-selected)] px-6 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="text-2xl"> <div className="text-2xl">
{cardData.type === 'task' && '📋'} {cardData.type === 'QUESTION' && '📋'}
{cardData.type === 'joker' && '🃏'} {cardData.type === 'JOKER' && '🃏'}
{cardData.type === 'luck' && '🎲'} {cardData.type === 'LUCK' && '🎲'}
</div> </div>
<div> <div>
<h2 className="text-xl font-bold text-[color:var(--color-text)]"> <h2 className="text-xl font-bold text-[color:var(--color-text)]">
{isCreating ? 'Új' : 'Szerkesztés'} {' '} {isCreating ? 'Új' : 'Szerkesztés'} {' '}
{cardData.type === 'task' && 'Feladat kártya'} {(isCreating ? cardType : cardData.type) === 'QUESTION' && 'Feladat kártya'}
{cardData.type === 'joker' && 'Joker kártya'} {(isCreating ? cardType : cardData.type) === 'JOKER' && 'Joker kártya'}
{cardData.type === 'luck' && 'Szerencse kártya'} {(isCreating ? cardType : cardData.type) === 'LUCK' && 'Szerencse kártya'}
</h2> </h2>
<div className="text-[color:var(--color-text-muted)] text-sm"> <div className="text-[color:var(--color-text-muted)] text-sm">
{cardData.type === 'task' && cardData.subType && ( {cardData.type === 'QUESTION' && cardData.subType && (
<> <>
{cardData.subType === 'quiz' && 'Quiz (A/B/C/D)'} {cardData.subType === 'quiz' && 'Quiz (A/B/C/D)'}
{cardData.subType === 'truefalse' && 'Igaz/Hamis'} {cardData.subType === 'truefalse' && 'Igaz/Hamis'}
@@ -168,7 +275,10 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
</button> </button>
<button <button
onClick={onCancel} onClick={() => {
notifyWarning('Kártya készítés megszakítva')
onCancel()
}}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-[color:var(--color-background)] hover:bg-[color:var(--color-surface-selected)] text-[color:var(--color-text)] transition-all duration-200" className="flex items-center gap-2 px-4 py-2 rounded-xl bg-[color:var(--color-background)] hover:bg-[color:var(--color-surface-selected)] text-[color:var(--color-text)] transition-all duration-200"
> >
<FaTimes /> <FaTimes />
@@ -189,28 +299,26 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
{/* Content */} {/* Content */}
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
{showPreview ? ( {showPreview ? (
/* Preview Mode */
<div className="h-full bg-[color:var(--color-background)] flex items-center justify-center p-6"> <div className="h-full bg-[color:var(--color-background)] flex items-center justify-center p-6">
<CardPreview card={cardData} /> <CardPreview card={cardData} />
</div> </div>
) : ( ) : (
/* Edit Mode */
<div className="h-full overflow-y-auto p-6"> <div className="h-full overflow-y-auto p-6">
{cardData.type === 'task' && ( {cardData.type === 'QUESTION' && (
<TaskCardEditor <TaskCardEditor
card={cardData} card={cardData}
onChange={updateCardData} onChange={updateCardData}
/> />
)} )}
{cardData.type === 'joker' && ( {cardData.type === 'JOKER' && (
<JokerCardEditor <JokerCardEditor
card={cardData} card={cardData}
onChange={updateCardData} onChange={updateCardData}
/> />
)} )}
{cardData.type === 'luck' && ( {cardData.type === 'LUCK' && (
<LuckCardEditor <LuckCardEditor
card={cardData} card={cardData}
onChange={updateCardData} onChange={updateCardData}
@@ -1,13 +1,23 @@
// src/components/DeckCreator/CardsList.jsx // src/components/DeckCreator/CardsList.jsx
// Bal oldali kártyák listája és új kártya létrehozás // Bal oldali kártyák listája és új kártya létrehozás
import React from "react" import React, { useState } from "react"
import { FaPlus, FaEdit, FaTrash, FaQuestionCircle, FaCheck, FaTimes, FaDice, FaTheaterMasks } from "react-icons/fa" import {
FaPlus,
FaEdit,
FaTrash,
FaQuestionCircle,
FaCheck,
FaTimes,
FaDice,
FaTheaterMasks
} from "react-icons/fa"
import { notifySuccess, notifyError } from "../../components/Toastify/toastifyServices"
const cardTypeIcons = { const cardTypeIcons = {
task: { icon: FaQuestionCircle, color: "var(--color-question)" }, QUESTION: { icon: FaQuestionCircle, color: "var(--color-question)" },
joker: { icon: FaTheaterMasks, color: "var(--color-fun)" }, JOKER: { icon: FaTheaterMasks, color: "var(--color-fun)" },
luck: { icon: FaDice, color: "var(--color-luck)" } LUCK: { icon: FaDice, color: "var(--color-luck)" }
} }
const cardSubTypeLabels = { const cardSubTypeLabels = {
@@ -20,84 +30,80 @@ const cardSubTypeLabels = {
export default function CardsList({ export default function CardsList({
cards, cards,
selectedCard, selectedCard,
deckType,
onSelectCard, onSelectCard,
onCreateCard, onCreateCard,
onDeleteCard, onDeleteCard,
isCreatingCard, isCreatingCard,
newCardType newCardType
}) { }) {
const [confirmingDelete, setConfirmingDelete] = useState(null)
const getCardPreview = (card) => { const getCardPreview = (card) => {
if (card.type === 'task') { if (card.type === 'QUESTION') {
return card.question || card.statement || 'Új feladat kártya' return card.question || card.statement || 'Új feladat kártya'
} }
if (card.type === 'joker') { if (card.type === 'JOKER') {
return card.text || 'Új joker kártya' return card.text || 'Új joker kártya'
} }
if (card.type === 'luck') { if (card.type === 'LUCK') {
return card.text || 'Új szerencse kártya' return card.text || 'Új szerencse kártya'
} }
return 'Ismeretlen kártya' return "Ismeretlen kártya"
} }
const getCardTypeLabel = (card) => { const getCardTypeLabel = (card) => {
if (card.type === 'task') { if (card.type === 'QUESTION') {
if (card.subType) { if (card.subType) {
return cardSubTypeLabels[card.subType] || 'Feladat' return cardSubTypeLabels[card.subType] || "Feladat"
} }
return 'Feladat' return "Feladat"
} }
if (card.type === 'joker') { if (card.type === 'JOKER') {
return 'Joker' return 'Joker'
} }
if (card.type === 'luck') { if (card.type === 'LUCK') {
return 'Szerencse' return 'Szerencse'
} }
return 'Ismeretlen' return "Ismeretlen"
}
const handleConfirmDelete = () => {
if (confirmingDelete) {
onDeleteCard(confirmingDelete)
notifySuccess("Kártya sikeresen törölve a pakliból!")
setConfirmingDelete(null)
}
}
const handleCancelDelete = () => {
setConfirmingDelete(null)
} }
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full relative">
{/* Header */} {/* Header */}
<div className="p-4 border-b border-[color:var(--color-surface-selected)]"> <div className="p-4 border-b border-[color:var(--color-surface-selected)]">
<h2 className="text-lg font-bold text-[color:var(--color-text)] mb-4 flex items-center gap-2"> <h2 className="text-lg font-bold text-[color:var(--color-text)] mb-4 flex items-center gap-2">
🃏 Kártyák 🃏 Kártyák
</h2> </h2>
{/* New Card Dropdown */} {/* New Card Button */}
<div className="relative group"> <button
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl bg-[color:var(--color-success)] hover:bg-[color:var(--color-success)]/80 text-[color:var(--color-text-inverse)] font-semibold transition-all duration-200 hover:scale-105 shadow-lg"> onClick={() => onCreateCard(deckType)}
className={`w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-[color:var(--color-text-inverse)] font-semibold transition-all duration-200 hover:scale-105 shadow-lg ${
deckType === 'QUESTION' ? 'bg-[color:var(--color-question)] hover:bg-[color:var(--color-question)]/80' :
deckType === 'LUCK' ? 'bg-[color:var(--color-luck)] hover:bg-[color:var(--color-luck)]/80' :
'bg-[color:var(--color-fun)] hover:bg-[color:var(--color-fun)]/80'
}`}
>
<FaPlus /> <FaPlus />
Új kártya <span>
{deckType === 'QUESTION' && '📋 Új feladat kártya'}
{deckType === 'JOKER' && '🃏 Új joker kártya'}
{deckType === 'LUCK' && '🎲 Új szerencse kártya'}
</span>
</button> </button>
{/* Dropdown Menu */}
<div className="absolute top-full left-0 right-0 mt-2 bg-[color:var(--color-card)] rounded-xl shadow-lg border border-[color:var(--color-surface-selected)] opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-10">
<button
onClick={() => onCreateCard('task')}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-[color:var(--color-surface-selected)] text-[color:var(--color-text)] transition-colors duration-200 rounded-t-xl"
>
<FaQuestionCircle className="text-[color:var(--color-question)]" />
📋 Feladat kártya
</button>
<button
onClick={() => onCreateCard('joker')}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-[color:var(--color-surface-selected)] text-[color:var(--color-text)] transition-colors duration-200"
>
<FaTheaterMasks className="text-[color:var(--color-fun)]" />
🃏 Joker kártya
</button>
<button
onClick={() => onCreateCard('luck')}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-[color:var(--color-surface-selected)] text-[color:var(--color-text)] transition-colors duration-200 rounded-b-xl"
>
<FaDice className="text-[color:var(--color-luck)]" />
🎲 Szerencse kártya
</button>
</div>
</div>
</div> </div>
{/* Cards List */} {/* Cards List */}
@@ -115,7 +121,7 @@ export default function CardsList({
)} )}
<div> <div>
<div className="text-[color:var(--color-text)] font-medium"> <div className="text-[color:var(--color-text)] font-medium">
Új {newCardType === 'task' ? 'feladat' : newCardType === 'joker' ? 'joker' : 'szerencse'} kártya Új {newCardType === "QUESTION" ? "feladat" : newCardType === "JOKER" ? "joker" : "szerencse"} kártya
</div> </div>
<div className="text-[color:var(--color-text-muted)] text-sm"> <div className="text-[color:var(--color-text-muted)] text-sm">
Szerkesztés folyamatban... Szerkesztés folyamatban...
@@ -135,17 +141,31 @@ export default function CardsList({
key={card.id} key={card.id}
onClick={() => onSelectCard(card)} onClick={() => onSelectCard(card)}
className={` 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' isSelected
: 'bg-[color:var(--color-background)]/50 border-[color:var(--color-surface-selected)] hover:bg-[color:var(--color-background)]/80' ? "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 && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="absolute inset-0 bg-[color:var(--color-error)]/5 backdrop-blur-[1px] rounded-xl"></div>
<div className="absolute rotate-[-15deg] border-t-2 border-[color:var(--color-error)] w-full"></div>
<span className="absolute top-1 right-1 text-xs text-[color:var(--color-error)] bg-[color:var(--color-error)]/10 px-2 py-1 rounded-lg">
Nem megfelelő típus
</span>
</div>
)}
{/* Card Header */} {/* Card Header */}
<div className="flex items-start justify-between gap-2 mb-3"> <div className="flex items-start justify-between gap-2 mb-3">
<div className="flex items-center gap-3 flex-1 min-w-0"> <div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex items-center justify-center w-10 h-10 rounded-full border-2" style={{ borderColor: cardIcon.color }}> <div
className="flex items-center justify-center w-10 h-10 rounded-full border-2"
style={{ borderColor: cardIcon.color }}
>
{React.createElement(cardIcon.icon, { {React.createElement(cardIcon.icon, {
style: { color: cardIcon.color }, style: { color: cardIcon.color },
className: "text-lg" className: "text-lg"
@@ -169,7 +189,7 @@ export default function CardsList({
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onDeleteCard(card.id) setConfirmingDelete(card.id)
}} }}
className="p-1.5 rounded-lg bg-[color:var(--color-error)]/10 hover:bg-[color:var(--color-error)]/20 text-[color:var(--color-error)] transition-colors duration-200" className="p-1.5 rounded-lg bg-[color:var(--color-error)]/10 hover:bg-[color:var(--color-error)]/20 text-[color:var(--color-error)] transition-colors duration-200"
> >
@@ -183,10 +203,10 @@ export default function CardsList({
<div <div
className="text-[color:var(--color-text)] text-sm leading-relaxed" className="text-[color:var(--color-text)] text-sm leading-relaxed"
style={{ style={{
display: '-webkit-box', display: "-webkit-box",
WebkitLineClamp: 2, WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical', WebkitBoxOrient: "vertical",
overflow: 'hidden' overflow: "hidden"
}} }}
> >
{getCardPreview(card)} {getCardPreview(card)}
@@ -209,20 +229,40 @@ export default function CardsList({
)} )}
</div> </div>
{/* Confirm Delete Popup */}
{confirmingDelete && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl p-6 w-80 text-center animate-fadeIn">
<h3 className="text-lg font-semibold mb-4 text-gray-800">
Biztosan törölni szeretnéd?
</h3>
<p className="text-sm text-gray-600 mb-6">
Ez a művelet nem visszavonható.
</p>
<div className="flex justify-center gap-4">
<button
onClick={handleConfirmDelete}
className="bg-[color:var(--color-error)] text-white px-4 py-2 rounded-lg hover:bg-red-600 transition"
>
Igen
</button>
<button
onClick={handleCancelDelete}
className="bg-gray-200 px-4 py-2 rounded-lg hover:bg-gray-300 transition"
>
Mégse
</button>
</div>
</div>
</div>
)}
{/* Footer Stats */} {/* Footer Stats */}
<div className="p-4 border-t border-[color:var(--color-surface-selected)] bg-[color:var(--color-background)]/30"> <div className="p-4 border-t border-[color:var(--color-surface-selected)] bg-[color:var(--color-background)]/30">
<div className="text-center"> <div className="text-center">
<div className="text-[color:var(--color-text)] font-semibold"> <div className="text-[color:var(--color-text)] font-semibold">
📊 Összesen: {cards.length} kártya 📊 Összesen: {cards.length} kártya
</div> </div>
{cards.length > 0 && (
<div className="flex justify-center gap-4 mt-2 text-xs text-[color:var(--color-text-muted)]">
<span>📋 {cards.filter(c => c.type === 'task').length}</span>
<span>🃏 {cards.filter(c => c.type === 'joker').length}</span>
<span>🎲 {cards.filter(c => c.type === 'luck').length}</span>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -1,13 +1,13 @@
// src/components/DeckCreator/DeckHeader.jsx // src/components/DeckCreator/DeckHeader.jsx
// Deck alapadatok szerkesztése és mentés // 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" import { FaSave, FaArrowLeft, FaGlobe, FaLock, FaQuestionCircle, FaDice, FaLaughBeam } from "react-icons/fa"
const deckTypes = [ const deckTypes = [
{ value: "Question", label: "Kérdés", icon: FaQuestionCircle, color: "var(--color-question)" }, { value: "QUESTION", label: "Kérdés", icon: FaQuestionCircle, color: "var(--color-question)" },
{ value: "Luck", label: "Szerencse", icon: FaDice, color: "var(--color-luck)" }, { value: "LUCK", label: "Szerencse", icon: FaDice, color: "var(--color-luck)" },
{ value: "Fun", label: "Szórakozás", icon: FaLaughBeam, color: "var(--color-fun)" } { value: "JOKER", label: "Joker", icon: FaLaughBeam, color: "var(--color-fun)" }
] ]
const privacyOptions = [ const privacyOptions = [
@@ -16,17 +16,35 @@ const privacyOptions = [
] ]
export default function DeckHeader({ deck, onUpdate, onSave, onBack }) { 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 currentDeckType = deckTypes.find(type => type.value === deck.type) || deckTypes[0]
const currentPrivacy = privacyOptions.find(option => option.value === deck.privacy) || privacyOptions[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) => { const handleInputChange = (field, value) => {
onUpdate({ [field]: value }) onUpdate({ [field]: value })
} }
const cardsCount = deck.cards?.length || 0 // Remove unused card count variables
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
return ( return (
<div className="bg-[color:var(--color-surface)] border-b border-[color:var(--color-surface-selected)] px-6 py-4"> <div className="bg-[color:var(--color-surface)] border-b border-[color:var(--color-surface-selected)] px-6 py-4">
@@ -42,7 +60,7 @@ export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {
</button> </button>
<h1 className="text-2xl font-bold text-[color:var(--color-text)]"> <h1 className="text-2xl font-bold text-[color:var(--color-text)]">
📝 Deck Szerkesztés 📝 Pakli Szerkesztés
</h1> </h1>
</div> </div>
@@ -56,41 +74,79 @@ export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {
</div> </div>
{/* Main Content Row */} {/* Main Content Row */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-end"> <div className="space-y-4">
{/* Deck Basic Info */} {/* Two Column Layout */}
<div className="lg:col-span-2 space-y-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Deck Name */} {/* Deck Name - Takes up 2 columns */}
<div> <div className="md:col-span-2">
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2"> <label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
📦 Deck neve 📦 Pakli neve
</label> </label>
<input <input
type="text" type="text"
value={deck.name} value={deck.name}
onChange={(e) => handleInputChange('name', e.target.value)} onChange={(e) => 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" 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..."
/> />
</div> </div>
{/* Type and Privacy Row */} {/* Empty space for visual balance */}
<div className="hidden md:block"></div>
</div>
{/* Type, Privacy and Description Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Deck Type */} {/* Deck Type */}
<div> <div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2"> <label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
🎯 Típus 🎯 Típus
</label> </label>
<select <div className="relative">
value={deck.type} <button
onChange={(e) => handleInputChange('type', e.target.value)} type="button"
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" onClick={() => setIsTypeDropdownOpen(!isTypeDropdownOpen)}
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 flex items-center"
style={{
paddingLeft: "2.5rem"
}}
>
<div className="absolute left-4 top-1/2 -translate-y-1/2">
{React.createElement(currentDeckType.icon, {
style: { color: currentDeckType.color }
})}
</div>
{currentDeckType.label}
<div className="absolute right-4 top-1/2 -translate-y-1/2">
<svg className={`w-4 h-4 text-[color:var(--color-text-muted)] transform transition-transform ${isTypeDropdownOpen ? 'rotate-180' : ''}`} viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</div>
</button>
{isTypeDropdownOpen && (
<div
className="absolute z-10 w-full mt-1 bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] rounded-xl shadow-lg overflow-hidden"
ref={typeDropdownRef}
> >
{deckTypes.map(type => ( {deckTypes.map(type => (
<option key={type.value} value={type.value}> <button
{type.label} key={type.value}
</option> onClick={() => {
handleInputChange('type', type.value);
setIsTypeDropdownOpen(false);
}}
className={`w-full px-4 py-2 flex items-center gap-3 hover:bg-[color:var(--color-surface-selected)] transition-colors text-[color:var(--color-text)] ${deck.type === type.value ? 'bg-[color:var(--color-surface-selected)]' : ''}`}
>
{React.createElement(type.icon, {
style: { color: type.color }
})}
<span>{type.label}</span>
</button>
))} ))}
</select> </div>
)}
</div>
</div> </div>
{/* Privacy */} {/* Privacy */}
@@ -98,17 +154,51 @@ export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2"> <label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
👁 Láthatóság 👁 Láthatóság
</label> </label>
<select <div className="relative">
value={deck.privacy} <button
onChange={(e) => handleInputChange('privacy', e.target.value)} type="button"
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" onClick={() => setIsPrivacyDropdownOpen(!isPrivacyDropdownOpen)}
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 flex items-center"
style={{
paddingLeft: "2.5rem"
}}
>
<div className="absolute left-4 top-1/2 -translate-y-1/2">
{React.createElement(currentPrivacy.icon, {
className: "text-[color:var(--color-text)]"
})}
</div>
{currentPrivacy.label}
<div className="absolute right-4 top-1/2 -translate-y-1/2">
<svg className={`w-4 h-4 text-[color:var(--color-text-muted)] transform transition-transform ${isPrivacyDropdownOpen ? 'rotate-180' : ''}`} viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</div>
</button>
{isPrivacyDropdownOpen && (
<div
className="absolute z-10 w-full mt-1 bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] rounded-xl shadow-lg overflow-hidden"
ref={privacyDropdownRef}
> >
{privacyOptions.map(option => ( {privacyOptions.map(option => (
<option key={option.value} value={option.value}> <button
{option.label} key={option.value}
</option> onClick={() => {
handleInputChange('privacy', option.value);
setIsPrivacyDropdownOpen(false);
}}
className={`w-full px-4 py-2 flex items-center gap-3 hover:bg-[color:var(--color-surface-selected)] transition-colors text-[color:var(--color-text)] ${deck.privacy === option.value ? 'bg-[color:var(--color-surface-selected)]' : ''}`}
>
{React.createElement(option.icon, {
className: "text-[color:var(--color-text)]"
})}
<span>{option.label}</span>
</button>
))} ))}
</select> </div>
)}
</div>
</div> </div>
{/* Description */} {/* Description */}
@@ -126,36 +216,6 @@ export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {
</div> </div>
</div> </div>
</div> </div>
{/* Stats Panel */}
<div className="bg-[color:var(--color-background)]/50 rounded-xl p-4 border border-[color:var(--color-surface-selected)]">
<h3 className="text-[color:var(--color-text)] font-semibold mb-3 flex items-center gap-2">
📊 Statisztikák
</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between items-center">
<span className="text-[color:var(--color-text-muted)]">Összes kártya:</span>
<span className="text-[color:var(--color-text)] font-semibold">{cardsCount}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[color:var(--color-text-muted)]">📋 Feladat:</span>
<span className="text-[color:var(--color-question)] font-semibold">{taskCards}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[color:var(--color-text-muted)]">🃏 Joker:</span>
<span className="text-[color:var(--color-success)] font-semibold">{jokerCards}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[color:var(--color-text-muted)]">🎲 Szerencse:</span>
<span className="text-[color:var(--color-luck)] font-semibold">{luckCards}</span>
</div>
</div>
</div>
</div>
</div> </div>
) )
} }
@@ -9,6 +9,8 @@ import {
FaSortAlphaDown, FaSortAlphaDown,
FaSortAlphaUp, FaSortAlphaUp,
FaQuestionCircle, FaQuestionCircle,
FaChevronLeft,
FaChevronRight,
} from "react-icons/fa" } from "react-icons/fa"
import SearchBox from "../Search/SearchBox" import SearchBox from "../Search/SearchBox"
import PopUp from "../PopUp/PopUp" import PopUp from "../PopUp/PopUp"
@@ -17,7 +19,7 @@ import DeckInfoPopUp from "../PopUp/DeckInfoPopUp"
const deckTypes = [ const deckTypes = [
{ label: "Luck", color: "var(--color-luck)" }, { label: "Luck", color: "var(--color-luck)" },
{ label: "Question", color: "var(--color-question)" }, { 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 // initial state will be fetched from backend
@@ -70,18 +72,25 @@ const DeckManager = () => {
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [showSortHelp, setShowSortHelp] = useState(false) const [showSortHelp, setShowSortHelp] = useState(false)
const [selectedDeck, setSelectedDeck] = useState(null) const [selectedDeck, setSelectedDeck] = useState(null)
const [decks, setDecks] = useState([]) const [allDecks, setAllDecks] = useState([]) // Összes pakli
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [itemsPerPage, setItemsPerPage] = useState(20)
const [currentPage, setCurrentPage] = useState(1)
// Load all decks once
useEffect(() => { useEffect(() => {
let mounted = true let mounted = true
const load = async () => { const load = async () => {
setLoading(true) setLoading(true)
try { try {
const result = await import('../../api/deckApi').then(m => m.getDecksPage(0, 49)) // Load all decks (0-99 is the max limit = 100 decks)
const result = await import('../../api/deckApi').then(m => m.getDecksPage(0, 99))
if (!mounted) return if (!mounted) return
// map backend deck shape to UI shape
const mapped = result.decks.map(d => ({ console.log('Loaded decks:', result) // Debug
// Map backend deck shape to UI shape
const mapped = (result.decks || []).map(d => ({
id: d.id, id: d.id,
name: d.name, name: d.name,
type: d.type === 2 ? 'Question' : d.type === 1 ? 'Joker' : 'Luck', type: d.type === 2 ? 'Question' : d.type === 1 ? 'Joker' : 'Luck',
@@ -89,7 +98,9 @@ const DeckManager = () => {
origin: d.ctype === 2 ? 'Vállalati' : d.ctype === 0 ? 'Mind' : 'Saját', origin: d.ctype === 2 ? 'Vállalati' : d.ctype === 0 ? 'Mind' : 'Saját',
raw: d raw: d
})) }))
setDecks(mapped)
console.log('Mapped decks:', mapped) // Debug
setAllDecks(mapped)
} catch (err) { } catch (err) {
console.error('Failed to load decks', err) console.error('Failed to load decks', err)
} finally { } finally {
@@ -101,8 +112,7 @@ const DeckManager = () => {
}, []) }, [])
// Filter logic // Filter logic
const sourceDecks = decks let filteredDecks = allDecks.filter((deck) => {
let filteredDecks = sourceDecks.filter((deck) => {
const typeMatch = selectedType === "All" || deck.type === selectedType const typeMatch = selectedType === "All" || deck.type === selectedType
const originMatch = selectedOrigin === "Mind" || deck.origin === selectedOrigin const originMatch = selectedOrigin === "Mind" || deck.origin === selectedOrigin
const searchMatch = !search || deck.name.toLowerCase().includes(search.toLowerCase()) const searchMatch = !search || deck.name.toLowerCase().includes(search.toLowerCase())
@@ -123,11 +133,23 @@ const DeckManager = () => {
return 0 return 0
}) })
// Pagination logic - frontend only
const totalDecks = filteredDecks.length
const totalPages = Math.ceil(totalDecks / itemsPerPage)
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const paginatedDecks = filteredDecks.slice(startIndex, endIndex)
// Reset to page 1 when filters or items per page change
useEffect(() => {
setCurrentPage(1)
}, [selectedType, selectedOrigin, search, sortBy, itemsPerPage])
return ( return (
<div className="w-full flex flex-col bg-[color:var(--color-background)]"> <div className="w-full flex flex-col bg-[color:var(--color-background)]">
<div className="w-full max-w-6xl mx-auto px-4 py-10"> <div className="w-full max-w-[1200px] mx-auto px-4 py-10">
{/* Filters */} {/* Filters */}
<div className="flex flex-col md:flex-row gap-4 justify-between items-center mb-10 bg-[color:var(--color-surface)]/80 backdrop-blur-lg rounded-2xl px-6 py-4 shadow-lg"> <div className="flex flex-col md:flex-row gap-3 justify-between items-center mb-10 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"> <div className="flex gap-2 items-center w-full md:w-auto">
<SearchBox <SearchBox
value={search} value={search}
@@ -163,8 +185,8 @@ const DeckManager = () => {
? "Szerencse" ? "Szerencse"
: type.label === "Question" : type.label === "Question"
? "Kérdés" ? "Kérdés"
: type.label === "Fun" : type.label === "Joker"
? "Szórakozás" ? "Joker"
: type.label} : type.label}
</button> </button>
))} ))}
@@ -263,11 +285,41 @@ const DeckManager = () => {
)} )}
</div> </div>
</div> </div>
{/* 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={20}>20</option>
<option value={30}>30</option>
<option value={40}>40</option>
<option value={50}>50</option>
</select>
</div>
<div className="text-[color:var(--color-text-muted)] text-sm">
{totalDecks > 0 ? (
<>
{startIndex + 1}-{Math.min(endIndex, totalDecks)} / {totalDecks} pakli
</>
) : (
<>0 pakli</>
)}
</div>
</div>
{/* Decks Grid */} {/* Decks Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-8 mt-8"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-8 mt-8">
{/* Create New Deck (Mockup) */} {/* Create New Deck (Mockup) */}
<div <div
onClick={() => navigate('/deck-creator')} onClick={() => navigate("/deck-creator")}
className="flex flex-col items-center justify-center h-48 bg-[color:var(--color-card)] border-2 border-dashed border-[color:var(--color-success)] rounded-2xl cursor-pointer hover:bg-[color:var(--color-success)]/20 transition-all duration-200 shadow-lg" className="flex flex-col items-center justify-center h-48 bg-[color:var(--color-card)] border-2 border-dashed border-[color:var(--color-success)] rounded-2xl cursor-pointer hover:bg-[color:var(--color-success)]/20 transition-all duration-200 shadow-lg"
> >
<FaPlus style={{ color: "var(--color-success)" }} className="text-5xl mb-2" /> <FaPlus style={{ color: "var(--color-success)" }} className="text-5xl mb-2" />
@@ -280,7 +332,7 @@ const DeckManager = () => {
{!loading && filteredDecks.length === 0 && ( {!loading && filteredDecks.length === 0 && (
<div className="col-span-full text-center text-[color:var(--color-text-muted)]">Nincsenek mentett paklik.</div> <div className="col-span-full text-center text-[color:var(--color-text-muted)]">Nincsenek mentett paklik.</div>
)} )}
{!loading && filteredDecks.map((deck) => { {!loading && paginatedDecks.map((deck) => {
const deckType = deckTypes.find((t) => t.label === deck.type) const deckType = deckTypes.find((t) => t.label === deck.type)
const borderColor = deckType ? deckType.color : "var(--color-success)" const borderColor = deckType ? deckType.color : "var(--color-success)"
return ( return (
@@ -303,7 +355,7 @@ const DeckManager = () => {
: deck.type === "Question" : deck.type === "Question"
? "Kérdés" ? "Kérdés"
: deck.type === "Fun" : deck.type === "Fun"
? "Szórakozás" ? "Joker"
: deck.type} : deck.type}
</span> </span>
<h2 className="text-xl font-bold text-[color:var(--color-text)] mb-1 truncate"> <h2 className="text-xl font-bold text-[color:var(--color-text)] mb-1 truncate">
@@ -317,15 +369,77 @@ const DeckManager = () => {
) )
})} })}
</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
// Show first page, last page, current page and neighbors
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>
)}
</div> </div>
{/* Deck Info Popup */} {/* Deck Info Popup */}
{selectedDeck && ( {selectedDeck && <DeckInfoPopUp deck={selectedDeck} onClose={() => setSelectedDeck(null)} />}
<DeckInfoPopUp
deck={selectedDeck}
onClose={() => setSelectedDeck(null)}
/>
)}
</div> </div>
) )
} }
@@ -4,17 +4,29 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { FaTheaterMasks, FaInfoCircle, FaUsers } from 'react-icons/fa' 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 }) { export default function JokerCardEditor({ card, onChange }) {
const [cardData, setCardData] = useState({ const [cardData, setCardData] = useState({
type: 'joker', type: 'JOKER',
text: '' text: '',
consequence: { type: 0, value: 1 },
wrongConsequence: { type: 1, value: 1 }
}) })
useEffect(() => { useEffect(() => {
if (card) { if (card) {
setCardData({ setCardData({
type: 'joker', type: 'JOKER',
text: card.text || '' text: card.text || '',
consequence: card.consequence || { type: 0, value: 1 },
wrongConsequence: card.wrongConsequence || { type: 1, value: 1 }
}) })
} }
}, [card]) }, [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 // Példa joker kártyák
const exampleCards = [ const exampleCards = [
"Felelsz vagy mersz? (Az előző játékos kérdez)", "Felelsz vagy mersz? (Az előző játékos kérdez)",
@@ -57,18 +99,10 @@ export default function JokerCardEditor({ card, onChange }) {
} }
return ( return (
<div className="max-w-4xl mx-auto"> <div className="space-y-6">
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
{/* Header */}
<div className="flex items-center gap-3 mb-6">
<FaTheaterMasks className="text-2xl text-[color:var(--color-fun)]" />
<h3 className="text-xl font-bold text-[color:var(--color-text)]">
Joker Kártya Szerkesztő
</h3>
</div>
{/* Info box */} {/* Info box */}
<div className="bg-[color:var(--color-fun)]/10 border border-[color:var(--color-fun)]/30 rounded-lg p-4 mb-6"> <div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<div className="bg-[color:var(--color-fun)]/10 border border-[color:var(--color-fun)]/30 rounded-lg p-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<FaInfoCircle className="text-[color:var(--color-fun)] mt-1 flex-shrink-0" /> <FaInfoCircle className="text-[color:var(--color-fun)] mt-1 flex-shrink-0" />
<div> <div>
@@ -86,18 +120,24 @@ export default function JokerCardEditor({ card, onChange }) {
</div> </div>
</div> </div>
</div> </div>
</div>
{/* Kártya szövege */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4 flex items-center gap-2">
<FaTheaterMasks className="text-[color:var(--color-fun)]" />
Kártya szövege
</h3>
{/* Card text input */}
<div className="space-y-4">
<div> <div>
<label className="block text-[color:var(--color-text)] font-medium mb-2"> <label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Joker kártya feladat * Joker kártya feladat *
</label> </label>
<textarea <textarea
value={cardData.text} value={cardData.text}
onChange={handleTextChange} onChange={handleTextChange}
placeholder="Pl: Felelsz vagy mersz? (Az előző játékos kérdez)" placeholder="Pl: Felelsz vagy mersz? (Az előző játékos kérdez)"
className="w-full h-32 p-4 bg-[color:var(--color-surface)] border border-[color:var(--color-border)] rounded-lg text-[color:var(--color-text)] placeholder-[color:var(--color-text-muted)] resize-none focus:outline-none focus:border-[color:var(--color-fun)] transition-colors" className="w-full h-32 px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] placeholder-[color:var(--color-text-muted)] resize-none focus:ring-2 focus:ring-[color:var(--color-fun)] focus:border-transparent outline-none transition-all duration-200"
maxLength={150} maxLength={150}
/> />
<div className="flex justify-between items-center mt-2"> <div className="flex justify-between items-center mt-2">
@@ -109,7 +149,6 @@ export default function JokerCardEditor({ card, onChange }) {
</span> </span>
</div> </div>
</div> </div>
</div>
{/* Example cards */} {/* Example cards */}
<div className="mt-6"> <div className="mt-6">
@@ -147,6 +186,100 @@ export default function JokerCardEditor({ card, onChange }) {
</div> </div>
)} )}
</div> </div>
{/* Következmények (teljesítés esetén) */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
🎯 Következmények (teljesítés esetén)
</h3>
<div className="space-y-4">
{/* Consequence Type */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Hatás típusa
</label>
<select
value={cardData.consequence?.type ?? 0}
onChange={(e) => updateConsequence('type', parseInt(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"
>
{consequenceTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
<div className="text-xs text-[color:var(--color-text-muted)] mt-1">
{consequenceTypes.find(t => t.value === (cardData.consequence?.type ?? 0))?.description}
</div>
</div>
{/* Consequence Value - csak ha előre/hátra lépés */}
{(cardData.consequence?.type === 0 || cardData.consequence?.type === 1) && (
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Mezők száma
</label>
<input
type="number"
value={cardData.consequence?.value ?? 1}
onChange={(e) => updateConsequence('value', parseInt(e.target.value) || 1)}
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"
min="1"
max="10"
/>
</div>
)}
</div>
</div>
{/* Következmények (nem teljesítés esetén) */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
Következmények (nem teljesítés esetén)
</h3>
<div className="space-y-4">
{/* Wrong Consequence Type */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Hatás típusa
</label>
<select
value={cardData.wrongConsequence?.type ?? 1}
onChange={(e) => updateWrongConsequence('type', parseInt(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-danger)] focus:border-transparent outline-none transition-all duration-200"
>
{consequenceTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
<div className="text-xs text-[color:var(--color-text-muted)] mt-1">
{consequenceTypes.find(t => t.value === (cardData.wrongConsequence?.type ?? 1))?.description}
</div>
</div>
{/* Wrong Consequence Value - csak ha előre/hátra lépés */}
{(cardData.wrongConsequence?.type === 0 || cardData.wrongConsequence?.type === 1) && (
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Mezők száma
</label>
<input
type="number"
value={cardData.wrongConsequence?.value ?? 1}
onChange={(e) => updateWrongConsequence('value', parseInt(e.target.value) || 1)}
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-danger)] focus:border-transparent outline-none transition-all duration-200"
min="1"
max="10"
/>
</div>
)}
</div>
</div>
</div> </div>
) )
} }
@@ -4,17 +4,27 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { FaDice, FaInfoCircle } from 'react-icons/fa' import { FaDice, FaInfoCircle } 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 LuckCardEditor({ card, onChange }) { export default function LuckCardEditor({ card, onChange }) {
const [cardData, setCardData] = useState({ const [cardData, setCardData] = useState({
type: 'luck', type: 'LUCK',
text: '' text: '',
consequence: { type: 0, value: 1 }
}) })
useEffect(() => { useEffect(() => {
if (card) { if (card) {
setCardData({ setCardData({
type: 'luck', type: 'LUCK',
text: card.text || '' text: card.text || '',
consequence: card.consequence || { type: 0, value: 1 }
}) })
} }
}, [card]) }, [card])
@@ -31,19 +41,26 @@ export default function LuckCardEditor({ card, onChange }) {
} }
} }
return ( const updateConsequence = (field, value) => {
<div className="max-w-4xl mx-auto"> const newCardData = {
<div className="bg-[color:var(--color-surface)] rounded-xl p-6"> ...cardData,
{/* Header */} consequence: {
<div className="flex items-center gap-3 mb-6"> ...cardData.consequence,
<FaDice className="text-2xl text-[color:var(--color-luck)]" /> [field]: value
<h3 className="text-xl font-bold text-[color:var(--color-text)]"> }
Szerencse Kártya Szerkesztő }
</h3> setCardData(newCardData)
</div>
if (onChange) {
onChange(newCardData)
}
}
return (
<div className="space-y-6">
{/* Info box */} {/* Info box */}
<div className="bg-[color:var(--color-luck)]/10 border border-[color:var(--color-luck)]/30 rounded-lg p-4 mb-6"> <div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<div className="bg-[color:var(--color-luck)]/10 border border-[color:var(--color-luck)]/30 rounded-lg p-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<FaInfoCircle className="text-[color:var(--color-luck)] mt-1 flex-shrink-0" /> <FaInfoCircle className="text-[color:var(--color-luck)] mt-1 flex-shrink-0" />
<div> <div>
@@ -60,18 +77,23 @@ export default function LuckCardEditor({ card, onChange }) {
</div> </div>
</div> </div>
</div> </div>
</div>
{/* Card text input */} {/* Kártya szövege */}
<div className="space-y-4"> <div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4 flex items-center gap-2">
<FaDice className="text-[color:var(--color-luck)]" />
Kártya szövege
</h3>
<div> <div>
<label className="block text-[color:var(--color-text)] font-medium mb-2"> <label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Kártya szövege * Szerencse esemény leírása *
</label> </label>
<textarea <textarea
value={cardData.text} value={cardData.text}
onChange={handleTextChange} onChange={handleTextChange}
placeholder="Pl: Órai projektekkel kiváltottál több vizsgát is! Lépj előre 4 mezőt" placeholder="Pl: Órai projektekkel kiváltottál több vizsgát is! Lépj előre 4 mezőt"
className="w-full h-32 p-4 bg-[color:var(--color-surface)] border border-[color:var(--color-border)] rounded-lg text-[color:var(--color-text)] placeholder-[color:var(--color-text-muted)] resize-none focus:outline-none focus:border-[color:var(--color-luck)] transition-colors" className="w-full h-32 px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] placeholder-[color:var(--color-text-muted)] resize-none focus:ring-2 focus:ring-[color:var(--color-luck)] focus:border-transparent outline-none transition-all duration-200"
maxLength={200} maxLength={200}
/> />
<div className="flex justify-between items-center mt-2"> <div className="flex justify-between items-center mt-2">
@@ -83,7 +105,6 @@ export default function LuckCardEditor({ card, onChange }) {
</span> </span>
</div> </div>
</div> </div>
</div>
{/* Preview */} {/* Preview */}
{cardData.text.trim() && ( {cardData.text.trim() && (
@@ -100,6 +121,53 @@ export default function LuckCardEditor({ card, onChange }) {
</div> </div>
)} )}
</div> </div>
{/* Következmények */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
🎯 Következmények
</h3>
<div className="space-y-4">
{/* Consequence Type */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Hatás típusa
</label>
<select
value={cardData.consequence?.type ?? 0}
onChange={(e) => updateConsequence('type', parseInt(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-luck)] focus:border-transparent outline-none transition-all duration-200"
>
{consequenceTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
<div className="text-xs text-[color:var(--color-text-muted)] mt-1">
{consequenceTypes.find(t => t.value === (cardData.consequence?.type ?? 0))?.description}
</div>
</div>
{/* Consequence Value - csak ha előre/hátra lépés */}
{(cardData.consequence?.type === 0 || cardData.consequence?.type === 1) && (
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Mezők száma
</label>
<input
type="number"
min="1"
max="10"
value={cardData.consequence?.value ?? 1}
onChange={(e) => updateConsequence('value', parseInt(e.target.value) || 1)}
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-luck)] focus:border-transparent outline-none transition-all duration-200"
/>
</div>
)}
</div>
</div>
</div> </div>
) )
} }
@@ -20,6 +20,14 @@ const timeLimits = [
{ value: 120, label: '2 perc' } { value: 120, label: '2 perc' }
] ]
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 TaskCardEditor({ card, onChange }) { export default function TaskCardEditor({ card, onChange }) {
const updateField = (field, value) => { const updateField = (field, value) => {
@@ -71,16 +79,39 @@ export default function TaskCardEditor({ card, onChange }) {
} }
const updateAcceptedAnswer = (index, value) => { const updateAcceptedAnswer = (index, value) => {
const newAnswers = [...card.acceptedAnswers] const currentAnswers = card.acceptedAnswers || ['']
const newAnswers = [...currentAnswers]
newAnswers[index] = value newAnswers[index] = value
onChange({ acceptedAnswers: newAnswers }) onChange({ acceptedAnswers: newAnswers })
} }
const removeAcceptedAnswer = (index) => { const removeAcceptedAnswer = (index) => {
const newAnswers = card.acceptedAnswers.filter((_, i) => i !== index) const currentAnswers = card.acceptedAnswers || ['']
if (currentAnswers.length <= 1) return // Legalább egy válasz maradjon
const newAnswers = currentAnswers.filter((_, i) => i !== index)
onChange({ acceptedAnswers: newAnswers }) onChange({ acceptedAnswers: newAnswers })
} }
const updateConsequence = (field, value) => {
const currentConsequence = card.consequence || { type: 0, value: 1 }
onChange({
consequence: {
...currentConsequence,
[field]: value
}
})
}
const updateWrongConsequence = (field, value) => {
const currentWrongConsequence = card.wrongConsequence || { type: 1, value: 1 }
onChange({
wrongConsequence: {
...currentWrongConsequence,
[field]: value
}
})
}
return ( return (
<div className="max-w-4xl mx-auto space-y-6"> <div className="max-w-4xl mx-auto space-y-6">
{/* Feladat típus választó */} {/* Feladat típus választó */}
@@ -347,7 +378,7 @@ export default function TaskCardEditor({ card, onChange }) {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{(card.acceptedAnswers || ['', '', '']).map((answer, index) => ( {(card.acceptedAnswers || ['']).map((answer, index) => (
<div key={index} className="flex gap-2"> <div key={index} className="flex gap-2">
<input <input
type="text" type="text"
@@ -357,7 +388,7 @@ export default function TaskCardEditor({ card, onChange }) {
placeholder={`Elfogadott válasz ${index + 1}...`} placeholder={`Elfogadott válasz ${index + 1}...`}
/> />
{(card.acceptedAnswers?.length || 0) > 1 && ( {(card.acceptedAnswers?.length || 1) > 1 && (
<button <button
onClick={() => removeAcceptedAnswer(index)} onClick={() => removeAcceptedAnswer(index)}
className="p-2 rounded-lg bg-[color:var(--color-error)]/10 text-[color:var(--color-error)] hover:bg-[color:var(--color-error)]/20 transition-all duration-200" className="p-2 rounded-lg bg-[color:var(--color-error)]/10 text-[color:var(--color-error)] hover:bg-[color:var(--color-error)]/20 transition-all duration-200"
@@ -374,13 +405,14 @@ export default function TaskCardEditor({ card, onChange }) {
</div> </div>
</div> </div>
{/* Beállítások */} {/* Beállítások - Később elérhető */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="relative">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 opacity-50 pointer-events-none blur-[1px]">
<label className="flex items-center gap-2 text-sm"> <label className="flex items-center gap-2 text-sm">
<input <input
type="checkbox" type="checkbox"
checked={card.caseSensitive || false} checked={false}
onChange={(e) => updateField('caseSensitive', e.target.checked)} disabled
className="w-4 h-4 text-[color:var(--color-success)] border-[color:var(--color-surface-selected)] rounded focus:ring-[color:var(--color-success)]" className="w-4 h-4 text-[color:var(--color-success)] border-[color:var(--color-surface-selected)] rounded focus:ring-[color:var(--color-success)]"
/> />
<span className="text-[color:var(--color-text)]">Kis/nagy betű érzékeny</span> <span className="text-[color:var(--color-text)]">Kis/nagy betű érzékeny</span>
@@ -389,8 +421,8 @@ export default function TaskCardEditor({ card, onChange }) {
<label className="flex items-center gap-2 text-sm"> <label className="flex items-center gap-2 text-sm">
<input <input
type="checkbox" type="checkbox"
checked={card.exactMatch || false} checked={false}
onChange={(e) => updateField('exactMatch', e.target.checked)} disabled
className="w-4 h-4 text-[color:var(--color-success)] border-[color:var(--color-surface-selected)] rounded focus:ring-[color:var(--color-success)]" className="w-4 h-4 text-[color:var(--color-success)] border-[color:var(--color-surface-selected)] rounded focus:ring-[color:var(--color-success)]"
/> />
<span className="text-[color:var(--color-text)]">Pontos egyezés</span> <span className="text-[color:var(--color-text)]">Pontos egyezés</span>
@@ -399,14 +431,24 @@ export default function TaskCardEditor({ card, onChange }) {
<label className="flex items-center gap-2 text-sm"> <label className="flex items-center gap-2 text-sm">
<input <input
type="checkbox" type="checkbox"
checked={card.partialAccepted || true} checked={false}
onChange={(e) => updateField('partialAccepted', e.target.checked)} disabled
className="w-4 h-4 text-[color:var(--color-success)] border-[color:var(--color-surface-selected)] rounded focus:ring-[color:var(--color-success)]" className="w-4 h-4 text-[color:var(--color-success)] border-[color:var(--color-surface-selected)] rounded focus:ring-[color:var(--color-success)]"
/> />
<span className="text-[color:var(--color-text)]">Részleges elfogadás</span> <span className="text-[color:var(--color-text)]">Részleges elfogadás</span>
</label> </label>
</div> </div>
{/* Hamarosan elérhető felület */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="bg-[color:var(--color-warning)]/20 backdrop-blur-sm border-2 border-[color:var(--color-warning)] rounded-lg px-4 py-2">
<span className="text-[color:var(--color-warning)] font-semibold text-sm flex items-center gap-2">
🚧 Hamarosan elérhető
</span>
</div>
</div>
</div>
{/* Tipp */} {/* Tipp */}
<div className="mt-4"> <div className="mt-4">
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2"> <label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
@@ -423,12 +465,14 @@ export default function TaskCardEditor({ card, onChange }) {
</div> </div>
)} )}
{/* Közös beállítások */} {/* Közös beállítások - Később elérhető */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6"> <div className="bg-[color:var(--color-surface)] rounded-xl p-6 relative">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4"> <h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
Beállítások Beállítások
</h3> </h3>
<div className="relative">
<div className="opacity-50 pointer-events-none blur-[1px]">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Pontszám */} {/* Pontszám */}
<div> <div>
@@ -437,8 +481,8 @@ export default function TaskCardEditor({ card, onChange }) {
</label> </label>
<input <input
type="number" type="number"
value={card.points || 10} value={10}
onChange={(e) => updateField('points', parseInt(e.target.value) || 0)} disabled
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" 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"
min="0" min="0"
max="1000" max="1000"
@@ -451,15 +495,10 @@ export default function TaskCardEditor({ card, onChange }) {
Időlimit Időlimit
</label> </label>
<select <select
value={card.timeLimit || 30} disabled
onChange={(e) => updateField('timeLimit', parseInt(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" 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"
> >
{timeLimits.map(time => ( <option>30 másodperc</option>
<option key={time.value} value={time.value}>
{time.label}
</option>
))}
</select> </select>
</div> </div>
@@ -471,8 +510,8 @@ export default function TaskCardEditor({ card, onChange }) {
</label> </label>
<input <input
type="number" type="number"
value={card.characterLimit || 100} value={100}
onChange={(e) => updateField('characterLimit', parseInt(e.target.value) || 0)} disabled
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" 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"
min="10" min="10"
max="500" max="500"
@@ -487,14 +526,118 @@ export default function TaskCardEditor({ card, onChange }) {
💡 Magyarázat (opcionális) 💡 Magyarázat (opcionális)
</label> </label>
<textarea <textarea
value={card.explanation || ''} disabled
onChange={(e) => updateField('explanation', e.target.value)}
className="w-full px-4 py-3 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 resize-none" className="w-full px-4 py-3 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 resize-none"
rows="3" rows="3"
placeholder="Magyarázat a helyes válaszhoz..." placeholder="Magyarázat a helyes válaszhoz..."
/> />
</div> </div>
</div> </div>
{/* Hamarosan elérhető felület */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="bg-[color:var(--color-warning)]/20 backdrop-blur-sm border-2 border-[color:var(--color-warning)] rounded-lg px-4 py-2">
<span className="text-[color:var(--color-warning)] font-semibold text-sm flex items-center gap-2">
🚧 Hamarosan elérhető
</span>
</div>
</div>
</div>
</div>
{/* Következmények (helyes válasz esetén) */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
🎯 Következmények (helyes válasz esetén)
</h3>
<div className="space-y-4">
{/* Consequence Type */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Hatás típusa
</label>
<select
value={card.consequence?.type ?? 0}
onChange={(e) => updateConsequence('type', parseInt(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"
>
{consequenceTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
<div className="text-xs text-[color:var(--color-text-muted)] mt-1">
{consequenceTypes.find(t => t.value === (card.consequence?.type ?? 0))?.description}
</div>
</div>
{/* Consequence Value - csak ha előre/hátra lépés */}
{(card.consequence?.type === 0 || card.consequence?.type === 1) && (
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Mezők száma
</label>
<input
type="number"
value={card.consequence?.value ?? 1}
onChange={(e) => updateConsequence('value', parseInt(e.target.value) || 1)}
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"
min="1"
max="10"
/>
</div>
)}
</div>
</div>
{/* Következmények (rossz válasz esetén) */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
Következmények (rossz válasz esetén)
</h3>
<div className="space-y-4">
{/* Wrong Consequence Type */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Hatás típusa
</label>
<select
value={card.wrongConsequence?.type ?? 1}
onChange={(e) => updateWrongConsequence('type', parseInt(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-danger)] focus:border-transparent outline-none transition-all duration-200"
>
{consequenceTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
<div className="text-xs text-[color:var(--color-text-muted)] mt-1">
{consequenceTypes.find(t => t.value === (card.wrongConsequence?.type ?? 1))?.description}
</div>
</div>
{/* Wrong Consequence Value - csak ha előre/hátra lépés */}
{(card.wrongConsequence?.type === 0 || card.wrongConsequence?.type === 1) && (
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Mezők száma
</label>
<input
type="number"
value={card.wrongConsequence?.value ?? 1}
onChange={(e) => updateWrongConsequence('value', parseInt(e.target.value) || 1)}
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-danger)] focus:border-transparent outline-none transition-all duration-200"
min="1"
max="10"
/>
</div>
)}
</div>
</div>
</div> </div>
) )
} }
@@ -34,13 +34,13 @@ const Footer = () => {
return ( return (
<footer <footer
ref={footerRef} ref={footerRef}
className="relative bg-zinc-900 text-white border-t-2 border-zinc-800 mt-auto py-8" className="relative bg-zinc-900 text-zinc-400 border-t-2 border-zinc-800 mt-auto py-8"
style={{ transformOrigin: "bottom center" }} style={{ transformOrigin: "bottom center" }}
> >
<div className="max-w-6xl mx-auto flex flex-wrap justify-between items-start gap-8 px-4"> <div className="max-w-6xl mx-auto flex flex-wrap justify-between items-start gap-8 px-4">
{/* Logó */} {/* Logó */}
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<a href="/" className="hover:scale-105 hover:brightness-125"> <a href="/" className="hover:scale-105 hover:brightness-110">
<Logo size={100} /> <Logo size={100} />
</a> </a>
<span className="font-extrabold text-xl mt-2 tracking-wide">SerpentRace</span> <span className="font-extrabold text-xl mt-2 tracking-wide">SerpentRace</span>
@@ -48,30 +48,30 @@ const Footer = () => {
{/* Oldalak */} {/* Oldalak */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm"> <span className="text-lg font-semibold text-green-600 underline underline-offset-4 mb-2 drop-shadow-sm">
Oldalak Oldalak
</span> </span>
<a href="/" className="hover:underline hover:text-green-400"> <a href="/" className="hover:underline hover:text-green-500">
Főoldal Főoldal
</a> </a>
<a href="/about" className="hover:underline hover:text-green-400"> <a href="/about" className="hover:underline hover:text-green-500">
Rólunk Rólunk
</a> </a>
<a href="/contact" className="hover:underline hover:text-green-400"> <a href="/contacts" className="hover:underline hover:text-green-500">
Kapcsolat Kapcsolat
</a> </a>
</div> </div>
{/* Közösség */} {/* Közösség */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm"> <span className="text-lg font-semibold text-green-600 underline underline-offset-4 mb-2 drop-shadow-sm">
Közösség Közösség
</span> </span>
<a <a
href="https://discord.gg/" href="https://discord.gg/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="hover:underline hover:text-green-400" className="hover:underline hover:text-green-500"
> >
Discord Discord
</a> </a>
@@ -79,7 +79,7 @@ const Footer = () => {
href="https://github.com/" href="https://github.com/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="hover:underline hover:text-green-400" className="hover:underline hover:text-green-500"
> >
GitHub GitHub
</a> </a>
@@ -87,11 +87,11 @@ const Footer = () => {
{/* Elérhetőség */} {/* Elérhetőség */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm"> <span className="text-lg font-semibold text-green-600 underline underline-offset-4 mb-2 drop-shadow-sm">
Elérhetőség Elérhetőség
</span> </span>
<span className="opacity-80">Email: info@serpentrace.hu</span> <span className="opacity-85">Email: info@serpentrace.hu</span>
<span className="opacity-80">Telefon: +36 30 123 4567</span> <span className="opacity-85">Telefon: +36 30 123 4567</span>
</div> </div>
</div> </div>
@@ -7,7 +7,7 @@ export default function InputBox({ type, placeholder, value, onChange, width })
return ( return (
<input <input
type={type} type={type}
className={`${widthClass} py-3 px-4 border border-battleship-gray rounded-lg focus:border-mint focus:outline-none text-text placeholder-text-muted bg-background font-semibold text-lg`} className={`${widthClass} py-3 px-4 border border-battleship-gray rounded-lg focus:border-mint-lighter focus:outline-none text-text placeholder-text-muted bg-background font-semibold text-lg`}
placeholder={placeholder} placeholder={placeholder}
value={value} value={value}
onChange={onChange} onChange={onChange}
@@ -5,8 +5,13 @@ import logoImg from "../../assets/pictures/Logo.png"
import ButtonGreen from "../Buttons/ButtonGreen.jsx" import ButtonGreen from "../Buttons/ButtonGreen.jsx"
import { FaUsers, FaPaintBrush, FaHeadset } from "react-icons/fa" import { FaUsers, FaPaintBrush, FaHeadset } from "react-icons/fa"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import { isAuthenticated } from "../../hooks/useRequireAuth" // <-- added import
import { useNavigate } from "react-router-dom" // <-- NEW
const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onNavigateToContacts }) => {
const auth = isAuthenticated() // <-- check without redirect
const navigate = useNavigate() // <-- NEW
const LandingPage = ({ onNavigateToPlay, onNavigateToAuth }) => {
return ( return (
<div className="w-full"> <div className="w-full">
{/* Hero Section */} {/* Hero Section */}
@@ -55,8 +60,16 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth }) => {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 1 }} transition={{ duration: 0.7, delay: 1 }}
> >
<ButtonGreen text="Bejelntekezés" onClick={onNavigateToPlay} width="w-60" /> {/* If not authenticated show Login/Register; if authenticated show Home button */}
{!auth ? (
<>
<ButtonGreen text="Bejelentkezés" onClick={onNavigateToPlay} width="w-60" />
<ButtonGreen text="Regisztráció" onClick={onNavigateToAuth} width="w-60" /> <ButtonGreen text="Regisztráció" onClick={onNavigateToAuth} width="w-60" />
<ButtonGreen text="Játék" onClick={onNavigateToGame} width="w-60" />
</>
) : (
<ButtonGreen text="Játék" onClick={onNavigateToGame} width="w-60" />
)}
</motion.div> </motion.div>
</div> </div>
</motion.section> </motion.section>
@@ -165,7 +178,7 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth }) => {
<ButtonGreen <ButtonGreen
text="Kapcsolatfelvétel" text="Kapcsolatfelvétel"
onClick={onNavigateToAuth} onClick={onNavigateToContacts}
className="px-12 py-4 text-xl font-bold" className="px-12 py-4 text-xl font-bold"
/> />
</motion.div> </motion.div>
@@ -4,9 +4,14 @@ import logoImg from "../../assets/pictures/Logo.png" // <-- EZT ADD HOZZÁ
import ButtonDark from "../Buttons/ButtonDark.jsx" import ButtonDark from "../Buttons/ButtonDark.jsx"
import InputBoxDark from "../Inputs/InputBoxDark.jsx" import InputBoxDark from "../Inputs/InputBoxDark.jsx"
const PlayMenu = ({ onJoinGame, onCreateGame, user }) => { const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
const [joinCode, setJoinCode] = useState("") const [joinCode, setJoinCode] = useState("")
const [error, setError] = useState("") const [error, setError] = useState("")
const [guestName, setGuestName] = useState("")
const [guestError, setGuestError] = useState("")
// gyors username kiolvasás (ha a parent objektum user={ { name: ... } } küldi)
const username = user?.name ?? null
const handleJoin = () => { const handleJoin = () => {
if (!joinCode.trim()) { if (!joinCode.trim()) {
@@ -21,9 +26,19 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user }) => {
onCreateGame() onCreateGame()
} }
// egyszerű segéd a kezdobetűk kinyerésére
const initials = username
? username
.split(" ")
.map((s) => s[0])
.join("")
.slice(0, 2)
.toUpperCase()
: ""
return ( return (
<section <section
className="w-[95%] max-w-6xl mx-auto my-16 flex flex-col md:flex-row items-center justify-center rounded-3xl shadow-2xl min-h-[60vh] overflow-hidden" className="w-[95%] max-w-6xl mx-auto my-16 flex flex-col md:flex-row items-center justify-center rounded-3xl shadow-2xl overflow-hidden"
style={{ style={{
background: "linear-gradient(90deg, var(--color-surface) 30%, var(--color-mint) 100%)", background: "linear-gradient(90deg, var(--color-surface) 30%, var(--color-mint) 100%)",
}} }}
@@ -32,10 +47,10 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user }) => {
<div className="flex-1 flex items-center justify-center w-full h-full py-10 md:py-0 md:pl-10"> <div className="flex-1 flex items-center justify-center w-full h-full py-10 md:py-0 md:pl-10">
<LogoCard <LogoCard
imageSrc={logoImg} imageSrc={logoImg}
containerHeight="450px" containerHeight="420px"
containerWidth="450px" containerWidth="420px"
imageHeight="450px" imageHeight="420px"
imageWidth="450px" imageWidth="420px"
rotateAmplitude={7} rotateAmplitude={7}
scaleOnHover={1.03} scaleOnHover={1.03}
showMobileWarning={false} showMobileWarning={false}
@@ -43,12 +58,49 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user }) => {
displayOverlayContent={false} displayOverlayContent={false}
/> />
</div> </div>
{/* Jobb oldali panel */} {/* Jobb oldali panel */}
<div className="flex-1 w-full flex flex-col items-center justify-center px-4 md:px-12 py-10"> <div className="flex-1 w-full flex items-center justify-center px-6 md:px-12 py-8">
<div className="w-full max-w-md rounded-2xl p-8 flex flex-col gap-8"> <div
className="w-full max-w-md rounded-2xl p-6 md:p-8 flex flex-col gap-6"
style={{ background: "rgba(0,0,0,0.15)", backdropFilter: "blur(6px)" }}
>
<div className="flex items-center justify-between">
{username ? (
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold"
style={{ background: "rgba(34,197,94,0.12)", color: "var(--color-mint)" }}
>
{initials}
</div>
<div className="text-[32px]" style={{ color: "var(--color-muted, #cbd5e1)" }}>
<span className="font-medium" style={{ color: "var(--color-text, #fff)" }}>
{username}
</span>
</div>
</div>
) : (
<div className="w-full">
<div className="font-semibold mb-3 text-text">Nincs bejelentkezve játssz vendégként:</div>
<InputBoxDark
type="text"
placeholder="Nickname..."
value={guestName}
onChange={(e) => {
setGuestName(e.target.value)
setGuestError("")
}}
width="w-full"
/>
{guestError && <div className="text-xs mt-2 text-error">{guestError}</div>}
</div>
)}
</div>
<div> <div>
<h2 className="text-lg font-semibold mb-2 text-text">Csatlakozás játékhoz</h2> <h2 className="font-semibold mb-3 text-text">Csatlakozás játékhoz</h2>
<div className={`${error ? "border border-error rounded-lg" : ""}`}> <div className={`${error ? "border border-error rounded-lg p-2" : ""}`}>
<InputBoxDark <InputBoxDark
type="text" type="text"
placeholder="Játék kódja" placeholder="Játék kódja"
@@ -57,18 +109,22 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user }) => {
width="w-full" width="w-full"
/> />
</div> </div>
{error && <div className="text-xs mt-1 text-error">{error}</div>} {error && <div className="text-xs mt-2 text-error">{error}</div>}
<div className="mt-4"> <div className="mt-4">
<ButtonDark text="Csatlakozás" type="button" onClick={handleJoin} width="w-full" /> <ButtonDark text="Csatlakozás" type="button" onClick={handleJoin} width="w-full" />
</div> </div>
</div> </div>
{user && ( {username ? (
<div className="border-t border-white/10 pt-4">
{username && (
<div> <div>
<h2 className="text-lg font-semibold mb-2 text-text">Új játék létrehozása</h2> <h3 className="font-semibold mb-3 text-text">Új játék létrehozása</h3>
<ButtonDark text="Játék létrehozása" type="button" onClick={handleCreate} width="w-full" /> <ButtonDark text="Játék létrehozása" type="button" onClick={handleCreate} width="w-full" />
</div> </div>
)} )}
</div> </div>
) : null}
</div>
</div> </div>
</section> </section>
) )
@@ -1,42 +1,99 @@
import React, { useState } from "react" import React, { useState } from "react"
import Logo from "../../assets/pictures/Logo" import Logo from "../../assets/pictures/Logo"
import About from "../../pages/About/About" import { Link, useNavigate } from "react-router-dom"
import Home from "../../pages/Landing/Home"
const navLinkClass = "px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10" const navLinkClass = "px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10"
const navLinkClassPlay =
"px-4 py-2 rounded-lg text-white bg-white/12 hover:bg-white/20 transition-all duration-200"
const Navbar = () => { const Navbar = () => {
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
const navigate = useNavigate()
const isLoggedIn = Boolean(localStorage.getItem("authLevel") && localStorage.getItem("username"))
const handleLogout = () => {
localStorage.removeItem("authLevel")
localStorage.removeItem("username")
navigate("/")
}
return ( return (
<nav className="bg-gradient-to-r from-green-700 to-emerald-500 shadow-lg"> <nav className="bg-gradient-to-r from-green-700 to-emerald-500 shadow-lg">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-0">
<div className="flex justify-between h-16 items-center"> <div className="flex justify-between h-16 items-center">
{/* Logo */} {/* Logo + Brand */}
<div className="flex-shrink-0 flex items-center gap-2"> <div className="flex-shrink-0 flex items-center gap-2">
<a href="/" className="flex items-center mt-1 h-9"> <Link to="/" className="flex items-center mt-1 h-9">
<Logo size={36} /> <Logo size={36} />
</a> </Link>
<a href="/" className="flex items-center h-9 text-white font-bold text-2xl tracking-tight"> <Link to="/" className="flex items-center h-9 text-white font-bold text-2xl tracking-tight">
SerpentRace SerpentRace
</a> </Link>
</div> </div>
{/* Desktop Menu */} {/* Desktop Menu */}
<div className="hidden md:flex space-x-8"> <div className="hidden md:flex items-center space-x-6">
<a href="/home" className={navLinkClass}> {/* Bal oldali linkek */}
Home <Link to="/" className={navLinkClass}>Főoldal</Link>
</a> <Link to="/about" className={navLinkClass}>Rólunk</Link>
<a href="/report" className={navLinkClass}> <Link to="/contacts" className={navLinkClass}>Kapcsolat</Link>
Stats
</a> {/* Csak bejelentkezve */}
<a href="/about" className={navLinkClass}> {isLoggedIn && (
About <>
</a> <Link to="/decks" className={navLinkClass}>Paklik</Link>
<a href="/companies" className={navLinkClass}> <Link to="/report" className={navLinkClass}>Statisztikák</Link>
Contact <Link to="/profile" className={navLinkClass}>Profil</Link>
</a> </>
)}
{/* Játék gomb */}
<Link to="/home" className={navLinkClassPlay}>Játék</Link>
{/* Jobb oldali akciók */}
{!isLoggedIn ? (
<div className="flex items-center space-x-4">
<Link
to="/login"
className="px-4 py-2 rounded-lg hover:bg-white/20 text-white font-semibold transition-all"
>
Bejelentkezés
</Link>
{/* Elválasztó vonal */}
<div className="w-px h-10 bg-white/100"></div>
<Link
to="/register"
className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white font-semibold transition-all"
>
Regisztráció
</Link>
</div> </div>
) : (
<button
onClick={handleLogout}
className="ml-2 p-2 rounded-full bg-[#166534] hover:bg-[#1f7a45] text-white shadow-lg hover:shadow-green-400/40 transition-all transform hover:scale-105 cursor-pointer"
title="Kijelentkezés"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 16l4-4m0 0l-4-4m4 4H3m13 4v1a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2v1"
/>
</svg>
</button>
)}
</div>
{/* Mobile Hamburger */} {/* Mobile Hamburger */}
<div className="md:hidden flex items-center"> <div className="md:hidden flex items-center">
<button <button
@@ -52,12 +109,7 @@ const Navbar = () => {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
{menuOpen ? ( {menuOpen ? (
<path <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
) : ( ) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8h16M4 16h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8h16M4 16h16" />
)} )}
@@ -66,21 +118,70 @@ const Navbar = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Mobile Menu */} {/* Mobile Menu */}
{menuOpen && ( {menuOpen && (
<div className="md:hidden bg-emerald-600 px-2 pt-2 pb-3 space-y-1"> <div className="md:hidden bg-emerald-600 px-2 pt-2 pb-3 space-y-1">
<a href="#" className={navLinkClass}> <Link to="/" onClick={() => setMenuOpen(false)} className={navLinkClass}>Főoldal</Link>
Home <Link to="/about" onClick={() => setMenuOpen(false)} className={navLinkClass}>Rólunk</Link>
</a> <Link to="/contacts" onClick={() => setMenuOpen(false)} className={navLinkClass}>Kapcsolat</Link>
<a href="#" className={navLinkClass}> {isLoggedIn && (
Leaderboard <>
</a> <Link to="/decks" onClick={() => setMenuOpen(false)} className={navLinkClass}>Paklik</Link>
<a href="#" className={navLinkClass}> <Link to="/report" onClick={() => setMenuOpen(false)} className={navLinkClass}>Statisztikák</Link>
About <Link to="/profile" onClick={() => setMenuOpen(false)} className={navLinkClass}>Profil</Link>
</a> </>
<a href="#" className={navLinkClass}> )}
Contact <Link to="/home" onClick={() => setMenuOpen(false)} className={navLinkClassPlay}>Játék</Link>
</a>
{!isLoggedIn ? (
<div className="flex flex-col space-y-2">
<Link
to="/login"
onClick={() => setMenuOpen(false)}
className="block px-3 py-2 rounded-lg hover:bg-white/20 text-white font-semibold transition-all"
>
Bejelentkezés
</Link>
{/* Elválasztó vonal mobilon */}
<div className="w-full h-px bg-white/30"></div>
<Link
to="/register"
onClick={() => setMenuOpen(false)}
className="block px-3 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white font-semibold transition-all"
>
Regisztráció
</Link>
</div>
) : (
<div className="flex justify-end px-2 pb-2">
<button
onClick={() => {
handleLogout()
setMenuOpen(false)
}}
className="p-2 rounded-full bg-[#166534] hover:bg-[#1f7a45] text-white shadow-lg hover:shadow-green-400/40 transition-all transform hover:scale-105 cursor-pointer"
title="Kijelentkezés"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 16l4-4m0 0l-4-4m4 4H3m13 4v1a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2v1"
/>
</svg>
</button>
</div>
)}
</div> </div>
)} )}
</nav> </nav>
@@ -1,4 +1,5 @@
import React, { useEffect } from "react" import React, { useEffect } from "react"
import { useNavigate } from "react-router-dom"
import { import {
FaUser, FaUser,
FaLock, FaLock,
@@ -12,11 +13,13 @@ import {
} from "react-icons/fa" } from "react-icons/fa"
export default function DeckInfoPopUp({ deck, onClose }) { export default function DeckInfoPopUp({ deck, onClose }) {
const navigate = useNavigate()
if (!deck) return null if (!deck) return null
// Debug: Log the deck structure to see what we're working with // Debug: Log the deck structure to see what we're working with
console.log('Deck in popup:', deck) console.log('Deck in popup:', deck)
console.log('Cards:', deck.cards) console.log('Raw deck data:', deck.raw)
// Scroll blokkolás amikor a popup nyitva van // Scroll blokkolás amikor a popup nyitva van
useEffect(() => { useEffect(() => {
@@ -29,50 +32,98 @@ export default function DeckInfoPopUp({ deck, onClose }) {
} }
}, []) }, [])
const deckTypes = { // Backend enum mapping
"Luck": { label: "Szerencse", color: "var(--color-luck)" }, const deckTypeMapping = {
"Question": { label: "Kérdés", color: "var(--color-question)" }, 0: { label: "Szerencse", color: "var(--color-luck)" }, // LUCK
"Fun": { label: "Szórakozás", color: "var(--color-fun)" } 1: { label: "Joker", color: "var(--color-fun)" }, // JOKER
2: { label: "Kérdés", color: "var(--color-question)" } // QUESTION
} }
const currentDeckType = deckTypes[deck.type] || { label: deck.type, color: "var(--color-success)" } const ctypeMapping = {
0: "Publikus", // PUBLIC
1: "Privát", // PRIVATE
2: "Vállalati" // ORGANIZATION
}
// Use real deck data with safe fallbacks const stateMapping = {
const creator = deck.creatorName || deck.creator || (deck.user && deck.user.name) || "Ismeretlen" 0: "Aktív", // ACTIVE
const privacy = deck.origin === "Vállalati" || deck.ctype === 1 || deck.privacy === 'public' ? "Publikus" : "Privát" 1: "Törölt" // SOFT_DELETE
}
// Get data from raw if available // Get data from raw (backend data)
const rawData = deck.raw || deck const rawData = deck.raw || deck
// Use played number from raw data for answers count // Type info
const questionsCount = rawData.cardCount || 0 const deckTypeInfo = deckTypeMapping[rawData.type] || { label: "Ismeretlen", color: "var(--color-success)" }
const answersCount = rawData.playedNumber || 0
console.log('Calculated counts:', { questionsCount, answersCount, rawData }) // Privacy/CType
const privacy = ctypeMapping[rawData.ctype] || "Ismeretlen"
// State
const state = stateMapping[rawData.state] || "Ismeretlen"
// Creator
const creator = rawData.user?.name || rawData.creatorName || rawData.creator || "Ismeretlen"
// Card count
const cardCount = rawData.cardCount || 0
// Played count
const playedNumber = rawData.playedNumber || 0
console.log('Mapped data:', {
type: rawData.type,
deckTypeInfo,
ctype: rawData.ctype,
privacy,
state,
cardCount,
playedNumber
})
const mockData = { const mockData = {
...deck, name: rawData.name || deck.name || "Névtelen pakli",
creator, creator,
privacy, privacy,
questionsCount, state,
answersCount questionsCount: cardCount,
answersCount: playedNumber,
created: rawData.creationdate || rawData.created || deck.created || new Date().toISOString(),
description: rawData.description || ""
} }
const formatDate = (dateString) => { const formatDate = (dateString) => {
try {
const date = new Date(dateString) const date = new Date(dateString)
return date.toLocaleDateString('hu-HU', { return date.toLocaleDateString('hu-HU', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric' day: 'numeric'
}) })
} catch (e) {
return dateString
}
} }
const handleOpenDeck = () => { const handleOpenDeck = () => {
// TODO: Megnyitás funkció - később implementálható
alert("⚠️ A pakli megnyitás funkció még fejlesztés alatt áll!") alert("⚠️ A pakli megnyitás funkció még fejlesztés alatt áll!")
} }
const handleEditDeck = () => { const handleEditDeck = () => {
alert("⚠️ A pakli szerkeszté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 deck creator with the deck ID
navigate(`/deck-creator/${deckId}`)
// Close the popup
onClose()
} }
return ( return (
@@ -86,7 +137,7 @@ export default function DeckInfoPopUp({ deck, onClose }) {
{/* Header with deck type color */} {/* Header with deck type color */}
<div <div
className="h-2 w-full" className="h-2 w-full"
style={{ backgroundColor: currentDeckType.color }} style={{ backgroundColor: deckTypeInfo.color }}
></div> ></div>
{/* Close button */} {/* Close button */}
@@ -108,13 +159,18 @@ export default function DeckInfoPopUp({ deck, onClose }) {
<span <span
className="inline-block px-3 py-1 rounded-full text-xs font-bold" className="inline-block px-3 py-1 rounded-full text-xs font-bold"
style={{ style={{
background: currentDeckType.color, background: deckTypeInfo.color,
color: "var(--color-text-inverse)", color: "var(--color-text-inverse)",
}} }}
> >
{currentDeckType.label} {deckTypeInfo.label}
</span> </span>
</div> </div>
{mockData.description && (
<p className="text-[color:var(--color-text-muted)] text-sm">
{mockData.description}
</p>
)}
</div> </div>
{/* Data grid */} {/* Data grid */}
@@ -149,13 +205,13 @@ export default function DeckInfoPopUp({ deck, onClose }) {
<div className="flex items-center gap-3 p-3 bg-[color:var(--color-background)]/50 rounded-xl"> <div className="flex items-center gap-3 p-3 bg-[color:var(--color-background)]/50 rounded-xl">
<div <div
className="flex items-center justify-center w-8 h-8 rounded-full" className="flex items-center justify-center w-8 h-8 rounded-full"
style={{ backgroundColor: `${currentDeckType.color}20` }} style={{ backgroundColor: `${deckTypeInfo.color}20` }}
> >
<FaTags style={{ color: currentDeckType.color }} className="text-sm" /> <FaTags style={{ color: deckTypeInfo.color }} className="text-sm" />
</div> </div>
<div> <div>
<div className="text-[color:var(--color-text-muted)] text-xs font-medium">Típus</div> <div className="text-[color:var(--color-text-muted)] text-xs font-medium">Típus</div>
<div className="text-[color:var(--color-text)] font-semibold text-sm">{currentDeckType.label}</div> <div className="text-[color:var(--color-text)] font-semibold text-sm">{deckTypeInfo.label}</div>
</div> </div>
</div> </div>
@@ -172,25 +228,25 @@ export default function DeckInfoPopUp({ deck, onClose }) {
{/* Questions and Answers in one row */} {/* Questions and Answers in one row */}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{/* Questions */} {/* Card Count (Kártyák) */}
<div className="flex items-center gap-2 p-3 bg-[color:var(--color-background)]/50 rounded-xl"> <div className="flex items-center gap-2 p-3 bg-[color:var(--color-background)]/50 rounded-xl">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-[color:var(--color-question)]/20"> <div className="flex items-center justify-center w-8 h-8 rounded-full bg-[color:var(--color-question)]/20">
<FaQuestionCircle className="text-[color:var(--color-question)] text-sm" /> <FaQuestionCircle className="text-[color:var(--color-question)] text-sm" />
</div> </div>
<div> <div>
<div className="text-[color:var(--color-text-muted)] text-xs font-medium">Kérdések</div> <div className="text-[color:var(--color-text-muted)] text-xs font-medium">Kártyák</div>
<div className="text-[color:var(--color-text)] font-semibold">{mockData.questionsCount}</div> <div className="text-[color:var(--color-text)] font-semibold">{mockData.questionsCount}</div>
</div> </div>
</div> </div>
{/* Answers */} {/* Played Count (Játszva) */}
<div className="flex items-center gap-2 p-3 bg-[color:var(--color-background)]/50 rounded-xl"> <div className="flex items-center gap-2 p-3 bg-[color:var(--color-background)]/50 rounded-xl">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-[color:var(--color-fun)]/20"> <div className="flex items-center justify-center w-8 h-8 rounded-full bg-[color:var(--color-fun)]/20">
<FaComment className="text-[color:var(--color-fun)] text-sm" /> <FaComment className="text-[color:var(--color-fun)] text-sm" />
</div> </div>
<div> <div>
<div className="text-[color:var(--color-text-muted)] text-xs font-medium">Válaszok</div> <div className="text-[color:var(--color-text-muted)] text-xs font-medium">Játszva</div>
<div className="text-[color:var(--color-text)] font-semibold">{mockData.answersCount}</div> <div className="text-[color:var(--color-text)] font-semibold">{mockData.answersCount}×</div>
</div> </div>
</div> </div>
</div> </div>
@@ -0,0 +1,165 @@
import { toast, ToastContainer, Bounce } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
/**
* 🔧 Ezt csak egyszer kell betenni az App.jsx-be!
* <ToastConfig /> = az a komponens, ami kirendereli a toastokat
*/
export const ToastConfig = () => (
<ToastContainer
position="bottom-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick={false}
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="light"
transition={Bounce}
/>
);
/**
* 🦄 Alapértelmezett toast üzenet (semleges)
* notify("Üzenet szövege")
*/
export const notify = (message) => {
toast(message, {
position: "bottom-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
theme: "light",
transition: Bounce,
});
};
/**
* Sikeres művelethez
* notifySuccess("Sikeres mentés!")
*/
export const notifySuccess = (message) => {
toast.success(message, {
position: "bottom-right",
autoClose: 4000,
theme: "light",
transition: Bounce,
});
};
/**
* Hibás művelethez
* notifyError("Hiba történt a mentés során!")
*/
export const notifyError = (message) => {
toast.error(message, {
position: "bottom-right",
autoClose: 5000,
theme: "light",
transition: Bounce,
});
};
/**
* Információs üzenethez
* notifyInfo("Friss adatok betöltve!")
*/
export const notifyInfo = (message) => {
toast.info(message, {
position: "bottom-right",
autoClose: 4000,
theme: "light",
transition: Bounce,
});
};
/**
* Figyelmeztetéshez
* notifyWarning("Figyelem! Nem mentett módosítások vannak!")
*/
export const notifyWarning = (message) => {
toast.warn(message, {
position: "bottom-right",
autoClose: 4000,
theme: "light",
transition: Bounce,
});
};
// import React, { useState } from "react";
// import { notifyWarning } from "../../components/Toastify/toastifyServices";
// function AuthLogin() {
// const [email, setEmail] = useState("");
// const [password, setPassword] = useState("");
// const handleLogin = async (e) => {
// e.preventDefault();
// // Példa jelszó ellenőrzés logikára:
// if (password !== "titkosjelszo123") {
// notifyWarning(" Hibás jelszó! Kérlek próbáld újra.");
// return;
// }
// // Ha jó a jelszó:
// notifySuccess(" Sikeres bejelentkezés!");
// };
// return (
// <form onSubmit={handleLogin} className="login-form">
// <input
// type="email"
// placeholder="Email cím"
// value={email}
// onChange={(e) => setEmail(e.target.value)}
// />
// <input
// type="password"
// placeholder="Jelszó"
// value={password}
// onChange={(e) => setPassword(e.target.value)}
// />
// <button type="submit">Bejelentkezés</button>
// </form>
// );
// }
// export default AuthLogin;
// meghivas
// import { toast } from "react-toastify";
// // 🔔 Alap toast
// export const notify = (msg) => toast(msg);
// // Sikeres üzenet
// export const notifySuccess = (msg) => toast.success(msg);
// // Figyelmeztetés
// export const notifyWarning = (msg) => toast.warning(msg);
// // Hiba
// export const notifyError = (msg) => toast.error(msg);
// // Információ
// export const notifyInfo = (msg) => toast.info(msg);
// hasznalat
// import { notifyWarning } from "../../components/Toastify/toastifyServices";
// if (password !== "titkos") {
// notifyWarning(" Hibás jelszó!");
// }
@@ -1,54 +1,210 @@
import React, { useState } from "react" import React, { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom"
import { import {
FaCommentDots, FaCommentDots,
FaUserFriends, FaUserFriends,
FaBriefcase, FaBriefcase,
FaFacebookF, FaMedal,
FaTwitter, FaEdit,
FaDribbble, FaSave,
FaSun, FaTimes,
FaMoon, FaTrash,
FaMedal FaEye,
FaEyeSlash
} from "react-icons/fa" } from "react-icons/fa"
import Navbar from "../Navbar/Navbar"
import Footer from "../Footer/Footer"
import Background from "../../assets/backgrounds/Background"
import { getUserProfile, updateUserProfile, deleteUserProfile } from "../../api/userApi"
import { notifySuccess, notifyError, notifyWarning } from "../Toastify/toastifyServices"
const ProfileCard = () => { const ProfileCard = () => {
const [darkMode, setDarkMode] = useState(false) const navigate = useNavigate()
const activityLevel = 87
const isPremium = true // State
const [user, setUser] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [isEditing, setIsEditing] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
// Edit form state
const [editForm, setEditForm] = useState({
username: "",
email: "",
fname: "",
lname: "",
phone: "",
password: ""
})
// Load user profile on mount
useEffect(() => {
loadUserProfile()
}, [])
const loadUserProfile = async () => {
setIsLoading(true)
try {
const data = await getUserProfile()
setUser(data)
setEditForm({
username: data.username || "",
email: data.email || "",
fname: data.fname || "",
lname: data.lname || "",
phone: data.phone || "",
password: ""
})
} catch (error) {
console.error('Failed to load user profile:', error)
notifyError('Hiba történt a profil betöltése során')
} finally {
setIsLoading(false)
}
}
const handleEditToggle = () => {
if (isEditing) {
setEditForm({
username: user.username || "",
email: user.email || "",
fname: user.fname || "",
lname: user.lname || "",
phone: user.phone || "",
password: ""
})
}
setIsEditing(!isEditing)
}
const handleInputChange = (e) => {
const { name, value } = e.target
setEditForm(prev => ({ ...prev, [name]: value }))
}
const handleSaveProfile = async () => {
try {
const updates = {}
if (editForm.username !== user.username) updates.username = editForm.username
if (editForm.email !== user.email) updates.email = editForm.email
if (editForm.fname !== user.fname) updates.fname = editForm.fname
if (editForm.lname !== user.lname) updates.lname = editForm.lname
if (editForm.phone !== user.phone) updates.phone = editForm.phone
if (editForm.password && editForm.password.trim() !== "") updates.password = editForm.password
if (Object.keys(updates).length === 0) {
notifyWarning('Nincs változtatás')
return
}
const updatedUser = await updateUserProfile(updates)
setUser(updatedUser)
setIsEditing(false)
setEditForm(prev => ({ ...prev, password: "" }))
notifySuccess('Profil sikeresen frissítve!')
} catch (error) {
console.error('Failed to update profile:', error)
const errorMessage = error?.response?.data?.error || error.message || 'Ismeretlen hiba'
notifyError('Hiba történt a mentés során: ' + errorMessage)
}
}
const handleDeleteProfile = () => {
setShowDeleteModal(true)
}
const handleConfirmDelete = async () => {
try {
await deleteUserProfile()
notifySuccess("Profil sikeresen törölve!")
localStorage.removeItem("authLevel")
localStorage.removeItem("username")
navigate("/")
} catch (err) {
console.error("Profil törlési hiba:", err)
notifyError(err.response?.data?.message || "Hiba a profil törlésekor!")
} finally {
setShowDeleteModal(false)
}
}
const handleCancelDelete = () => {
setShowDeleteModal(false)
}
if (isLoading) {
return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
<div className="fixed inset-0 -z-10 pointer-events-none">
<Background />
</div>
<div className="fixed top-0 left-0 right-0 z-30">
<Navbar />
</div>
<main className="flex-1 min-h-[calc(100vh-64px)] flex mt-[64px] items-center justify-center">
<div className="text-center">
<div className="text-6xl mb-4"></div>
<p className="text-xl text-[color:var(--color-text)]">Betöltés...</p>
</div>
</main>
</div>
)
}
if (!user) {
return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
<div className="fixed inset-0 -z-10 pointer-events-none">
<Background />
</div>
<div className="fixed top-0 left-0 right-0 z-30">
<Navbar />
</div>
<main className="flex-1 min-h-[calc(100vh-64px)] flex mt-[64px] items-center justify-center">
<div className="text-center">
<div className="text-6xl mb-4">😢</div>
<p className="text-xl text-[color:var(--color-text)]">Nem sikerült betölteni a profilt</p>
</div>
</main>
</div>
)
}
const isPremium = user.state === 2
const activityLevel = user.activity_level || 65
let activityColor = "" let activityColor = ""
let activityEmoji = "" let activityEmoji = ""
let blocksToColor = 1 let blocksToColor = 1
let celebrationEmoji = ""
if (activityLevel <= 24) { if (activityLevel <= 24) {
activityColor = "red-600" activityColor = "error"
activityEmoji = "😞" activityEmoji = "😞"
blocksToColor = 1 blocksToColor = 1
} else if (activityLevel <= 49) { } else if (activityLevel <= 49) {
activityColor = "orange-500" activityColor = "warning"
activityEmoji = "😐" activityEmoji = "😐"
blocksToColor = 2 blocksToColor = 2
} else if (activityLevel <= 74) { } else if (activityLevel <= 74) {
activityColor = "yellow-400" activityColor = "secondary"
activityEmoji = "🙂" activityEmoji = "🙂"
blocksToColor = 3 blocksToColor = 3
} else { } else {
activityColor = "emerald-500" activityColor = "success"
activityEmoji = "😄" activityEmoji = "😄"
blocksToColor = 4 blocksToColor = 4
celebrationEmoji = "🎉"
} }
const colorMap = { const colorMap = {
"red-600": "#dc2626", success: "#5fa985",
"orange-500": "#f97316", warning: "#e6c04f",
"yellow-400": "#facc15", error: "#e15b64",
"emerald-500": "#10b981" secondary: "#8d8e83"
} }
const getBlockStyle = (index) => ({ const getBlockStyle = (index) => ({
backgroundColor: index < blocksToColor ? colorMap[activityColor] : (darkMode ? "#4b5563" : "#d1d5db") backgroundColor: index < blocksToColor ? colorMap[activityColor] : "#314045"
}) })
const stats = [ const stats = [
@@ -61,101 +217,333 @@ const ProfileCard = () => {
const badges = ["🏆", "🔥", "🎯", "🧠", "💎", "🚀"] const badges = ["🏆", "🔥", "🎯", "🧠", "💎", "🚀"]
return ( return (
<div className={`${darkMode ? "bg-gray-900" : "bg-gray-100"} min-h-screen flex flex-col justify-center items-center px-4 py-12 transition-colors duration-500`}> <div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
<div className={`relative max-w-sm w-full rounded-xl shadow-lg overflow-hidden border <div className="fixed inset-0 -z-10 pointer-events-none">
${darkMode ? "bg-gray-800 border-gray-700" : "bg-white border-gray-200"}`}> <Background />
</div>
<div className="fixed top-0 left-0 right-0 z-30">
<Navbar />
</div>
<main className="flex-1 min-h-[calc(100vh-64px)] flex mt-[64px] flex-col items-center justify-center py-8 px-4">
<div className="relative max-w-5xl w-full animate-fadeInUp">
<button {/* Hero Section - Cover Photo */}
onClick={() => setDarkMode(!darkMode)} <div className="relative rounded-t-2xl overflow-hidden bg-gradient-to-r from-[color:var(--color-mint)] via-[color:var(--color-success)] to-[color:var(--color-mint)] h-32 shadow-lg">
className={`absolute top-4 right-4 p-2 rounded-full focus:outline-none <div className="absolute inset-0 opacity-10">
${darkMode ? "bg-yellow-400 text-gray-900 hover:bg-yellow-300" : "bg-gray-800 text-yellow-400 hover:bg-gray-700"}`} <div className="absolute inset-0" style={{
aria-label="Toggle dark mode" backgroundImage: 'radial-gradient(circle, white 1px, transparent 1px)',
backgroundSize: '20px 20px'
}}></div>
</div>
</div>
{/* Main Profile Card */}
<div className="relative bg-[color:var(--color-card)] rounded-b-2xl shadow-2xl border-2 border-[color:var(--color-surface-selected)] -mt-16">
{/* Avatar & Name Section */}
<div className="flex flex-col sm:flex-row items-center sm:items-end gap-6 px-8 pb-6">
{/* Avatar */}
<div className="relative -mt-12 flex-shrink-0">
<div className="w-32 h-32 rounded-2xl bg-gradient-to-br from-[color:var(--color-mint)] to-[color:var(--color-success)] flex items-center justify-center shadow-2xl ring-4 ring-[color:var(--color-card)] transform hover:scale-105 transition-transform">
<span className="text-white text-4xl font-bold">
{user.lname?.[0]?.toUpperCase() || ''}.{user.fname?.[0]?.toUpperCase() || ''}
</span>
</div>
</div>
{/* Name & Status */}
<div className="flex-1 text-center sm:text-left">
<div className="flex flex-col sm:flex-row sm:items-center gap-3 mb-2">
<h1 className="text-3xl font-bold text-[color:var(--color-text)]">
{user.username}
</h1>
<div
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold shadow-md ${
isPremium
? "bg-gradient-to-r from-[color:var(--color-mint)] to-[color:var(--color-success)] text-white"
: "bg-[color:var(--color-surface)] text-[color:var(--color-text-muted)] border border-[color:var(--color-surface-selected)]"
}`}
> >
{darkMode ? <FaSun size={24} /> : <FaMoon size={24} />} {isPremium ? "👑 Premium" : user.state === 1 ? "✓ Verified" : "Free"}
</div>
</div>
<p className="text-lg text-[color:var(--color-text)] font-medium">
{user.fname} {user.lname}
</p>
</div>
{/* Action Buttons */}
{!isEditing && (
<div className="flex gap-2 flex-shrink-0">
<button
onClick={handleEditToggle}
className="px-4 py-2 rounded-lg bg-[color:var(--color-mint)] text-white font-semibold hover:opacity-90 transition-all flex items-center gap-2 shadow-md"
>
<FaEdit /> Szerkesztés
</button> </button>
<button
onClick={handleDeleteProfile}
className="p-2 rounded-lg bg-[color:var(--color-error)] text-white hover:opacity-90 transition-all shadow-md"
title="Profil törlése"
>
<FaTrash />
</button>
</div>
)}
</div>
<div className="p-6 text-center space-y-6"> {/* Content Grid */}
<img <div className="px-8 pb-8 space-y-6">
src="https://i.pravatar.cc/150?img=12"
alt="Avatar"
className={`w-24 h-24 mx-auto rounded-full border-4 mb-4 ${darkMode ? "border-blue-700" : "border-blue-200"}`}
/>
<div className="space-y-2"> {!isEditing ? (
<h2 className={`text-xl font-semibold ${darkMode ? "text-gray-100" : "text-gray-800"}`}> <>
BÉKAAAAA {/* Contact Info & Activity Row */}
</h2> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Contact Information */}
<div className="lg:col-span-2 space-y-4">
<h3 className="text-lg font-bold text-[color:var(--color-text)] border-b-2 border-[color:var(--color-mint)] pb-2">
📋 Kapcsolati adatok
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="flex items-center gap-3 p-3 rounded-lg bg-[color:var(--color-surface)] border border-[color:var(--color-surface-selected)]">
<span className="text-2xl">📧</span>
<div> <div>
<div className={`inline-block px-3 py-1 rounded-full text-xs font-semibold <p className="text-xs text-[color:var(--color-text-muted)] font-medium">Email</p>
${darkMode <p className="text-sm text-[color:var(--color-text)] font-semibold">{user.email}</p>
? (isPremium ? "bg-green-600 text-white" : "bg-gray-600 text-gray-200")
: (isPremium ? "bg-green-200 text-green-800" : "bg-blue-200 text-blue-800")}`}>
{isPremium ? "Premium Account" : "Free Account"}
</div> </div>
</div> </div>
<p className={`text-sm ${darkMode ? "text-gray-400" : "text-gray-500"}`}>
Active | Male | 23.05.1992
</p>
</div>
<div className="flex justify-center items-center space-x-2"> <div className="flex items-center gap-3 p-3 rounded-lg bg-[color:var(--color-surface)] border border-[color:var(--color-surface-selected)]">
<p className={`text-sm font-medium flex items-center space-x-1`} style={{ color: colorMap[activityColor] }}> <span className="text-2xl">📱</span>
<span>Activity Level: {activityLevel}%</span> <div>
<span className="text-xl">{activityEmoji}</span> <p className="text-xs text-[color:var(--color-text-muted)] font-medium">Telefon</p>
{celebrationEmoji && <span className="text-xl ml-1">{celebrationEmoji}</span>} <p className="text-sm text-[color:var(--color-text)] font-semibold">{user.phone || "Nincs megadva"}</p>
</p> </div>
</div>
</div>
</div> </div>
<div className="flex justify-center space-x-1 mb-4"> {/* Activity Level */}
{[0, 1, 2, 3].map(i => ( <div className="space-y-4">
<h3 className="text-lg font-bold text-[color:var(--color-text)] border-b-2 border-[color:var(--color-mint)] pb-2">
Aktivitás
</h3>
<div className="p-4 rounded-lg bg-gradient-to-br from-[color:var(--color-surface)] to-[color:var(--color-surface-selected)] border border-[color:var(--color-surface-selected)]">
<div className="flex justify-between items-center mb-3">
<span className="text-sm font-medium text-[color:var(--color-text)]">Szint</span>
<span
className="text-sm font-bold px-3 py-1 rounded-full text-white"
style={{ backgroundColor: colorMap[activityColor] }}
>
{activityLevel}% {activityEmoji}
</span>
</div>
<div className="flex gap-1.5">
{[0, 1, 2, 3].map((i) => (
<div <div
key={i} key={i}
className="w-8 h-1 rounded-full" className="flex-1 h-2.5 rounded-full transition-all duration-500"
style={getBlockStyle(i)} style={getBlockStyle(i)}
/> />
))} ))}
</div> </div>
</div>
{/* Badge szekció */}
<div className={`rounded-lg p-4 ${darkMode ? "bg-blue-900" : "bg-blue-200"}`}>
<h3 className={`text-sm font-bold mb-2 ${darkMode ? "text-white" : "text-blue-800"}`}>Badge-ek</h3>
<div className="flex justify-center flex-wrap gap-2">
{badges.map((badge, i) => (
<span key={i} className="text-xl">
{badge}
</span>
))}
</div> </div>
</div> </div>
{/* Statisztikák */} {/* Stats Grid */}
<div className={`flex flex-wrap justify-around items-center rounded-lg py-6 mt-4 text-white ${darkMode ? "bg-blue-700" : "bg-blue-500"}`}> <div>
<h3 className="text-lg font-bold text-[color:var(--color-text)] border-b-2 border-[color:var(--color-mint)] pb-2 mb-4">
📊 Statisztikák
</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{stats.map((s, i) => ( {stats.map((s, i) => (
<div key={i} className="text-center px-2 w-1/2 sm:w-1/4 mb-4"> <div
<div className="mb-1 flex justify-center">{s.icon}</div> key={i}
<p className="text-sm font-semibold">{s.value}</p> className="relative group overflow-hidden p-4 rounded-xl bg-gradient-to-br from-[color:var(--color-mint)] to-[color:var(--color-success)] text-white shadow-lg hover:shadow-xl transition-all"
<p className="text-xs">{s.label}</p> >
<div className="relative z-10">
<div className="text-3xl mb-2 opacity-80">{s.icon}</div>
<p className="text-2xl font-bold mb-1">{s.value}</p>
<p className="text-xs opacity-90">{s.label}</p>
</div>
<div className="absolute inset-0 bg-white opacity-0 group-hover:opacity-10 transition-opacity"></div>
</div> </div>
))} ))}
</div> </div>
</div>
<p className={`text-xs mb-4 ${darkMode ? "text-gray-400" : "text-gray-500"}`}> {/* Badges */}
Gyere és játsz velünk! <div>
<h3 className="text-lg font-bold text-[color:var(--color-text)] border-b-2 border-[color:var(--color-mint)] pb-2 mb-4">
🏆 Badge-ek
</h3>
<div className="flex flex-wrap gap-3 p-4 rounded-lg bg-[color:var(--color-surface)] border border-[color:var(--color-surface-selected)]">
{badges.map((badge, i) => (
<div
key={i}
className="w-14 h-14 flex items-center justify-center bg-gradient-to-br from-[color:var(--color-mint)]/20 to-[color:var(--color-success)]/20 rounded-lg hover:scale-110 transition-transform cursor-pointer border border-[color:var(--color-mint)]/30"
>
<span className="text-2xl">{badge}</span>
</div>
))}
</div>
</div>
</>
) : (
<>
{/* Edit Mode */}
<div className="max-w-2xl mx-auto space-y-6">
<h3 className="text-xl font-bold text-[color:var(--color-text)] text-center mb-6">
Profil szerkesztése
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="sm:col-span-2">
<label className="block text-sm font-bold text-[color:var(--color-text)] mb-2">
Felhasználónév
</label>
<input
type="text"
name="username"
value={editForm.username}
onChange={handleInputChange}
className="w-full px-4 py-2.5 rounded-lg bg-[color:var(--color-surface)] border-2 border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-mint)] focus:border-[color:var(--color-mint)] outline-none transition-all"
/>
</div>
<div>
<label className="block text-sm font-bold text-[color:var(--color-text)] mb-2">
Keresztnév
</label>
<input
type="text"
name="fname"
value={editForm.fname}
onChange={handleInputChange}
className="w-full px-4 py-2.5 rounded-lg bg-[color:var(--color-surface)] border-2 border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-mint)] focus:border-[color:var(--color-mint)] outline-none transition-all"
/>
</div>
<div>
<label className="block text-sm font-bold text-[color:var(--color-text)] mb-2">
Vezetéknév
</label>
<input
type="text"
name="lname"
value={editForm.lname}
onChange={handleInputChange}
className="w-full px-4 py-2.5 rounded-lg bg-[color:var(--color-surface)] border-2 border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-mint)] focus:border-[color:var(--color-mint)] outline-none transition-all"
/>
</div>
<div className="sm:col-span-2">
<label className="block text-sm font-bold text-[color:var(--color-text)] mb-2">
Email
</label>
<input
type="email"
name="email"
value={editForm.email}
disabled
className="w-full px-4 py-2.5 rounded-lg bg-[color:var(--color-surface)] border-2 border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] outline-none opacity-50 cursor-not-allowed"
/>
</div>
<div className="sm:col-span-2">
<label className="block text-sm font-bold text-[color:var(--color-text)] mb-2">
Telefonszám
</label>
<input
type="tel"
name="phone"
value={editForm.phone}
onChange={handleInputChange}
className="w-full px-4 py-2.5 rounded-lg bg-[color:var(--color-surface)] border-2 border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-mint)] focus:border-[color:var(--color-mint)] outline-none transition-all"
/>
</div>
<div className="sm:col-span-2">
<label className="block text-sm font-bold text-[color:var(--color-text)] mb-2">
Új jelszó (opcionális)
</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
name="password"
value={editForm.password}
onChange={handleInputChange}
placeholder="Hagyd üresen ha nem változtatod"
className="w-full px-4 py-2.5 rounded-lg bg-[color:var(--color-surface)] border-2 border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-mint)] focus:border-[color:var(--color-mint)] outline-none pr-12 transition-all"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-[color:var(--color-text-muted)] hover:text-[color:var(--color-mint)] transition-colors"
>
{showPassword ? <FaEyeSlash size={18} /> : <FaEye size={18} />}
</button>
</div>
</div>
</div>
{/* Save/Cancel buttons */}
<div className="flex gap-3 pt-4">
<button
onClick={handleSaveProfile}
className="flex-1 px-6 py-3 rounded-lg bg-gradient-to-r from-[color:var(--color-success)] to-[color:var(--color-mint)] text-white font-bold hover:shadow-lg transition-all flex items-center justify-center gap-2"
>
<FaSave /> Mentés
</button>
<button
onClick={handleEditToggle}
className="flex-1 px-6 py-3 rounded-lg bg-[color:var(--color-surface-selected)] text-[color:var(--color-text)] font-bold hover:bg-[color:var(--color-surface)] transition-all flex items-center justify-center gap-2"
>
<FaTimes /> Mégse
</button>
</div>
</div>
</>
)}
</div>
</div>
</div>
</main>
{/* Delete Confirmation Modal */}
{showDeleteModal && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl p-6 w-80 text-center animate-fadeIn">
<h3 className="text-lg font-semibold mb-4 text-gray-800">
Biztosan törölni szeretnéd a profilodat?
</h3>
<p className="text-sm text-gray-600 mb-6">
Ez a művelet nem visszavonható!
</p> </p>
<div className="flex justify-center gap-4">
<button
onClick={handleConfirmDelete}
className="bg-[color:var(--color-error)] text-white px-4 py-2 rounded-lg hover:opacity-80 transition"
>
Igen, törlöm
</button>
<button
onClick={handleCancelDelete}
className="bg-gray-200 px-4 py-2 rounded-lg hover:bg-gray-300 transition"
>
Mégse
</button>
</div>
</div>
</div>
)}
<div className={`flex justify-center gap-6 text-2xl ${darkMode ? "text-blue-400" : "text-blue-600"}`}> <Footer />
<FaFacebookF className="cursor-pointer hover:text-blue-600" />
<FaTwitter className="cursor-pointer hover:text-sky-400 hover:text-sky-500" />
<FaDribbble className="cursor-pointer hover:text-pink-400" />
</div>
</div>
</div>
</div> </div>
) )
} }
export default ProfileCard export default ProfileCard
import UserProfile from "../../components/Userdetails/Userdetails.jsx"
@@ -0,0 +1,52 @@
import { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom"
export function requireAuthSync({ key = "username", redirectTo = "/login", replace = true } = {}) {
const value = localStorage.getItem(key)
if (!value) {
if (replace) window.location.replace(redirectTo)
else window.location.assign(redirectTo)
return false
}
return true
}
// New: non-redirecting check for auth status
export function isAuthenticated(key = "username") {
try {
return !!localStorage.getItem(key)
} catch {
return false
}
}
// Default hook: ad vissza egy [value, setValue] párt, szinkronizálja localStorage-t és opcionálisan átirányít, ha nincs érték
export default function useRequireAuth({ key = "username", redirectTo = "/login", redirect = true } = {}) {
const navigate = useNavigate()
const [value, setValue] = useState(() => {
try {
return localStorage.getItem(key)
} catch {
return null
}
})
// Ha nincs érték és redirect engedélyezve van, átirányítjuk (komponens mount-oláskor)
useEffect(() => {
if (!value && redirect) {
navigate(redirectTo)
}
}, [navigate, value, redirectTo, redirect])
// Szinkronizáljuk a localStorage-t amikor a state változik
useEffect(() => {
try {
if (value == null) localStorage.removeItem(key)
else localStorage.setItem(key, value)
} catch {
// fail silently
}
}, [key, value])
return [value, setValue]
}
+5 -1
View File
@@ -8,9 +8,13 @@
--color-eerie-black: #181d23; --color-eerie-black: #181d23;
--color-mint: #15803d; --color-mint: #15803d;
--color-mint-dark: #136636; --color-mint-dark: #136636;
--color-mint-darker: #11522b; --color-mint-darker: #11522b;
--color-mint-light: #16a34a;
--color-mint-lighter: #22c55e;
/* Gombok */ /* Gombok */
--color-button-primary: #16a34a; --color-button-primary: #16a34a;
--color-button-primary-hover: #15803d; --color-button-primary-hover: #15803d;
@@ -27,7 +31,7 @@
/* Deck típus színek */ /* Deck típus színek */
--color-luck: #5fa985; /* zöld, mint a success */ --color-luck: #5fa985; /* zöld, mint a success */
--color-question: #4f7be6; /* új kék, illik az oldalhoz */ --color-question: #4f7be6; /* új kék, illik az oldalhoz */
--color-fun: #e15b64; /* piros, mint az error */ --color-fun: #FFD700; /* citromsárga a joker kártyákhoz */
/* Háttérszínek */ /* Háttérszínek */
--color-background: #181d23; --color-background: #181d23;
+127 -11
View File
@@ -1,12 +1,11 @@
// src/pages/Auth/LoginForm.jsx // src/pages/Auth/LoginForm.jsx
// Bejelentkezési űrlap
import InputBox from "../../components/Inputs/InputBox" import InputBox from "../../components/Inputs/InputBox"
import Button from "../../components/Buttons/Button" import Button from "../../components/Buttons/Button"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { useLocation, useNavigate } from "react-router-dom" import { useLocation, useNavigate } from "react-router-dom"
import { login } from "../../api/userApi" import { login, forgotPassword } from "../../api/userApi"
import { FaArrowLeft } from "react-icons/fa"
export default function LoginForm() { export default function LoginForm() {
const [email, setEmail] = useState("") const [email, setEmail] = useState("")
@@ -16,11 +15,21 @@ export default function LoginForm() {
const navigate = useNavigate() const navigate = useNavigate()
const [showSuccess, setShowSuccess] = useState(false) const [showSuccess, setShowSuccess] = useState(false)
const [showErrorPopup, setShowErrorPopup] = useState(false) const [showErrorPopup, setShowErrorPopup] = useState(false)
const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false)
const [forgotEmail, setForgotEmail] = useState("")
const [forgotPasswordMessage, setForgotPasswordMessage] = useState("")
const [successMessage, setSuccessMessage] = useState("")
const [isSendingEmail, setIsSendingEmail] = useState(false)
useEffect(() => { useEffect(() => {
if (location.state && location.state.success) { if (location.state && location.state.success) {
const message = location.state.message || "Sikeres regisztráció! Az email ellenőrzése után be tudsz lépni."
setSuccessMessage(message)
setShowSuccess(true) setShowSuccess(true)
setTimeout(() => setShowSuccess(false), 4000) setTimeout(() => {
setShowSuccess(false)
setSuccessMessage("")
}, 4000)
} }
}, [location.state]) }, [location.state])
@@ -32,23 +41,23 @@ export default function LoginForm() {
e.preventDefault() e.preventDefault()
setError("") setError("")
setShowErrorPopup(false) setShowErrorPopup(false)
if (!email || !password) { if (!email || !password) {
setError("Minden mező kitöltése kötelező.") setError("Minden mező kitöltése kötelező.")
setShowErrorPopup(true) setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000) setTimeout(() => setShowErrorPopup(false), 2000)
return return
} }
if (!validateEmail(email)) { if (!validateEmail(email)) {
setError("Hibás email formátum.") setError("Hibás email formátum.")
setShowErrorPopup(true) setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000) setTimeout(() => setShowErrorPopup(false), 2000)
return return
} }
// Backend API
login(email, password) login(email, password)
.then((response) => { .then((response) => {
console.log(response)
// Csak a response.status-t ellenőrizd!
if (response && response.status === 200) { if (response && response.status === 200) {
if (response.data && response.data.user) { if (response.data && response.data.user) {
localStorage.setItem("username", response.data.user.username) localStorage.setItem("username", response.data.user.username)
@@ -61,13 +70,39 @@ export default function LoginForm() {
setTimeout(() => setShowErrorPopup(false), 2000) setTimeout(() => setShowErrorPopup(false), 2000)
} }
}) })
.catch((error) => { .catch(() => {
setError("Hibás bejelentkezési adatok.") setError("Hibás bejelentkezési adatok.")
setShowErrorPopup(true) setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000) setTimeout(() => setShowErrorPopup(false), 2000)
}) })
} }
const handleForgotPassword = async (e) => {
e.preventDefault()
setForgotPasswordMessage("")
if (!forgotEmail || !validateEmail(forgotEmail)) {
setForgotPasswordMessage("Kérlek adj meg egy érvényes email címet!")
return
}
setIsSendingEmail(true)
try {
await forgotPassword(forgotEmail)
setForgotPasswordMessage("Jelszó visszaállító email elküldve! Ellenőrizd a postaládád.")
setTimeout(() => {
setShowForgotPasswordModal(false)
setForgotEmail("")
setForgotPasswordMessage("")
setIsSendingEmail(false)
}, 3000)
} catch (error) {
setForgotPasswordMessage("Hiba történt. Próbáld újra később!")
setIsSendingEmail(false)
}
}
return ( return (
<motion.div <motion.div
key="login" key="login"
@@ -75,19 +110,36 @@ export default function LoginForm() {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.25 }} transition={{ duration: 0.25 }}
className="relative flex flex-col items-center"
> >
<h2 className="text-4xl font-extrabold text-center mb-6 text-gray-800 tracking-wide">Bejelentkezés</h2> {/* 🔙 Vissza nyíl gomb — most pontosan a fehér box bal felső sarkában */}
<div
className="absolute -top-6 -left-6 flex items-center group cursor-pointer select-none"
onClick={() => navigate("/")}
>
<FaArrowLeft className="text-gray-700 text-xl transition-transform duration-300 group-hover:-translate-x-1" />
<span className="ml-2 text-gray-700 font-medium opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300">
Vissza a főoldalra
</span>
</div>
<h2 className="text-4xl font-extrabold text-center mb-6 text-gray-800 tracking-wide">
Bejelentkezés
</h2>
{showSuccess && ( {showSuccess && (
<div className="fixed top-6 left-1/2 -translate-x-1/2 bg-green-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300"> <div className="fixed top-6 left-1/2 -translate-x-1/2 bg-green-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300">
Sikeres regisztráció! Az email ellenőrzése után be tudsz lépni. {successMessage || "Sikeres művelet!"}
</div> </div>
)} )}
{showErrorPopup && error && ( {showErrorPopup && error && (
<div className="fixed top-6 left-1/2 -translate-x-1/2 bg-red-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300"> <div className="fixed top-6 left-1/2 -translate-x-1/2 bg-red-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300">
{error} {error}
</div> </div>
)} )}
<form onSubmit={handleSubmit} className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-4 w-full">
<InputBox <InputBox
type="email" type="email"
placeholder="Email cím" placeholder="Email cím"
@@ -100,8 +152,72 @@ export default function LoginForm() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
/> />
{/* Elfelejtett jelszó link */}
<div className="text-right -mt-2">
<span
onClick={() => setShowForgotPasswordModal(true)}
className="text-sm text-green-600 hover:text-green-700 hover:underline cursor-pointer font-medium"
>
Elfelejtetted a jelszavad?
</span>
</div>
<Button text="Bejelentkezés" type="submit" /> <Button text="Bejelentkezés" type="submit" />
</form> </form>
{/* Forgot Password Modal */}
{showForgotPasswordModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setShowForgotPasswordModal(false)}>
<div className="bg-white rounded-xl shadow-2xl p-8 w-96 max-w-full" onClick={(e) => e.stopPropagation()}>
<h3 className="text-2xl font-bold text-gray-800 mb-4">Jelszó visszaállítás</h3>
<p className="text-gray-600 mb-6 text-sm">
Add meg az email címed és küldünk egy jelszó visszaállító linket.
</p>
<form onSubmit={handleForgotPassword} className="space-y-4">
<InputBox
type="email"
placeholder="Email cím"
value={forgotEmail}
onChange={(e) => setForgotEmail(e.target.value)}
/>
{forgotPasswordMessage && (
<div className={`text-sm p-3 rounded ${forgotPasswordMessage.includes("elküldve") ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700"}`}>
{forgotPasswordMessage}
</div>
)}
<div className="flex gap-3">
<button
type="submit"
disabled={isSendingEmail}
className={`flex-1 px-4 py-2 rounded-lg font-semibold transition-colors ${
isSendingEmail
? 'bg-gray-400 text-gray-200 cursor-not-allowed'
: 'bg-[color:var(--color-mint)] text-white hover:opacity-90'
}`}
>
{isSendingEmail ? 'Küldés...' : 'Küldés'}
</button>
<button
type="button"
onClick={() => {
setShowForgotPasswordModal(false)
setForgotEmail("")
setForgotPasswordMessage("")
setIsSendingEmail(false)
}}
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors font-semibold"
>
Mégse
</button>
</div>
</form>
</div>
</div>
)}
</motion.div> </motion.div>
) )
} }
@@ -1,12 +1,12 @@
// src/pages/Auth/RegisterForm.jsx // src/pages/Auth/RegisterForm.jsx
// Regisztrációs űrlap
import InputBox from "../../components/Inputs/InputBox" import InputBox from "../../components/Inputs/InputBox"
import Button from "../../components/Buttons/Button" import Button from "../../components/Buttons/Button"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import { useState } from "react" import { useState } from "react"
import { register } from "../../api/userApi" import { register } from "../../api/userApi"
import { useNavigate } from "react-router-dom" import { useNavigate, useLocation } from "react-router-dom"
import { ToastConfig } from "../../components/Toastify/toastifyServices"
import { FaArrowLeft } from "react-icons/fa"
export default function RegisterForm() { export default function RegisterForm() {
const [lastname, setLastname] = useState("") const [lastname, setLastname] = useState("")
@@ -19,6 +19,7 @@ export default function RegisterForm() {
const [error, setError] = useState("") const [error, setError] = useState("")
const [showErrorPopup, setShowErrorPopup] = useState(false) const [showErrorPopup, setShowErrorPopup] = useState(false)
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation()
function validateEmail(email) { function validateEmail(email) {
return /\S+@\S+\.\S+/.test(email) return /\S+@\S+\.\S+/.test(email)
@@ -45,28 +46,26 @@ export default function RegisterForm() {
setError("A jelszavak nem egyeznek.") setError("A jelszavak nem egyeznek.")
return return
} }
// Backend API
try { try {
const response = await register(username, email, password, firstname, lastname, phone) const response = await register(username, email, password, firstname, lastname, phone)
// Check for 201 Created status
if (response && response.status === 201) { if (response && response.status === 201) {
ToastConfig("✅ Sikeres regisztráció!")
if (location.pathname === "/login") {
navigate("/login", { state: { success: true } }) navigate("/login", { state: { success: true } })
window.location.reload()
} else {
navigate("/login", { state: { success: true } })
}
} else { } else {
let msg = "Sikertelen regisztráció." let msg = "Sikertelen regisztráció."
if (response && response.data && response.data.error) { if (response?.data?.error) msg = response.data.error
msg = response.data.error
}
setError(msg) setError(msg)
setShowErrorPopup(true) setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000) setTimeout(() => setShowErrorPopup(false), 2000)
} }
} catch (err) { } catch (err) {
let msg = "Ismeretlen hiba történt." let msg = err?.response?.data?.error || err.message || "Ismeretlen hiba történt."
if (err.response && err.response.data && err.response.data.error) {
msg = err.response.data.error
} else if (err.message) {
msg = err.message
}
setError(msg) setError(msg)
setShowErrorPopup(true) setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000) setTimeout(() => setShowErrorPopup(false), 2000)
@@ -80,56 +79,37 @@ export default function RegisterForm() {
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.25 }} transition={{ duration: 0.25 }}
className="relative flex flex-col items-center"
> >
<h2 className="text-4xl font-extrabold text-center mb-6 text-gray-800 tracking-wide">Regisztráció</h2> {/* 🔙 Vissza nyíl gomb ugyanott, mint a login oldalon */}
<div
className="absolute -top-2 -left-1 flex items-center group cursor-pointer select-none"
onClick={() => navigate("/")}
>
<FaArrowLeft className="text-gray-700 text-xl transition-transform duration-300 group-hover:-translate-x-1" />
<span className="ml-2 text-gray-700 font-medium opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300">
Vissza a főoldalra
</span>
</div>
<h2 className="text-4xl font-extrabold text-center mb-6 text-gray-800 tracking-wide">
Regisztráció
</h2>
{showErrorPopup && error && ( {showErrorPopup && error && (
<div className="fixed top-6 left-1/2 -translate-x-1/2 bg-red-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300"> <div className="fixed top-6 left-1/2 -translate-x-1/2 bg-red-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300">
{error} {error}
</div> </div>
)} )}
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<InputBox <InputBox type="text" placeholder="Vezetéknév" value={lastname} onChange={(e) => setLastname(e.target.value)} />
type="text" <InputBox type="text" placeholder="Keresztnév" value={firstname} onChange={(e) => setFirstname(e.target.value)} />
placeholder="Vezetéknév" <InputBox type="text" placeholder="Felhasználónév" value={username} onChange={(e) => setUsername(e.target.value)} />
value={lastname} <InputBox type="email" placeholder="Email cím" value={email} onChange={(e) => setEmail(e.target.value)} />
onChange={(e) => setLastname(e.target.value)} <InputBox type="phone" placeholder="Telefonszám" value={phone} onChange={(e) => setPhone(e.target.value)} />
/> <InputBox type="password" placeholder="Jelszó" value={password} onChange={(e) => setPassword(e.target.value)} />
<InputBox <InputBox type="password" placeholder="Jelszó megerősítése" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} />
type="text"
placeholder="Keresztnév"
value={firstname}
onChange={(e) => setFirstname(e.target.value)}
/>
<InputBox
type="text"
placeholder="Felhasználónév"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<InputBox
type="email"
placeholder="Email cím"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<InputBox
type="phone"
placeholder="Telefonszám"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
<InputBox
type="password"
placeholder="Jelszó"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<InputBox
type="password"
placeholder="Jelszó megerősítése"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
<Button text="Regisztráció" type="submit" /> <Button text="Regisztráció" type="submit" />
</form> </form>
</motion.div> </motion.div>
@@ -1,26 +1,63 @@
// src/pages/Auth/ResetPassword.jsx // src/pages/Auth/ResetPassword.jsx
// Új jelszó megadása // Új jelszó megadása
import { useState } from "react"; import { useState, useEffect } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import Background from "../../assets/backgrounds/Background"; import Background from "../../assets/backgrounds/Background";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import Button from "../../components/Buttons/Button"; import Button from "../../components/Buttons/Button";
import InputBox from "../../components/Inputs/InputBox"; import InputBox from "../../components/Inputs/InputBox";
import { resetPassword } from "../../api/userApi";
import { FaArrowLeft } from "react-icons/fa";
export default function ResetPassword() { export default function ResetPassword() {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const token = searchParams.get("token");
const handleSubmit = (e) => { useEffect(() => {
if (!token) {
setError("Érvénytelen vagy hiányzó token!");
}
}, [token]);
const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (password !== confirmPassword) { setError("");
setError("A jelszavak nem egyeznek.");
if (!password || !confirmPassword) {
setError("Minden mező kitöltése kötelező!");
return; return;
} }
setError("");
// Backend API if (password.length < 6) {
console.log("Új jelszó:", password); setError("A jelszónak legalább 6 karakter hosszúnak kell lennie!");
return;
}
if (password !== confirmPassword) {
setError("A jelszavak nem egyeznek!");
return;
}
if (!token) {
setError("Érvénytelen token!");
return;
}
try {
await resetPassword(token, password);
setSuccess(true);
setTimeout(() => {
navigate("/login", { state: { success: true, message: "Jelszó sikeresen megváltoztatva! Jelentkezz be az új jelszóval." } });
}, 2000);
} catch (err) {
setError(err.response?.data?.message || "Hiba történt a jelszó visszaállítása során!");
}
}; };
return ( return (
@@ -28,18 +65,41 @@ export default function ResetPassword() {
<Background /> <Background />
<motion.div <motion.div
initial={{ height: "auto" }} initial={{ height: "auto" }}
animate={{ height: "350px" }} animate={{ height: "auto" }}
transition={{ duration: 0.5, ease: "easeInOut" }} transition={{ duration: 0.5, ease: "easeInOut" }}
className="absolute flex max-w-2xl w-full bg-white rounded-2xl shadow-lg overflow-hidden items-center justify-center" className="absolute flex max-w-2xl w-full bg-white rounded-2xl shadow-lg overflow-hidden items-center justify-center"
> >
<div className="w-full p-10 relative"> <div className="w-full p-10 relative">
<h2 className="text-4xl font-extrabold text-center mb-6 text-gray-800 tracking-wide"> {/* Vissza gomb */}
<div
className="absolute -top-(-2) -left-(-1) flex items-center group cursor-pointer select-none"
onClick={() => navigate("/login")}
>
<FaArrowLeft className="text-gray-700 text-xl transition-transform duration-300 group-hover:-translate-x-1" />
<span className="ml-2 text-gray-700 font-medium opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300">
Vissza a bejelentkezéshez
</span>
</div>
<h2 className="text-4xl font-extrabold text-center mb-6 text-gray-800 tracking-wide mt-6">
Új jelszó megadása Új jelszó megadása
</h2> </h2>
<form onSubmit={handleSubmit}>
{success ? (
<div className="text-center">
<div className="text-green-500 text-6xl mb-4"></div>
<p className="text-xl text-green-600 font-semibold">
Jelszó sikeresen megváltoztatva!
</p>
<p className="text-gray-600 mt-2">
Átirányítás a bejelentkezéshez...
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
<InputBox <InputBox
type="password" type="password"
placeholder="Új jelszó" placeholder="Új jelszó (min. 6 karakter)"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
/> />
@@ -50,10 +110,13 @@ export default function ResetPassword() {
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
/> />
{error && ( {error && (
<div className="text-red-500 text-sm mb-2">{error}</div> <div className="bg-red-100 text-red-700 p-3 rounded text-sm">
{error}
</div>
)} )}
<Button text="Jelszó beállítása" type="submit" /> <Button text="Jelszó beállítása" type="submit" />
</form> </form>
)}
</div> </div>
</motion.div> </motion.div>
</div> </div>
@@ -0,0 +1,30 @@
// src/pages/Auth/ResetPasswordRedirect.jsx
// Redirect component for /api/auth/reset-password links from emails
import { useEffect } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
export default function ResetPasswordRedirect() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
useEffect(() => {
const token = searchParams.get("token");
if (token) {
// Redirect to the actual reset password page
navigate(`/reset-password?token=${token}`, { replace: true });
} else {
// No token, redirect to login
navigate("/login", { replace: true });
}
}, [searchParams, navigate]);
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="text-6xl mb-4"></div>
<p className="text-xl">Átirányítás...</p>
</div>
</div>
);
}
@@ -0,0 +1,89 @@
import { useEffect, useState, useRef } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import Background from "../../assets/backgrounds/Background";
import { notifySuccess, notifyError } from "../../components/Toastify/toastifyServices";
import { verifyEmail } from "../../api/userApi";
export default function VerifyEmailPage() {
const navigate = useNavigate();
const location = useLocation();
const [status, setStatus] = useState("loading");
const [message, setMessage] = useState("Email címe hitelesítés alatt...");
const hasNotified = useRef(false); // <-- ez biztosítja, hogy csak egyszer legyen a toast
useEffect(() => {
const queryParams = new URLSearchParams(location.search);
const token = queryParams.get("token");
if (!token) {
setStatus("error");
setMessage("Hiányzó hitelesítő token!");
if (!hasNotified.current) {
notifyError("❌ Hiányzó hitelesítő token!");
hasNotified.current = true;
}
return;
}
const verify = async () => {
try {
const response = await verifyEmail(token);
const data = response.data;
if (data?.success) {
setStatus("success");
setMessage("Sikeres hitelesítés!");
if (!hasNotified.current) {
notifySuccess("✅ Email címe sikeresen hitelesítve!");
hasNotified.current = true;
}
setTimeout(() => navigate("/login"), 2500);
} else {
throw new Error(data?.message || "Sikertelen hitelesítés");
}
} catch (err) {
setStatus("error");
setMessage("Sikertelen hitelesítés. Kérjük, vegye fel velünk a kapcsolatot!");
if (!hasNotified.current) {
notifyError("❌ Sikertelen hitelesítés!");
hasNotified.current = true;
}
}
};
verify();
}, [location, navigate]);
return (
<div className="w-full min-h-screen flex items-center justify-center relative overflow-hidden">
<div className="fixed inset-0 -z-10">
<Background />
</div>
<div className="bg-white/10 backdrop-blur-lg p-10 rounded-2xl shadow-lg text-center border border-white/20 max-w-md">
{status === "loading" && (
<div className="flex flex-col items-center gap-3 text-white">
<div className="w-10 h-10 border-4 border-white border-t-transparent rounded-full animate-spin"></div>
<p className="text-lg font-semibold">{message}</p>
</div>
)}
{status === "success" && (
<div className="flex flex-col items-center gap-3 text-green-300">
<span className="text-4xl"></span>
<p className="text-lg font-semibold">{message}</p>
<p className="text-sm text-gray-200">Átirányítás a bejelentkezéshez...</p>
</div>
)}
{status === "error" && (
<div className="flex flex-col items-center gap-3 text-red-300">
<span className="text-4xl"></span>
<p className="text-lg font-semibold text-center">{message}</p>
<p className="text-sm text-gray-200">support@serpentrace.hu</p>
</div>
)}
</div>
</div>
);
}
@@ -1,7 +1,7 @@
import React from "react" import React, { useEffect, useRef, useState } from "react"
import Navbar from "../../components/Navbar/Navbar.jsx" import Navbar from "../../components/Navbar/Navbar.jsx"
import Footer from "../../components/Footer/Footer.jsx" import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background" import Background from "../../assets/backgrounds/Background.jsx"
import { import {
FaBuilding, FaBuilding,
FaEnvelope, FaEnvelope,
@@ -56,6 +56,20 @@ const SectionContainer = ({ id, title, children }) => {
} }
const CompanyHub = () => { const CompanyHub = () => {
const [visible, setVisible] = useState(false)
const sectionRef = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setVisible(true)
},
{ threshold: 0.3 }
)
if (sectionRef.current) observer.observe(sectionRef.current)
return () => observer.disconnect()
}, [])
return ( return (
<div className=" relative min-h-screen text-white"> <div className=" relative min-h-screen text-white">
{/* Background fixed behind everything */} {/* Background fixed behind everything */}
@@ -67,6 +81,12 @@ const CompanyHub = () => {
<Navbar /> <Navbar />
<main className="flex-grow relative px-4 py-8 md:px-12 md:py-16 overflow-y-auto scroll-smooth"> <main className="flex-grow relative px-4 py-8 md:px-12 md:py-16 overflow-y-auto scroll-smooth">
<section
ref={sectionRef}
className={`max-w-5xl mx-auto transition-all duration-1000 ease-out ${
visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
}`}
>
<div className="flex justify-center gap-6 mt-8 flex-wrap"> <div className="flex justify-center gap-6 mt-8 flex-wrap">
<Card <Card
icon={<FaBuilding />} icon={<FaBuilding />}
@@ -215,6 +235,7 @@ const CompanyHub = () => {
</ul> </ul>
</div> </div>
</section> </section>
</section>
</main> </main>
<Footer /> <Footer />
@@ -7,18 +7,19 @@ import Navbar from "../../components/Navbar/Navbar.jsx"
import DeckHeader from "../../components/DeckCreator/DeckHeader.jsx" import DeckHeader from "../../components/DeckCreator/DeckHeader.jsx"
import CardsList from "../../components/DeckCreator/CardsList.jsx" import CardsList from "../../components/DeckCreator/CardsList.jsx"
import CardEditor from "../../components/DeckCreator/CardEditor.jsx" import CardEditor from "../../components/DeckCreator/CardEditor.jsx"
import { createDeck } from '../../api/deckApi' import { createDeck, getDeckById, updateDeck } from '../../api/deckApi'
import { notifySuccess, notifyError, notifyWarning } from "../../components/Toastify/toastifyServices"
export default function DeckCreator() { export default function DeckCreator() {
const { deckId } = useParams() // URL-ből deck ID (új deck esetén undefined) const { deckId } = useParams()
const navigate = useNavigate() const navigate = useNavigate()
// Deck alapadatok // Deck alapadatok
const [deck, setDeck] = useState({ const [deck, setDeck] = useState({
id: null, id: null,
name: "Új Deck", name: "Új Pakli",
type: "Question", // Question, Luck, Fun type: "QUESTION",
privacy: "private", // private, public privacy: "private",
description: "", description: "",
cards: [] cards: []
}) })
@@ -26,19 +27,18 @@ export default function DeckCreator() {
// UI állapotok // UI állapotok
const [selectedCard, setSelectedCard] = useState(null) const [selectedCard, setSelectedCard] = useState(null)
const [isCreatingCard, setIsCreatingCard] = useState(false) const [isCreatingCard, setIsCreatingCard] = useState(false)
const [newCardType, setNewCardType] = useState(null) // task, joker, luck const [newCardType, setNewCardType] = useState(null)
const [isLoading, setIsLoading] = useState(false)
// Betöltés (később API-ból) // Betöltés API-ból
useEffect(() => { useEffect(() => {
if (deckId) { if (deckId) {
// TODO: Betöltés API-ból
loadDeck(deckId) loadDeck(deckId)
} else { } else {
// Új deck
setDeck({ setDeck({
id: null, id: null,
name: "Új Deck", name: "Új Pakli",
type: "Question", type: "QUESTION",
privacy: "private", privacy: "private",
description: "", description: "",
cards: [] cards: []
@@ -47,38 +47,44 @@ export default function DeckCreator() {
}, [deckId]) }, [deckId])
const loadDeck = async (id) => { const loadDeck = async (id) => {
// Mock deck betöltés setIsLoading(true)
const mockDeck = { try {
id: id, const deckData = await getDeckById(id)
name: "Quiz Night", console.log('Loaded deck:', deckData)
type: "Question",
privacy: "public", // Type mapping from backend to frontend
description: "Szórakoztató kvíz este", const typeMapping = {
cards: [ 0: 'LUCK',
{ 1: 'JOKER',
id: 1, 2: 'QUESTION'
type: "task",
subType: "quiz",
question: "Mi Magyarország fővárosa?",
options: ["Budapest", "Debrecen", "Szeged", "Pécs"],
correctAnswer: 0,
points: 10,
timeLimit: 30,
explanation: "Budapest 1873 óta Magyarország fővárosa."
},
{
id: 2,
type: "task",
subType: "truefalse",
statement: "A Duna Magyarország leghosszabb folyója.",
isTrue: false,
points: 5,
timeLimit: 15,
explanation: "A Tisza a leghosszabb folyó Magyarországon."
} }
]
// CType mapping from backend to frontend
const ctypeMapping = {
0: 'public',
1: 'private',
2: 'organization'
}
setDeck({
id: deckData.id,
name: deckData.name || "Névtelen pakli",
type: typeMapping[deckData.type] || 'QUESTION',
privacy: ctypeMapping[deckData.ctype] || 'private',
description: deckData.description || "",
cards: deckData.cards || [],
creationdate: deckData.creationdate,
updatedate: deckData.updatedate
})
// Success notification removed - silent load for better UX
} catch (error) {
console.error('Pakli betöltési hiba:', error)
notifyError('Hiba történt a pakli betöltése során: ' + (error?.response?.data?.error || error.message))
navigate('/decks')
} finally {
setIsLoading(false)
} }
setDeck(mockDeck)
} }
const handleDeckUpdate = (updates) => { const handleDeckUpdate = (updates) => {
@@ -87,31 +93,76 @@ export default function DeckCreator() {
const handleSaveDeck = async () => { const handleSaveDeck = async () => {
try { try {
console.log("Deck mentése:", deck) // Típus konverzió backendhez
const typeMapping = {
const payload = { 'LUCK': 0,
name: deck.name, 'JOKER': 1,
type: (deck.type || 'Question').toString().toUpperCase(), 'QUESTION': 2
ctype: deck.privacy === 'public' ? 'PUBLIC' : 'PRIVATE',
description: deck.description || '',
cards: deck.cards.map(c => ({ ...c, text: c.question || c.statement || c.text || '' }))
} }
const saved = await createDeck(payload) const ctypeMapping = {
setDeck(prev => ({ ...prev, id: saved.id ?? prev.id, creationdate: saved.creationdate ?? prev.creationdate, updatedate: saved.updatedate ?? prev.updatedate })) 'public': 0,
'private': 1,
'organization': 2
}
const payload = {
name: deck.name?.trim() || "Névtelen pakli",
type: typeMapping[deck.type] ?? 2,
ctype: ctypeMapping[deck.privacy] ?? 1,
cards: deck.cards || []
}
// Note: description field is not sent to backend as it's not supported yet
console.log('=== DECK SAVE DEBUG ===')
console.log('Deck ID:', deck.id)
console.log('Deck object:', deck)
console.log('Payload to send:', payload)
console.log('Is Update?', !!deck.id)
let saved
if (deck.id) {
// Update existing deck
console.log('Calling updateDeck with ID:', deck.id)
saved = await updateDeck(deck.id, payload)
console.log('Update response:', saved)
notifySuccess('Pakli sikeresen frissítve!')
} else {
// Create new deck
console.log('Calling createDeck')
saved = await createDeck(payload)
console.log('Create response:', saved)
notifySuccess('Pakli sikeresen létrehozva!')
}
setDeck(prev => ({
...prev,
id: saved.id ?? prev.id,
creationdate: saved.creationdate ?? prev.creationdate,
updatedate: saved.updatedate ?? prev.updatedate
}))
console.log('Deck saved (backend):', saved) console.log('Deck saved (backend):', saved)
alert('✅ Deck sikeresen mentve!')
} catch (error) { } catch (error) {
console.error('Mentési hiba:', error) console.error('=== DECK SAVE ERROR ===')
alert('❌ Hiba történt a mentés során: ' + (error?.response?.data?.error || error.message || String(error))) console.error('Full error:', error)
console.error('Error response:', error?.response)
console.error('Error response data:', error?.response?.data)
console.error('Error message:', error?.message)
const errorMessage = error?.response?.data?.error
|| error?.response?.data?.message
|| error?.message
|| 'Ismeretlen hiba történt'
notifyError('Hiba történt a mentés során: ' + errorMessage)
} }
} }
const handleBack = () => { const handleBack = () => {
if (confirm("Biztosan visszamész? A nem mentett változtatások elvesznek.")) {
navigate("/decks") navigate("/decks")
} }
}
const handleCreateCard = (cardType) => { const handleCreateCard = (cardType) => {
setNewCardType(cardType) setNewCardType(cardType)
@@ -125,34 +176,72 @@ export default function DeckCreator() {
setNewCardType(null) setNewCardType(null)
} }
// 💡 Demo verzió: beállítások szekció kihagyva
const handleSaveCard = (cardData) => { const handleSaveCard = (cardData) => {
if (isCreatingCard) { try {
// Új kártya hozzáadása if (cardData.section === "settings") {
const newCard = { console.log("Beállítások szekció kihagyva (demo verzió)")
...cardData, return
id: Date.now(), // Temporary ID
} }
setDeck(prev => ({
...prev, const defaultConsequence = { type: 0, value: 1 }
cards: [...prev.cards, newCard] const defaultWrongConsequence = { type: 1, value: 1 }
}))
setIsCreatingCard(false) const updatedCard = {
setNewCardType(null) ...cardData,
setSelectedCard(newCard) id: isCreatingCard ? Date.now() : cardData.id,
} else { consequence: cardData.consequence || defaultConsequence,
// Meglévő kártya frissítése ...(cardData.type === 'QUESTION' || cardData.type === 'JOKER' || cardData.type === 'PAIRING'
setDeck(prev => ({ ? { wrongConsequence: cardData.wrongConsequence || defaultWrongConsequence }
...prev, : {}
cards: prev.cards.map(card =>
card.id === cardData.id ? cardData : card
) )
})) }
setSelectedCard(cardData)
let wasInvalidCardDeleted = false
setDeck(prev => {
const invalidCards = prev.cards.filter(card => card.type !== prev.type)
if (isCreatingCard && cardData.type === prev.type && invalidCards.length > 0) {
wasInvalidCardDeleted = true
return {
...prev,
cards: [
...prev.cards.filter(card => card.type === prev.type),
updatedCard
]
} }
} }
return {
...prev,
cards: isCreatingCard
? [...prev.cards, updatedCard]
: prev.cards.map(card => card.id === updatedCard.id ? updatedCard : card)
}
})
setSelectedCard(updatedCard)
setIsCreatingCard(false)
setNewCardType(null)
if (wasInvalidCardDeleted) {
const invalidCount = deck.cards.filter(card => card.type !== deck.type).length
notifyWarning(`Kártya mentve! ${invalidCount} db nem megfelelő típusú kártya törlésre került.`)
} else {
notifySuccess('Kártya sikeresen mentve!')
}
} catch (error) {
console.error('Kártya mentési hiba:', error)
notifyError('Hiba történt a kártya mentése során')
}
}
// 💬 Felugró ablak törlés előtt
const handleDeleteCard = (cardId) => { const handleDeleteCard = (cardId) => {
if (confirm("Biztosan törlöd ezt a kártyát?")) { const confirmDelete = window.confirm("Biztosan törölni szeretnéd ezt a kártyát?")
if (!confirmDelete) return
setDeck(prev => ({ setDeck(prev => ({
...prev, ...prev,
cards: prev.cards.filter(card => card.id !== cardId) cards: prev.cards.filter(card => card.id !== cardId)
@@ -161,13 +250,27 @@ export default function DeckCreator() {
if (selectedCard?.id === cardId) { if (selectedCard?.id === cardId) {
setSelectedCard(null) setSelectedCard(null)
} }
}
notifySuccess("Kártya törölve a pakliból!")
} }
return ( return (
<div className="w-full min-h-screen bg-[color:var(--color-background)] flex flex-col"> <div className="w-full min-h-screen bg-[color:var(--color-background)] flex flex-col">
<Navbar /> <Navbar />
{isLoading ? (
<main className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className="text-6xl mb-4"></div>
<div className="text-[color:var(--color-text)] text-xl font-semibold mb-2">
Pakli betöltése...
</div>
<div className="text-[color:var(--color-text-muted)]">
Kérlek várj, amíg betöltjük a pakli adatait.
</div>
</div>
</main>
) : (
<main className="flex-1 flex flex-col"> <main className="flex-1 flex flex-col">
{/* Deck Header */} {/* Deck Header */}
<DeckHeader <DeckHeader
@@ -184,6 +287,7 @@ export default function DeckCreator() {
<CardsList <CardsList
cards={deck.cards} cards={deck.cards}
selectedCard={selectedCard} selectedCard={selectedCard}
deckType={deck.type}
onSelectCard={handleSelectCard} onSelectCard={handleSelectCard}
onCreateCard={handleCreateCard} onCreateCard={handleCreateCard}
onDeleteCard={handleDeleteCard} onDeleteCard={handleDeleteCard}
@@ -197,7 +301,7 @@ export default function DeckCreator() {
<CardEditor <CardEditor
card={selectedCard} card={selectedCard}
isCreating={isCreatingCard} isCreating={isCreatingCard}
cardType={newCardType} cardType={isCreatingCard ? newCardType : deck.type}
onSave={handleSaveCard} onSave={handleSaveCard}
onCancel={() => { onCancel={() => {
setIsCreatingCard(false) setIsCreatingCard(false)
@@ -208,6 +312,7 @@ export default function DeckCreator() {
</div> </div>
</div> </div>
</main> </main>
)}
</div> </div>
) )
} }
@@ -1,7 +1,7 @@
// src/pages/Decks/DeckManagerPage.jsx // src/pages/Decks/DeckManagerPage.jsx
// Deck Management Page (with Navbar, no Footer) // Deck Management Page (with Navbar, no Footer)
import DeckManager from "../../components/Landingpage/DeckManager.jsx" import DeckManager from "../../components/DeckCreator/DeckManager.jsx"
import Navbar from "../../components/Navbar/Navbar.jsx" import Navbar from "../../components/Navbar/Navbar.jsx"
export default function DeckManagerPage() { export default function DeckManagerPage() {
@@ -66,12 +66,17 @@ const GameScreen = () => {
{ id: 3, name: "Fürtös", position: 68, score: 14, color: "bg-yellow-600", emoji: "😂" }, { id: 3, name: "Fürtös", position: 68, score: 14, color: "bg-yellow-600", emoji: "😂" },
]) ])
// New: selected dice value from dropdown (null = none)
const [selectedDice, setSelectedDice] = useState(null)
// Sort players by position in descending order // Sort players by position in descending order
const sortedPlayers = [...players].sort((a, b) => b.position - a.position) const sortedPlayers = [...players].sort((a, b) => b.position - a.position)
// Handle dice roll // Handle dice roll completion
const handleDiceRoll = (value) => { const handleDiceRoll = (value) => {
console.log("Rolled:", value) console.log("Rolled:", value)
// reset dropdown selection after roll
setSelectedDice(null)
// You can add logic here to move the current player based on the dice value // You can add logic here to move the current player based on the dice value
} }
@@ -118,9 +123,6 @@ const GameScreen = () => {
{/* Háttér */} {/* Háttér */}
<div className="absolute w-full h-full opacity-10 pointer-events-none overflow-hidden"> <div className="absolute w-full h-full opacity-10 pointer-events-none overflow-hidden">
{[...Array(35)].map((_, i) => ( {[...Array(35)].map((_, i) => (
// Sajat pulse effect! => node_modules/tailwindcss/index.css:
// --animate-pulse8: pulse 6s cubic-bezier(0.4, 0.2, 0.6, 1) infinite;
<div <div
key={i} key={i}
className="absolute rounded-full bg-teal-600 animate-pulse8" className="absolute rounded-full bg-teal-600 animate-pulse8"
@@ -187,7 +189,7 @@ const GameScreen = () => {
{sortedPlayers.map((player, index) => ( {sortedPlayers.map((player, index) => (
<div <div
key={player.id} key={player.id}
className="flex items-center mb-3 p-2 bg-gray-900 rounded-lg hover:bg-gray-800 transition-colors" className="flex items-center mb-3 p-2 bg-gray-900 rounded-lg hover:bg-gray-700 transition-colors"
> >
<div <div
className={`w-8 h-8 ${player.color} rounded-full mr-3 flex items-center justify-center text-white text-sm font-bold shadow-md`} className={`w-8 h-8 ${player.color} rounded-full mr-3 flex items-center justify-center text-white text-sm font-bold shadow-md`}
@@ -222,8 +224,31 @@ const GameScreen = () => {
{/* Dice Container */} {/* Dice Container */}
<div className="bg-gray-800 rounded-xl p-4 shadow-lg border border-teal-700 text-center"> <div className="bg-gray-800 rounded-xl p-4 shadow-lg border border-teal-700 text-center">
<h2 className="text-xl font-semibold mb-3 text-teal-300">Dobókocka</h2> <h2 className="text-xl font-semibold mb-3 text-teal-300">Dobókocka</h2>
<p className="text-gray-300 text-sm mb-4">Kattints a kockára dobáshoz!</p> <p className="text-gray-300 text-sm mb-4">
<Dice onRoll={handleDiceRoll} /> Kattints a kockára dobáshoz vagy válassz egy számot az alábbiból!
</p>
{/* Dropdown to select number 1-6 (triggers animated roll to that number) */}
<div className="mb-3">
<select
value={selectedDice ?? ""}
onChange={(e) => {
const v = e.target.value ? Number(e.target.value) : null
setSelectedDice(v)
}}
className="bg-gray-900 text-gray-200 rounded-md p-2 border border-gray-700"
>
<option value="">Válassz számot...</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
</div>
<Dice onRoll={handleDiceRoll} selectedValue={selectedDice} />
</div> </div>
</div> </div>
</div> </div>
@@ -1,46 +0,0 @@
// src/pages/Home/Home.jsx
// Régi PlayMenu-s oldal, "Home" néven
import { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom"
import Navbar from "../../components/Navbar/Navbar"
import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx"
import PlayMenu from "../../components/Landingpage/PlayMenu.jsx"
export default function Home() {
const navigate = useNavigate()
useEffect(() => {
const username = localStorage.getItem("username")
const authLevel = localStorage.getItem("authLevel")
if (!username || !authLevel) {
navigate("/login")
}
}, [navigate])
// Dummy callbackok és user példa
const handleJoinGame = (code) => {
alert(`Csatlakozás játékhoz: ${code}`)
}
const handleCreateGame = () => {
alert("Új játék létrehozása")
}
const user = { name: localStorage.getItem("username") }
return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
<div className="fixed inset-0 -z-10 pointer-events-none">
<Background />
</div>
<div className="fixed top-0 left-0 right-0 z-30">
<Navbar />
</div>
<main className="flex-1 flex flex-col items-center justify-start py-15 min-h-0 mt-[64px]">
<PlayMenu onJoinGame={handleJoinGame} onCreateGame={handleCreateGame} user={user} />
{/* Ide jöhetnek további szekciók, ha szeretnél még tartalmat */}
</main>
<Footer />
</div>
)
}
@@ -1,13 +1,17 @@
// src/pages/Home/Home.jsx // src/pages/Home/Home.jsx
// Régi PlayMenu-s oldal, "Home" néven // Régi PlayMenu-s oldal, "Home" néven
import { useState } from "react" import { useEffect } from "react"
import Navbar from "../../components/Navbar/Navbar.jsx" import useRequireAuth from "../../hooks/useRequireAuth"
import Navbar from "../../components/Navbar/Navbar"
import Footer from "../../components/Footer/Footer.jsx" import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx" import Background from "../../assets/backgrounds/Background.jsx"
import PlayMenu from "../../components/Landingpage/PlayMenu.jsx" import PlayMenu from "../../components/Landingpage/PlayMenu.jsx"
export default function Home() { export default function Home() {
// a hook inicializálja a user-t a localStorage-ból és visszaadja a state-et + settert
const [user, setUser] = useRequireAuth({ redirect: false }) // no redirect on unauthenticated visitors
// Dummy callbackok és user példa // Dummy callbackok és user példa
const handleJoinGame = (code) => { const handleJoinGame = (code) => {
alert(`Csatlakozás játékhoz: ${code}`) alert(`Csatlakozás játékhoz: ${code}`)
@@ -15,7 +19,9 @@ export default function Home() {
const handleCreateGame = () => { const handleCreateGame = () => {
alert("Új játék létrehozása") alert("Új játék létrehozása")
} }
const user = { name: "Teszt Elek" } const userObj = { name: user }
// ha szükséges a user módosítása máshol: setUser("újnév") automatikusan menti localStorage-be
return ( return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden"> <div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
@@ -25,8 +31,13 @@ export default function Home() {
<div className="fixed top-0 left-0 right-0 z-30"> <div className="fixed top-0 left-0 right-0 z-30">
<Navbar /> <Navbar />
</div> </div>
<main className="flex-1 flex flex-col items-center justify-start py-15 min-h-0 mt-[64px]"> <main className="flex-1 min-h-[calc(100vh-64px)] flex mt-[64px] flex-col items-center justify-center">
<PlayMenu onJoinGame={handleJoinGame} onCreateGame={handleCreateGame} user={user} /> <PlayMenu
onJoinGame={handleJoinGame}
onCreateGame={handleCreateGame}
user={userObj}
setUser={setUser}
/>
{/* Ide jöhetnek további szekciók, ha szeretnél még tartalmat */} {/* Ide jöhetnek további szekciók, ha szeretnél még tartalmat */}
</main> </main>
<Footer /> <Footer />
@@ -2,7 +2,7 @@
// Főoldal - Landing Page // Főoldal - Landing Page
import { useNavigate } from "react-router-dom" import { data, useNavigate } from "react-router-dom"
import Navbar from "../../components/Navbar/Navbar" import Navbar from "../../components/Navbar/Navbar"
import Footer from "../../components/Footer/Footer.jsx" import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx" import Background from "../../assets/backgrounds/Background.jsx"
@@ -12,11 +12,22 @@ export default function LandingPageMain() {
const navigate = useNavigate(); const navigate = useNavigate();
const handleNavigateToPlay = () => { const handleNavigateToPlay = () => {
navigate("/login"); navigate("/login", { preventScrollReset: false });
window.scrollTo(0, 0);
}; };
const handleNavigateToAuth = () => { const handleNavigateToAuth = () => {
navigate("/register"); navigate("/companies", { preventScrollReset: false });
window.scrollTo(0, 0);
};
const handleNavigateToGame = () => {
navigate("/home", { preventScrollReset: false });
window.scrollTo(0, 0);
};
const handleNavigateToContacts = () => {
navigate("/contacts");
}; };
return ( return (
@@ -28,7 +39,7 @@ export default function LandingPageMain() {
<Navbar /> <Navbar />
</div> </div>
<main className="flex-1 flex flex-col items-center justify-start py-15 min-h-0 mt-[64px]"> <main className="flex-1 flex flex-col items-center justify-start py-15 min-h-0 mt-[64px]">
<LandingPage onNavigateToPlay={handleNavigateToPlay} onNavigateToAuth={handleNavigateToAuth} /> <LandingPage onNavigateToContacts={handleNavigateToContacts} onNavigateToPlay={handleNavigateToPlay} onNavigateToAuth={handleNavigateToAuth} onNavigateToGame={handleNavigateToGame} />
</main> </main>
<Footer /> <Footer />
</div> </div>
@@ -0,0 +1,96 @@
import React, { useEffect, useRef, useState } from "react"
import { useNavigate, useLocation } from "react-router-dom"
import Navbar from "../../components/Navbar/Navbar"
import Background from "../../assets/backgrounds/Background.jsx"
import useRequireAuth from "../../hooks/useRequireAuth"
const Lobby = () => {
const [visible, setVisible] = useState(false)
const sectionRef = useRef(null)
const navigate = useNavigate()
const location = useLocation()
const [user, setUser] = useRequireAuth()
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setVisible(true)
},
{ threshold: 0.3 }
)
if (sectionRef.current) observer.observe(sectionRef.current)
return () => observer.disconnect()
}, [])
const handleExit = () => {
if (window.confirm("Biztosan ki szeretnél lépni a lobbyból?")) {
navigate("/home")
}
}
const getInitials = (name) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.slice(0, 2)
.toUpperCase()
}
return (
<div className="flex flex-col min-h-screen overflow-y-auto relative">
<div className="fixed top-0 left-0 w-full h-full -z-10">
<Background />
</div>
<div className="fixed top-0 left-0 right-0 z-30">
<Navbar />
</div>
<main className="flex-grow text-white px-6 pt-16 mt-0 mb-20 flex items-center justify-center">
<section
ref={sectionRef}
className={`w-full max-w-3xl rounded-2xl p-8 md:p-10 transition-all duration-1000 ease-out backdrop-blur-md shadow-2xl ${
visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
}`}
style={{ background: "rgba(0,0,0,0.25)" }}
>
<h1 className="text-4xl md:text-5xl font-extrabold text-green-300 mb-4 text-center tracking-wide drop-shadow-lg">
{user} Lobby-ja
</h1>
<p className="text-lg text-zinc-300 mb-8 text-center">
Játékosok, akik csatlakoztak ehhez a szobához:
</p>
<div className="bg-zinc-800/90 rounded-xl shadow-lg p-6 mb-8">
<ul className="flex flex-col gap-4">
<li className="bg-zinc-700 py-3 px-4 rounded-xl text-green-400 font-semibold flex items-center gap-4 shadow hover:shadow-green-500/20 transition">
<div
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold"
style={{ background: "rgba(34,197,94,0.12)", color: "var(--color-mint)" }}
>
{getInitials(user)}
</div>
<span className="text-white text-lg">{user}</span>
</li>
</ul>
</div>
<div className="flex justify-center">
<button
onClick={handleExit}
className="bg-gradient-to-r from-green-700 to-green-500 hover:from-green-600 hover:to-green-400 text-white px-8 py-3 rounded-xl font-semibold shadow-lg hover:shadow-green-400/30 transition-transform transform hover:scale-105"
>
Kilépés
</button>
</div>
</section>
</main>
</div>
)
}
export default Lobby
@@ -4,8 +4,11 @@ import Navbar from "../../components/Navbar/Navbar.jsx"
import Footer from "../../components/Footer/Footer.jsx" import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx" import Background from "../../assets/backgrounds/Background.jsx"
import { getUserStats } from "../../api/userApi.js" import { getUserStats } from "../../api/userApi.js"
import useRequireAuth from "../../hooks/useRequireAuth.jsx"
export default function Reports() { export default function Reports() {
const [username] = useRequireAuth({ key: "username", redirectTo: "/login" })
return ( return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden"> <div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
{/* Háttér */} {/* Háttér */}
@@ -24,9 +27,8 @@ export default function Reports() {
{/* Fejléc */} {/* Fejléc */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<h2 className="text-3xl font-bold text-white">Játék Riportok</h2> <h2 className="text-3xl font-bold text-white">Játék Riportok</h2>
<p className="text-gray-300 mt-2"> <p className="text-gray-300 mt-2">Áttekintés a legutóbbi játékokról és statisztikákról</p>
Áttekintés a legutóbbi játékokról és statisztikákról {username && <p className="text-sm text-gray-400 mt-1">Bejelentkezett: {username}</p>}
</p>
</div> </div>
{/* Statisztikai kártyák */} {/* Statisztikai kártyák */}
@@ -6,7 +6,7 @@ import Logo from "../../assets/pictures/Logo.jsx"
import Navbar from "../../components/Navbar/Navbar" import Navbar from "../../components/Navbar/Navbar"
import Footer from "../../components/Footer/Footer.jsx" import Footer from "../../components/Footer/Footer.jsx"
import UserProfile from "../../components/Userdetails/Userdetails.jsx" import UserProfile from "../../components/Userdetails/Userdetails.jsx"
import CompanyHub from "../Companies/Companies.jsx" import CompanyHub from "../Contacts/Contacts.jsx"
import RatingSet from "../../components/PopUp/RatingSet" // <- statisztikai komponens import RatingSet from "../../components/PopUp/RatingSet" // <- statisztikai komponens
+97 -65
View File
@@ -1,74 +1,103 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from "react"
import { dotPositions } from "./diceDotPositions"
const Dice = ({ onRoll }) => { const Dice = ({ onRoll, selectedValue }) => {
const [diceValue, setDiceValue] = useState(1); const [diceValue, setDiceValue] = useState(1)
const [isRolling, setIsRolling] = useState(false); const [isRolling, setIsRolling] = useState(false)
const animationRef = useRef(null); const animationRef = useRef(null)
const rollTimeoutRef = useRef(null); const rollTimeoutRef = useRef(null)
const diceFaces = [ const diceFaces = [
[<div key="center" className="dice-dot"></div>], [<div key="center" className="dice-dot" style={dotPositions.center}></div>],
[<div key="top-left" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot"></div>], [
[<div key="top-left" className="dice-dot"></div>, <div key="center" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot"></div>], <div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>,
[<div key="top-left" className="dice-dot"></div>, <div key="top-right" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
<div key="bottom-left" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot"></div>], ],
[<div key="top-left" className="dice-dot"></div>, <div key="top-right" className="dice-dot"></div>, [
<div key="center" className="dice-dot"></div>, <div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>,
<div key="bottom-left" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot"></div>], <div key="center" className="dice-dot" style={dotPositions.center}></div>,
[<div key="top-left" className="dice-dot"></div>, <div key="top-right" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
<div key="middle-left" className="dice-dot"></div>, <div key="middle-right" className="dice-dot"></div>, ],
<div key="bottom-left" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot"></div>] [
]; <div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>,
<div key="top-right" className="dice-dot" style={dotPositions.topRight}></div>,
<div key="bottom-left" className="dice-dot" style={dotPositions.bottomLeft}></div>,
<div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
],
[
<div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>,
<div key="top-right" className="dice-dot" style={dotPositions.topRight}></div>,
<div key="center" className="dice-dot" style={dotPositions.center}></div>,
<div key="bottom-left" className="dice-dot" style={dotPositions.bottomLeft}></div>,
<div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
],
[
<div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>,
<div key="top-right" className="dice-dot" style={dotPositions.topRight}></div>,
<div key="middle-left" className="dice-dot" style={dotPositions.middleLeft}></div>,
<div key="middle-right" className="dice-dot" style={dotPositions.middleRight}></div>,
<div key="bottom-left" className="dice-dot" style={dotPositions.bottomLeft}></div>,
<div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
],
]
useEffect(() => { useEffect(() => {
return () => { return () => {
if (animationRef.current) cancelAnimationFrame(animationRef.current); if (animationRef.current) cancelAnimationFrame(animationRef.current)
if (rollTimeoutRef.current) clearTimeout(rollTimeoutRef.current); if (rollTimeoutRef.current) clearTimeout(rollTimeoutRef.current)
}; }
}, []); }, [])
const rollDice = () => { // Helper that starts the rolling animation and finishes with targetValue if provided
if (isRolling) return; const startRoll = (targetValue = null) => {
if (isRolling) return
setIsRolling(true); setIsRolling(true)
let duration = 0; let duration = 0
const rollInterval = 80; // ms between dice face changes const rollInterval = 80 // ms between dice face changes
const maxDuration = 1500; // total animation time const maxDuration = 1500 // total animation time
const rollAnimation = () => { const rollAnimation = () => {
const randomValue = Math.floor(Math.random() * 6) + 1; const randomValue = Math.floor(Math.random() * 6) + 1
setDiceValue(randomValue); setDiceValue(randomValue)
duration += rollInterval; duration += rollInterval
if (duration < maxDuration) { if (duration < maxDuration) {
// Speed effect: slow down towards the end // Speed effect: slow down towards the end
const nextInterval = rollInterval * (1 + (duration / maxDuration) * 2); const nextInterval = rollInterval * (1 + (duration / maxDuration) * 2)
rollTimeoutRef.current = setTimeout(() => { rollTimeoutRef.current = setTimeout(() => {
animationRef.current = requestAnimationFrame(rollAnimation); animationRef.current = requestAnimationFrame(rollAnimation)
}, nextInterval); }, nextInterval)
} else { } else {
// Final roll // Final roll (use targetValue if provided)
const finalValue = Math.floor(Math.random() * 6) + 1; const finalValue = targetValue != null ? Number(targetValue) : Math.floor(Math.random() * 6) + 1
setDiceValue(finalValue); setDiceValue(finalValue)
setIsRolling(false); setIsRolling(false)
if (onRoll) onRoll(finalValue); if (onRoll) onRoll(finalValue)
}
} }
};
animationRef.current = requestAnimationFrame(rollAnimation); animationRef.current = requestAnimationFrame(rollAnimation)
}; }
// Click to roll randomly
const rollDice = () => {
startRoll(null)
}
// If parent provides a selectedValue, animate to that value
useEffect(() => {
if (selectedValue != null) {
startRoll(Number(selectedValue))
}
}, [selectedValue])
return ( return (
<div <div className={`dice-container ${isRolling ? "rolling" : ""}`} onClick={rollDice}>
className={`dice-container ${isRolling ? 'rolling' : ''}`} <div className="dice">{diceFaces[diceValue - 1]}</div>
onClick={rollDice}
>
<div className="dice">
{diceFaces[diceValue - 1]}
</div>
<style jsx>{` <style jsx>{`
.dice-container { .dice-container {
width: 80px; width: 80px;
@@ -107,11 +136,21 @@ const Dice = ({ onRoll }) => {
} }
@keyframes roll { @keyframes roll {
0% { transform: rotateX(0deg) rotateY(0deg); } 0% {
25% { transform: rotateX(90deg) rotateY(45deg); } transform: rotateX(0deg) rotateY(0deg);
50% { transform: rotateX(180deg) rotateY(90deg); } }
75% { transform: rotateX(270deg) rotateY(135deg); } 25% {
100% { transform: rotateX(360deg) rotateY(180deg); } transform: rotateX(90deg) rotateY(45deg);
}
50% {
transform: rotateX(180deg) rotateY(90deg);
}
75% {
transform: rotateX(270deg) rotateY(135deg);
}
100% {
transform: rotateX(360deg) rotateY(180deg);
}
} }
.dice-dot { .dice-dot {
@@ -123,17 +162,10 @@ const Dice = ({ onRoll }) => {
box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.3); box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.3);
} }
/* Positioning dots */ /* removed :nth-child positioning — positions are provided inline from diceDotPositions.js */
.dice-dot:nth-child(1) { top: 50%; left: 50%; transform: translate(-50%, -50%); } /* Center */
.dice-dot:nth-child(2) { top: 20%; left: 20%; } /* Top-left */
.dice-dot:nth-child(3) { bottom: 20%; right: 20%; } /* Bottom-right */
.dice-dot:nth-child(4) { top: 20%; right: 20%; } /* Top-right */
.dice-dot:nth-child(5) { bottom: 20%; left: 20%; } /* Bottom-left */
.dice-dot:nth-child(6) { top: 50%; left: 20%; transform: translateY(-50%); } /* Middle-left */
.dice-dot:nth-child(7) { top: 50%; right: 20%; transform: translateY(-50%); } /* Middle-right */
`}</style> `}</style>
</div> </div>
); )
}; }
export default Dice; export default Dice
@@ -0,0 +1,9 @@
export const dotPositions = {
center: { top: "50%", left: "50%", transform: "translate(-50%, -50%)" },
topLeft: { top: "20%", left: "20%" },
bottomRight: { bottom: "20%", right: "20%" },
topRight: { top: "20%", right: "20%" },
bottomLeft: { bottom: "20%", left: "20%" },
middleLeft: { top: "50%", left: "20%", transform: "translateY(-50%)" },
middleRight: { top: "50%", right: "20%", transform: "translateY(-50%)" },
}
+12
View File
@@ -18,6 +18,7 @@ if %errorlevel% neq 0 (
if "%1"=="dev:start" goto dev_start if "%1"=="dev:start" goto dev_start
if "%1"=="dev:watch" goto dev_watch if "%1"=="dev:watch" goto dev_watch
if "%1"=="dev:watch_nat" goto dev_watch_nat
if "%1"=="dev:stop" goto dev_stop if "%1"=="dev:stop" goto dev_stop
if "%1"=="prod:start" goto prod_start if "%1"=="prod:start" goto prod_start
if "%1"=="prod:stop" goto prod_stop if "%1"=="prod:stop" goto prod_stop
@@ -49,6 +50,16 @@ docker-compose -f docker-compose.watch.yml --env-file .env.dev up --build --watc
cd .. cd ..
goto end goto end
:dev_watch_nat
echo [INFO] Starting SerpentRace development environment with file watchers without nat...
echo [INFO] This will automatically sync file changes and rebuild containers as needed
cd SerpentRace_Docker
echo [INFO] This will use system network to avoid nat
docker-compose -f docker-compose.watch.nat.yml --env-file .env.dev up --build --watch
cd ..
goto end
:dev_stop :dev_stop
echo [INFO] Stopping SerpentRace development environment... echo [INFO] Stopping SerpentRace development environment...
cd SerpentRace_Docker cd SerpentRace_Docker
@@ -109,6 +120,7 @@ echo.
echo Commands: echo Commands:
echo dev:start Start development environment with hot reload echo dev:start Start development environment with hot reload
echo dev:watch Start development environment with file watchers (auto-rebuild) echo dev:watch Start development environment with file watchers (auto-rebuild)
echo dev:watch_nat Start development environment with file watchers (auto-rebuild); Without NAT
echo dev:stop Stop development environment echo dev:stop Stop development environment
echo prod:start Start production environment echo prod:start Start production environment
echo prod:stop Stop production environment echo prod:stop Stop production environment
Regular → Executable
View File
+11
View File
@@ -0,0 +1,11 @@
kapcs fel routing
navbar széthúz
footer kapcsolat
navabar gomboksorrend
vagy kontat vagy kapcsolat
navbar bejelent
navbar layout finomít
deck lista kialakít köv oldal max stb
palki info get
deck hibák javítása