23 Commits

Author SHA1 Message Date
mategergely33 322059ace0 telos nezet landin, home, navbar, footer 2025-11-13 18:54:06 +01:00
mategergely33 0ac5ead63a joker/szerencse kartyak kezelese 2025-11-11 19:00:14 +01:00
mategergely33 63533c0313 adatkonzisztencia leirasa 2025-11-04 17:36:03 +01:00
mategergely33 1af7bdc3f0 kartya inspect 2025-10-31 17:38:14 +01:00
Donat 129ea694f8 Merge pull request 'game workflow corrected' (#84) from Backend_Fix into main
Reviewed-on: #84
2025-10-30 18:40:36 +00:00
magdo 9f3a5b6fd7 game workflow corrected 2025-10-30 19:39:41 +01:00
Donat 79786d8bb1 Merge pull request 'szerkesztes jog megoldva+ Frontend' (#83) from decksetting into main
Reviewed-on: #83
2025-10-30 18:20:20 +00:00
zsola03 f8917f6862 szerkesztes jog megoldva+ Frontend 2025-10-30 19:17:45 +01:00
Donat 384456ffd3 Merge pull request 'game workflow corrected' (#82) from Backend_Fix into main
Reviewed-on: #82
2025-10-30 17:43:54 +00:00
Donat 3c85fd72ef Merge pull request 'javitasok-plusz' (#81) from javitasok-plusz into main
Reviewed-on: #81
2025-10-30 17:43:40 +00:00
magdo 6065ab2800 game workflow corrected 2025-10-30 18:43:16 +01:00
GitG0r0 bfcdd3ec9d Deck törlés funkció implementálása modal-lal és consequence értékek finomítása 2025-10-30 18:25:25 +01:00
GitG0r0 46369ed112 Merge remote changes into javitasok-plusz 2025-10-27 21:01:57 +01:00
Donat d915a7fe1c Merge pull request '"activate user admin"' (#80) from Backend_Fix into main
Reviewed-on: #80
2025-10-27 19:35:56 +00:00
magdo 99ed8fea54 "activate user admin" 2025-10-27 20:35:07 +01:00
Donat a818d49701 Merge pull request 'fixes' (#79) from Backend_Fix into main
Reviewed-on: #79
2025-10-27 19:22:59 +00:00
magdo 04954cec4a fixes 2025-10-27 20:22:39 +01:00
Donat dbe06c5c0c Merge pull request 'landing navbar footer javítás' (#78) from ujbarni into main
Reviewed-on: #78
2025-10-27 19:05:33 +00:00
Barni 8ce04afe8b landing navbar footer javítás 2025-10-27 19:36:03 +01:00
Donat e21980d07d Merge pull request 'navbar meg merge' (#77) from navbar+aboutű into main
Reviewed-on: #77
2025-10-27 18:07:22 +00:00
Walke 39e0d36a7f navbar meg merge 2025-10-27 18:55:54 +01:00
GitG0r0 d3dcb7f7da fix: Consequence kezelés és deck szerkesztés javítások
- TaskCardEditor és JokerCardEditor: consequence mezők eltávolítása (csak LuckCardEditor-nél marad)
- DeckCreator: kártya type konverzió javítása betöltéskor (szám -> string)
- DeckCreator: csak megfelelő típusú kártyák megtartása mentéskor
- UpdateDeckCommandHandler: userstate -> authLevel javítás (interface mező helyesen)
- sql_schema_only.sql: trigger függvény javítása (NEW.updatedate -> NEW.update_date)
2025-10-27 18:27:40 +01:00
Donat d0741c273f Merge pull request 'Backend_Fix' (#76) from Backend_Fix into main
Reviewed-on: #76
2025-10-26 22:59:00 +00:00
41 changed files with 6358 additions and 792 deletions
File diff suppressed because it is too large Load Diff
@@ -141,32 +141,32 @@ router.get('/users/:userId',
}); });
// Search users including soft-deleted ones // Search users including soft-deleted ones
// router.get('/users/search/:searchTerm', router.get('/users/search/:searchTerm',
// adminRequired, adminRequired,
// ValidationMiddleware.validateStringLength({ searchTerm: { min: 2, max: 100 } }), ValidationMiddleware.validateStringLength({ searchTerm: { min: 2, max: 100 } }),
// async (req: Request, res: Response) => { async (req: Request, res: Response) => {
// try { try {
// const { searchTerm } = req.params; const { searchTerm } = req.params;
// const includeDeleted = req.query.includeDeleted === 'true'; const includeDeleted = req.query.includeDeleted === 'true';
// logRequest('Admin search users endpoint accessed', req, res, { searchTerm, includeDeleted }); logRequest('Admin search users endpoint accessed', req, res, { searchTerm, includeDeleted });
// const users = includeDeleted const users = includeDeleted
// ? await container.userRepository.searchIncludingDeleted(searchTerm) ? await container.userRepository.searchIncludingDeleted(searchTerm)
// : await container.userRepository.search(searchTerm); : await container.userRepository.search(searchTerm);
// logRequest('Admin user search completed', req, res, { logRequest('Admin user search completed', req, res, {
// searchTerm, searchTerm,
// resultCount: Array.isArray(users) ? users.length : (users.totalCount || 0), resultCount: Array.isArray(users) ? users.length : (users.totalCount || 0),
// includeDeleted includeDeleted
// }); });
// res.json(users); res.json(users);
// } catch (error) { } catch (error) {
// logError('Admin search users endpoint error', error as Error, req, res); logError('Admin search users endpoint error', error as Error, req, res);
// res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
// } }
// }); });
// Update any user (admin only) // Update any user (admin only)
router.patch('/users/:userId', router.patch('/users/:userId',
@@ -213,6 +213,32 @@ router.patch('/users/:userId',
} }
}); });
// Activate user (admin only)
router.post('/users/:userId/activate',
adminRequired,
ValidationMiddleware.validateUUIDFormat(['userId']),
async (req: Request, res: Response) => {
try {
const targetUserId = req.params.userId;
const adminUserId = (req as any).user.userId;
logRequest('Admin activate user endpoint accessed', req, res, { adminUserId, targetUserId });
const result = await container.activateUserCommandHandler.execute({ id: targetUserId });
if (!result) {
return res.status(404).json({ error: 'User not found' });
}
logAuth('User activated by admin', targetUserId, { adminUserId }, req, res);
res.json({ message: 'User activated successfully', user: result });
} catch (error) {
logError('Admin activate user endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Deactivate user (admin only) // Deactivate user (admin only)
router.post('/users/:userId/deactivate', router.post('/users/:userId/deactivate',
adminRequired, adminRequired,
@@ -1,7 +1,6 @@
import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate'; import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate';
import { UserAggregate } from '../../../Domain/User/UserAggregate'; import { UserAggregate } from '../../../Domain/User/UserAggregate';
import { CreateDeckDto, UpdateDeckDto, ShortDeckDto, DetailDeckDto } from '../DeckDto'; import { CreateDeckDto, UpdateDeckDto, ShortDeckDto, DetailDeckDto } from '../DeckDto';
import e from 'express';
export class DeckMapper { export class DeckMapper {
static toShortDto(deck: DeckAggregate, userId?: string): ShortDeckDto { static toShortDto(deck: DeckAggregate, userId?: string): ShortDeckDto {
@@ -14,7 +13,7 @@ export class DeckMapper {
cardCount: deck.cards.length, cardCount: deck.cards.length,
creator: deck.user?.username || 'Unknown', creator: deck.user?.username || 'Unknown',
creationdate: deck.creationdate, creationdate: deck.creationdate,
editable: deck.isEditable() ? deck.isEditable()(userId!) : undefined editable: deck.isEditable(userId!) ? deck.isEditable(userId!) : undefined
}; };
} }
@@ -41,7 +40,7 @@ export class DeckMapper {
cardCount: deck.cards.length, cardCount: deck.cards.length,
creator: deck.user?.username || 'Unknown', creator: deck.user?.username || 'Unknown',
creationdate: deck.creationdate, creationdate: deck.creationdate,
editable: deck.isEditable() ? deck.isEditable()(userId!) : undefined editable: deck.isEditable(userId!) ? deck.isEditable(userId!) : undefined
})); }));
} }
} }
@@ -22,7 +22,7 @@ export class OrganizationMapper {
contactemail: org.contactemail, contactemail: org.contactemail,
state: org.state, state: org.state,
regdate: org.regdate, regdate: org.regdate,
updatedate: org.updatedate, updateDate: org.updateDate,
url: org.url, url: org.url,
userinorg: org.userinorg, userinorg: org.userinorg,
maxOrganizationalDecks: org.maxOrganizationalDecks, maxOrganizationalDecks: org.maxOrganizationalDecks,
@@ -27,7 +27,7 @@ export interface DetailOrganizationDto {
contactemail: string; contactemail: string;
state: number; state: number;
regdate: Date; regdate: Date;
updatedate: Date; updateDate: Date;
url: string | null; url: string | null;
userinorg: number; userinorg: number;
maxOrganizationalDecks: number | null; maxOrganizationalDecks: number | null;
@@ -9,12 +9,12 @@ export class UpdateDeckCommandHandler {
constructor(private readonly deckRepo: IDeckRepository) {} constructor(private readonly deckRepo: IDeckRepository) {}
async execute(cmd: UpdateDeckCommand): Promise<ShortDeckDto | null> { async execute(cmd: UpdateDeckCommand): Promise<ShortDeckDto | null> {
if(cmd.state !== undefined && cmd.userstate!==1) { if(cmd.state !== undefined && cmd.authLevel !== 1) {
throw new Error('Only admin users can change deck state'); throw new Error('Only admin users can change deck state');
} }
try { try {
let existingDeck: DeckAggregate | null = null; let existingDeck: DeckAggregate | null = null;
if (cmd.userstate === 1) { if (cmd.authLevel === 1) {
existingDeck = await this.deckRepo.findByIdIncludingDeleted(cmd.id); existingDeck = await this.deckRepo.findByIdIncludingDeleted(cmd.id);
} else { } else {
existingDeck = await this.deckRepo.findById(cmd.id); existingDeck = await this.deckRepo.findById(cmd.id);
@@ -13,13 +13,33 @@ export interface CloserAnswer {
percent: number; percent: number;
} }
/**
* Sentence pair for matching left to right
*/
export interface SentencePair {
id: string; // Unique identifier for this pair
left: string; // Left part to match
right: string; // Right part (scrambled position)
}
/**
* Player's answer for sentence pairing (array of matches)
*/
export interface SentencePairingAnswer {
pairId: string; // ID of the pair
leftText: string; // Left part
rightText: string; // Player's chosen right part
}
export interface CardClientData { export interface CardClientData {
cardid: string; cardid: string;
question: string; question: string;
type: CardType; type: CardType;
timeLimit: number;
// Type-specific client data // Type-specific client data
options?: QuizOption[]; // For QUIZ answerOptions?: QuizOption[]; // For QUIZ
words?: string[]; // For SENTENCE_PAIRING (scrambled) words?: string[]; // For SENTENCE_PAIRING (legacy scrambled words)
sentencePairs?: SentencePair[]; // For SENTENCE_PAIRING (left-right matching)
acceptableAnswers?: string[]; // For OWN_ANSWER (not sent to client) acceptableAnswers?: string[]; // For OWN_ANSWER (not sent to client)
// CLOSER and TRUE_FALSE send only question // CLOSER and TRUE_FALSE send only question
} }
@@ -50,7 +70,8 @@ export class CardProcessingService {
const baseData: CardClientData = { const baseData: CardClientData = {
cardid: card.cardid, cardid: card.cardid,
question: card.question, question: card.question,
type: card.type type: card.type,
timeLimit: 60 // Default 60 seconds for question cards
}; };
switch (card.type) { switch (card.type) {
@@ -116,18 +137,50 @@ export class CardProcessingService {
return { return {
...baseData, ...baseData,
options: card.answer as QuizOption[] answerOptions: card.answer as QuizOption[]
}; };
} }
/** /**
* Prepare SENTENCE_PAIRING card with scrambled words * Prepare SENTENCE_PAIRING card with scrambled left/right pairs
*
* Expected card.answer format:
* [
* { left: "Apple", right: "Red" },
* { left: "Banana", right: "Yellow" },
* { left: "Orange", right: "Orange color" }
* ]
*
* OR legacy string format: "word1 word2 word3" (will be split and scrambled)
*/ */
private prepareSentencePairingCard(card: GameCard, baseData: CardClientData): CardClientData { private prepareSentencePairingCard(card: GameCard, baseData: CardClientData): CardClientData {
if (typeof card.answer !== 'string') { // NEW FORMAT: Array of pairs (left-right matching)
throw new Error('Sentence pairing card answer must be a string'); if (Array.isArray(card.answer)) {
// Validate structure
const pairs = card.answer as Array<{ left: string; right: string }>;
if (!pairs.every(p => p.left && p.right)) {
throw new Error('Sentence pairing card answer must be array of {left, right} objects');
} }
// Create pairs with IDs and scramble the right parts
const leftParts = pairs.map((p, idx) => ({ id: `pair_${idx}`, left: p.left, right: p.right }));
const rightParts = this.scrambleArray([...pairs.map(p => p.right)]);
// Send left parts in order, right parts scrambled
const sentencePairs: SentencePair[] = leftParts.map((lp, idx) => ({
id: lp.id,
left: lp.left,
right: rightParts[idx] // Scrambled position
}));
return {
...baseData,
sentencePairs
};
}
// LEGACY FORMAT: Single sentence to reconstruct (backward compatibility)
if (typeof card.answer === 'string') {
const words = card.answer.split(' ').filter(word => word.trim() !== ''); const words = card.answer.split(' ').filter(word => word.trim() !== '');
const scrambledWords = this.scrambleArray([...words]); const scrambledWords = this.scrambleArray([...words]);
@@ -137,6 +190,9 @@ export class CardProcessingService {
}; };
} }
throw new Error('Sentence pairing card answer must be array of pairs or string');
}
/** /**
* Prepare OWN_ANSWER card (only question, acceptable answers hidden) * Prepare OWN_ANSWER card (only question, acceptable answers hidden)
*/ */
@@ -187,17 +243,65 @@ export class CardProcessingService {
} }
/** /**
* Validate SENTENCE_PAIRING answer (reconstructed sentence) * Validate SENTENCE_PAIRING answer
*
* Supports two formats:
* 1. NEW: Array of { pairId, leftText, rightText } matches
* 2. LEGACY: Reconstructed sentence string or array of words
*/ */
private validateSentencePairingAnswer(card: GameCard, playerAnswer: string[] | string): CardValidationResult { private validateSentencePairingAnswer(card: GameCard, playerAnswer: any): CardValidationResult {
if (typeof card.answer !== 'string') { // NEW FORMAT: Array of pairs (left-right matching)
throw new Error('Sentence pairing card answer must be a string'); if (Array.isArray(card.answer) && card.answer.every((p: any) => p.left && p.right)) {
const correctPairs = card.answer as Array<{ left: string; right: string }>;
// Player answer should be array of SentencePairingAnswer objects
if (!Array.isArray(playerAnswer)) {
throw new Error('Player answer must be array of pair matches');
} }
const playerMatches = playerAnswer as SentencePairingAnswer[];
// Check if all pairs match correctly
let correctCount = 0;
const results: string[] = [];
for (const correctPair of correctPairs) {
const playerMatch = playerMatches.find(pm =>
pm.leftText.toLowerCase().trim() === correctPair.left.toLowerCase().trim()
);
if (playerMatch) {
const isMatch = playerMatch.rightText.toLowerCase().trim() ===
correctPair.right.toLowerCase().trim();
if (isMatch) {
correctCount++;
results.push(`✓ "${correctPair.left}" → "${correctPair.right}"`);
} else {
results.push(`✗ "${correctPair.left}" → "${playerMatch.rightText}" (should be "${correctPair.right}")`);
}
} else {
results.push(`✗ "${correctPair.left}" → (not matched)`);
}
}
const isCorrect = correctCount === correctPairs.length;
return {
isCorrect,
submittedAnswer: playerMatches,
correctAnswer: correctPairs,
explanation: isCorrect
? `✅ Perfect! All ${correctCount} pairs matched correctly!\n${results.join('\n')}`
: `❌ Only ${correctCount}/${correctPairs.length} pairs correct:\n${results.join('\n')}`
};
}
// LEGACY FORMAT: Single sentence to reconstruct (backward compatibility)
if (typeof card.answer === 'string') {
// Handle both array of words and joined string // Handle both array of words and joined string
const reconstructed = Array.isArray(playerAnswer) const reconstructed = Array.isArray(playerAnswer)
? playerAnswer.join(' ').toLowerCase().trim() ? playerAnswer.join(' ').toLowerCase().trim()
: playerAnswer.toLowerCase().trim(); : (typeof playerAnswer === 'string' ? playerAnswer.toLowerCase().trim() : '');
const correctSentence = card.answer.toLowerCase().trim(); const correctSentence = card.answer.toLowerCase().trim();
const isCorrect = reconstructed === correctSentence; const isCorrect = reconstructed === correctSentence;
@@ -212,6 +316,9 @@ export class CardProcessingService {
}; };
} }
throw new Error('Sentence pairing card answer must be array of pairs or string');
}
/** /**
* Validate OWN_ANSWER (check against acceptable answers array) * Validate OWN_ANSWER (check against acceptable answers array)
*/ */
@@ -39,6 +39,7 @@ import { ProcessOrgAuthCallbackCommandHandler } from '../Organization/commands/P
import { CreateContactCommandHandler } from '../Contact/commands/CreateContactCommandHandler'; import { CreateContactCommandHandler } from '../Contact/commands/CreateContactCommandHandler';
import { UpdateContactCommandHandler } from '../Contact/commands/UpdateContactCommandHandler'; import { UpdateContactCommandHandler } from '../Contact/commands/UpdateContactCommandHandler';
import { DeleteContactCommandHandler } from '../Contact/commands/DeleteContactCommandHandler'; import { DeleteContactCommandHandler } from '../Contact/commands/DeleteContactCommandHandler';
import { ActivateUserCommandHandler } from '../User/commands/ActivateUserCommandHandler';
// Query Handlers // Query Handlers
import { GetUserByIdQueryHandler } from '../User/queries/GetUserByIdQueryHandler'; import { GetUserByIdQueryHandler } from '../User/queries/GetUserByIdQueryHandler';
@@ -121,6 +122,7 @@ export class DIContainer {
private _updateContactCommandHandler: UpdateContactCommandHandler | null = null; private _updateContactCommandHandler: UpdateContactCommandHandler | null = null;
private _deleteContactCommandHandler: DeleteContactCommandHandler | null = null; private _deleteContactCommandHandler: DeleteContactCommandHandler | null = null;
private _generateBoardCommandHandler: GenerateBoardCommandHandler | null = null; private _generateBoardCommandHandler: GenerateBoardCommandHandler | null = null;
private _activateUserCommandHandler: ActivateUserCommandHandler | null = null;
// Query Handlers // Query Handlers
private _getUserByIdQueryHandler: GetUserByIdQueryHandler | null = null; private _getUserByIdQueryHandler: GetUserByIdQueryHandler | null = null;
@@ -306,6 +308,13 @@ export class DIContainer {
return this._deactivateUserCommandHandler; return this._deactivateUserCommandHandler;
} }
public get activateUserCommandHandler(): ActivateUserCommandHandler {
if (!this._activateUserCommandHandler) {
this._activateUserCommandHandler = new ActivateUserCommandHandler(this.userRepository);
}
return this._activateUserCommandHandler;
}
public get deleteUserCommandHandler(): DeleteUserCommandHandler { public get deleteUserCommandHandler(): DeleteUserCommandHandler {
if (!this._deleteUserCommandHandler) { if (!this._deleteUserCommandHandler) {
this._deleteUserCommandHandler = new DeleteUserCommandHandler(this.userRepository); this._deleteUserCommandHandler = new DeleteUserCommandHandler(this.userRepository);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
export interface ActivateUserCommand {
id: string;
}
@@ -0,0 +1,12 @@
import { IUserRepository } from '../../../Domain/IRepository/IUserRepository';
import { ActivateUserCommand } from './ActivateUserCommand';
export class ActivateUserCommandHandler {
constructor(private readonly userRepo: IUserRepository) {}
async execute(cmd: ActivateUserCommand): Promise<boolean> {
await this.userRepo.activate(cmd.id);
return true;
}
}
@@ -86,7 +86,7 @@ export class LogoutCommandHandler {
// 5. Update user's last logout timestamp in database // 5. Update user's last logout timestamp in database
try { try {
const updateResult = await this.userRepo.update(userId, { updatedate: new Date() }); const updateResult = await this.userRepo.update(userId, { updateDate: new Date() });
if (updateResult) { if (updateResult) {
logAuth('User last logout timestamp updated', userId); logAuth('User last logout timestamp updated', userId);
} }
@@ -151,7 +151,7 @@ export class LogoutCommandHandler {
} }
// Update user logout timestamp // Update user logout timestamp
await this.userRepo.update(userId, { updatedate: new Date() }); await this.userRepo.update(userId, { updateDate: new Date() });
logAuth('User logged out from all devices', userId); logAuth('User logged out from all devices', userId);
return true; return true;
@@ -74,8 +74,8 @@ export class DeckAggregate {
@Column({ type: 'int', default: CType.PUBLIC }) @Column({ type: 'int', default: CType.PUBLIC })
ctype!: CType; ctype!: CType;
@UpdateDateColumn({ name: 'update_date' }) @UpdateDateColumn()
updatedate!: Date; updateDate!: Date;
@Column({ type: 'int', default: State.ACTIVE }) @Column({ type: 'int', default: State.ACTIVE })
state!: State; state!: State;
@@ -88,10 +88,16 @@ export class DeckAggregate {
@JoinColumn({ name: 'user_id' }) @JoinColumn({ name: 'user_id' })
user!: UserAggregate | null; user!: UserAggregate | null;
isEditable() { isEditable(userId:string): boolean{
// A deck is editable if the user is the creator // A deck is editable if the user is the creator
return (userId: string) => { if (!this.user) {
return this.user?.id.toString() === userId; logError(`DeckAggregate.isEditable: User is null for deck id ${this.id}`);
}; return false;
}
//if admin, always editable
if (this.user?.isAdmin) {
return true;
}
return this.user?.id.toString() === userId;;
} }
} }
@@ -86,8 +86,8 @@ export class GameAggregate {
@Column({ type: 'timestamp', nullable: true, name: 'finishDate' }) @Column({ type: 'timestamp', nullable: true, name: 'finishDate' })
enddate!: Date | null; enddate!: Date | null;
@UpdateDateColumn({ name: 'updateDate' }) @UpdateDateColumn()
updatedate!: Date; updateDate!: Date;
} }
// Board Generation Types // Board Generation Types
@@ -7,4 +7,5 @@ export interface IUserRepository extends IPaginatedRepository<UserAggregate, { u
findByEmail(email: string): Promise<UserAggregate | null>; findByEmail(email: string): Promise<UserAggregate | null>;
findByToken(token: string): Promise<UserAggregate | null>; findByToken(token: string): Promise<UserAggregate | null>;
deactivate(id: string): Promise<UserAggregate | null>; deactivate(id: string): Promise<UserAggregate | null>;
activate(id: string): Promise<UserAggregate | null>;
} }
@@ -35,8 +35,8 @@ export class OrganizationAggregate {
@CreateDateColumn() @CreateDateColumn()
regdate!: Date; regdate!: Date;
@UpdateDateColumn() @UpdateDateColumn({ name: 'updateDate' })
updatedate!: Date; updateDate!: Date;
@Column({ type: 'varchar', length: 500, nullable: true }) @Column({ type: 'varchar', length: 500, nullable: true })
url!: string | null; url!: string | null;
@@ -51,8 +51,12 @@ export class UserAggregate {
regdate!: Date; regdate!: Date;
@UpdateDateColumn() @UpdateDateColumn()
updatedate!: Date; updateDate!: Date;
@Column({ type: 'timestamp', nullable: true }) @Column({ type: 'timestamp', nullable: true })
Orglogindate!: Date | null; Orglogindate!: Date | null;
get isAdmin(): boolean {
return this.state === UserState.ADMIN;
}
} }
@@ -1,10 +1,7 @@
import { MigrationInterface, QueryRunner } from "typeorm"; import { MigrationInterface, QueryRunner } from "typeorm";
<<<<<<<< HEAD:SerpentRace_Backend/src/Infrastructure/Migrationsettings/1758463928499-full.ts
export class Full1758463928499 implements MigrationInterface { export class Full1758463928499 implements MigrationInterface {
========
export class Full1757939815062 implements MigrationInterface {
>>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2:SerpentRace_Backend/src/Infrastructure/Migrationsettings/1757939815062-full.ts
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
} }
@@ -1,10 +1,5 @@
import { MigrationInterface, QueryRunner } from "typeorm"; import { MigrationInterface, QueryRunner } from "typeorm";
<<<<<<<< HEAD:SerpentRace_Backend/src/Infrastructure/Migrationsettings/1758463928499-full.ts
export class Full1758463928499 implements MigrationInterface { export class Full1758463928499 implements MigrationInterface {
========
export class Full1757939815062 implements MigrationInterface {
>>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2:SerpentRace_Backend/src/Infrastructure/Migrationsettings/1757939815062-full.ts
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
} }
@@ -29,7 +29,7 @@ export class DeckRepository implements IDeckRepository {
// Get paginated results // Get paginated results
const decks = await this.repo.find({ const decks = await this.repo.find({
where: { state: Not(State.SOFT_DELETE) }, where: { state: Not(State.SOFT_DELETE) },
order: { updatedate: 'DESC' }, order: { updateDate: 'DESC' },
take: limit, take: limit,
skip: offset skip: offset
}); });
@@ -57,7 +57,7 @@ export class DeckRepository implements IDeckRepository {
// Get paginated results // Get paginated results
const decks = await this.repo.find({ const decks = await this.repo.find({
order: { updatedate: 'DESC' }, order: { updateDate: 'DESC' },
take: limit, take: limit,
skip: offset skip: offset
}); });
@@ -39,7 +39,7 @@ export class GameRepository implements IGameRepository {
// Get paginated results // Get paginated results
const games = await this.repo.find({ const games = await this.repo.find({
where: { state: Not(GameState.CANCELLED) }, where: { state: Not(GameState.CANCELLED) },
order: { updatedate: 'DESC' }, order: { updateDate: 'DESC' },
take: limit, take: limit,
skip: offset skip: offset
}); });
@@ -67,7 +67,7 @@ export class GameRepository implements IGameRepository {
// Get paginated results (including deleted) // Get paginated results (including deleted)
const games = await this.repo.find({ const games = await this.repo.find({
order: { updatedate: 'DESC' }, order: { updateDate: 'DESC' },
take: limit, take: limit,
skip: offset skip: offset
}); });
@@ -153,7 +153,7 @@ export class GameRepository implements IGameRepository {
queryBuilder.skip(offset); queryBuilder.skip(offset);
} }
const games = await queryBuilder.orderBy('game.updatedate', 'DESC').getMany(); const games = await queryBuilder.orderBy('game.updateDate', 'DESC').getMany();
const endTime = performance.now(); const endTime = performance.now();
logDatabase('Game search completed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}, found: ${games.length}, total: ${totalCount}`); logDatabase('Game search completed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}, found: ${games.length}, total: ${totalCount}`);
@@ -184,7 +184,7 @@ export class GameRepository implements IGameRepository {
queryBuilder.skip(offset); queryBuilder.skip(offset);
} }
const games = await queryBuilder.orderBy('game.updatedate', 'DESC').getMany(); const games = await queryBuilder.orderBy('game.updateDate', 'DESC').getMany();
const endTime = performance.now(); const endTime = performance.now();
logDatabase('Game search (including deleted) completed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}, found: ${games.length}, total: ${totalCount}`); logDatabase('Game search (including deleted) completed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}, found: ${games.length}, total: ${totalCount}`);
@@ -251,7 +251,7 @@ export class GameRepository implements IGameRepository {
try { try {
const games = await this.repo.find({ const games = await this.repo.find({
where: { state: GameState.ACTIVE }, where: { state: GameState.ACTIVE },
order: { updatedate: 'DESC' } order: { updateDate: 'DESC' }
}); });
const endTime = performance.now(); const endTime = performance.now();
logDatabase('Active games query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}`); logDatabase('Active games query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}`);
@@ -270,7 +270,7 @@ export class GameRepository implements IGameRepository {
const queryBuilder = this.repo.createQueryBuilder('game') const queryBuilder = this.repo.createQueryBuilder('game')
.where('game.state != :cancelledState', { cancelledState: GameState.CANCELLED }) .where('game.state != :cancelledState', { cancelledState: GameState.CANCELLED })
.andWhere('JSON_CONTAINS(game.players, :playerId)', { playerId: `"${playerId}"` }) .andWhere('JSON_CONTAINS(game.players, :playerId)', { playerId: `"${playerId}"` })
.orderBy('game.updatedate', 'DESC'); .orderBy('game.updateDate', 'DESC');
const games = await queryBuilder.getMany(); const games = await queryBuilder.getMany();
const endTime = performance.now(); const endTime = performance.now();
@@ -345,5 +345,25 @@ export class UserRepository implements IUserRepository {
} }
} }
async activate(id: string) {
const startTime = Date.now();
try {
await this.repo.update(id, { state: UserState.VERIFIED_REGULAR });
const result = await this.findById(id);
logDatabase('User activated successfully', `update(${id}, { state: VERIFIED_REGULAR })`, Date.now() - startTime, {
userId: id,
success: !!result
});
return result;
}
catch (error) {
logError('UserRepository.activate error', error as Error);
// Handle invalid UUID format
if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) {
throw new Error('Invalid user ID format');
}
throw new Error('Failed to activate user in database');
}
}
} }
+3 -37
View File
@@ -19,7 +19,7 @@ CREATE TABLE "Users" (
"phone" VARCHAR(20) NULL, "phone" VARCHAR(20) NULL,
"state" INTEGER NOT NULL DEFAULT 0, "state" INTEGER NOT NULL DEFAULT 0,
"regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"Orglogindate" TIMESTAMP NULL "Orglogindate" TIMESTAMP NULL
); );
@@ -33,7 +33,7 @@ CREATE TABLE "Organizations" (
"contactemail" VARCHAR(255) NOT NULL, "contactemail" VARCHAR(255) NOT NULL,
"state" INTEGER NOT NULL DEFAULT 0, "state" INTEGER NOT NULL DEFAULT 0,
"regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "regdate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"url" VARCHAR(500) NULL, "url" VARCHAR(500) NULL,
"userinorg" INTEGER NOT NULL DEFAULT 0, "userinorg" INTEGER NOT NULL DEFAULT 0,
"maxOrganizationalDecks" INTEGER NULL "maxOrganizationalDecks" INTEGER NULL
@@ -49,7 +49,7 @@ CREATE TABLE "Decks" (
"cards" JSONB NOT NULL DEFAULT '[]', "cards" JSONB NOT NULL DEFAULT '[]',
"played_number" INTEGER NOT NULL DEFAULT 0, "played_number" INTEGER NOT NULL DEFAULT 0,
"ctype" INTEGER NOT NULL DEFAULT 0, "ctype" INTEGER NOT NULL DEFAULT 0,
"update_date" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "updateDate" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"state" INTEGER NOT NULL DEFAULT 0, "state" INTEGER NOT NULL DEFAULT 0,
"organization_id" UUID NULL "organization_id" UUID NULL
); );
@@ -174,40 +174,6 @@ CREATE INDEX "IDX_Games_State" ON "Games" ("state");
CREATE INDEX "IDX_Games_CreatedBy" ON "Games" ("createdBy"); CREATE INDEX "IDX_Games_CreatedBy" ON "Games" ("createdBy");
CREATE INDEX "IDX_Games_OrganizationId" ON "Games" ("organizationid"); CREATE INDEX "IDX_Games_OrganizationId" ON "Games" ("organizationid");
-- Create update trigger for updatedate columns
CREATE OR REPLACE FUNCTION update_updatedate_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updatedate = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Apply update triggers
CREATE TRIGGER update_users_updatedate
BEFORE UPDATE ON "Users"
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
CREATE TRIGGER update_organizations_updatedate
BEFORE UPDATE ON "Organizations"
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
CREATE TRIGGER update_decks_updatedate
BEFORE UPDATE ON "Decks"
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
CREATE TRIGGER update_chats_updatedate
BEFORE UPDATE ON "Chats"
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
CREATE TRIGGER update_contacts_updatedate
BEFORE UPDATE ON "Contacts"
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
CREATE TRIGGER update_games_updatedate
BEFORE UPDATE ON "Games"
FOR EACH ROW EXECUTE FUNCTION update_updatedate_column();
-- Comments for documentation -- Comments for documentation
COMMENT ON TABLE "Users" IS 'User accounts with authentication and profile information'; COMMENT ON TABLE "Users" IS 'User accounts with authentication and profile information';
COMMENT ON TABLE "Organizations" IS 'Organizations that can have multiple users and premium features'; COMMENT ON TABLE "Organizations" IS 'Organizations that can have multiple users and premium features';
+96
View File
@@ -0,0 +1,96 @@
# ⚡ Gyors Összefoglaló - Felesleges Adatok Tisztítás
## 🎯 Mi a probléma?
A frontend **10 felesleges mezőt** küld a backendnek minden kártya mentésekor.
## 📊 Számok
- **Felesleges deck mezők:** 1 db (`description`)
- **Felesleges kártya mezők:** 9 db
- **Payload csökkenés:** ~32-60%
- **Implementációs idő:** ~3-4 óra
## ✅ Használt mezők (BACKEND)
```javascript
{
name: "Pakli neve",
type: 2, // 0=LUCK, 1=JOKER, 2=QUESTION
ctype: 1, // 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION
cards: [
{
text: "Kérdés szövege",
type: 0, // CardType enum (0-4)
answer: "..." // TÍPUS-SPECIFIKUS formátum!
}
]
}
```
## ❌ Felesleges mezők (TÖRLENDŐ)
### Deck:
- `description` - nincs a backend sémában
### Kártya:
- `id` (frontend generált) - backend UUID-t használ
- `question` - duplikáció (`text` használandó)
- `statement` - duplikáció (`text` használandó)
- `options` - `answer` array-ben kell lennie
- `correctAnswer` - `answer` array-ben kell lennie
- `leftItems`, `rightItems`, `correctPairs` - `answer` array-ben kell lennie
- `acceptedAnswers` - `answer` array-ként kell lennie
- `hint` - nincs implementálva
## 🔄 Helyes answer formátumok
| Típus | answer formátum |
|-------|----------------|
| QUIZ (0) | `[{answer: "A", text: "...", correct: true}, ...]` |
| PAIRING (1) | `[{left: "...", right: "..."}, ...]` |
| OWN_ANSWER (2) | `["answer1", "answer2", ...]` |
| TRUE_FALSE (3) | `true` vagy `false` |
| CLOSER (4) | `{correct: 123, percent: 10}` |
## 🛠️ Következő lépések
1. ✅ Olvasd el: `FRONTEND_TO_BACKEND_DATA_CLEANUP.md`
2. 🔧 Implementáld: `cardBackendConverter.js` utility
3. 🔄 Módosítsd: `DeckCreator.jsx` mentés logikát
4. ✅ Teszteld: minden kártyatípust
## 📁 Kapcsolódó fájlok
- **Részletes dokumentáció:** `FRONTEND_TO_BACKEND_DATA_CLEANUP.md`
- **Módosítandó frontend:** `src/pages/DeckCreator/DeckCreator.jsx`
- **Backend referencia:** `SerpentRace_Backend/src/Application/Services/CardProcessingService.ts`
---
**Gyors példa:**
```javascript
// ❌ ROSSZ (jelenleg)
{
text: "Kérdés",
question: "Kérdés", // Duplikáció
options: ["A", "B", "C"], // Felesleges
correctAnswer: 0 // Felesleges
}
// ✅ JÓ (célállapot)
{
text: "Kérdés",
type: 0,
answer: [
{answer: "A", text: "A", correct: true},
{answer: "B", text: "B", correct: false},
{answer: "C", text: "C", correct: false}
]
}
```
---
📖 **Teljes dokumentáció:** Lásd `FRONTEND_TO_BACKEND_DATA_CLEANUP.md`
@@ -0,0 +1,750 @@
# Frontend → Backend Felesleges Adatok Dokumentáció
## 📋 Összefoglaló
Ez a dokumentum tartalmazza azokat a mezőket és adatokat, amiket a frontend küld a backendnek, de **nem szükségesek** vagy **nem használtak** a backend oldalon.
**🎯 Fő probléma:** A frontend sok felesleges mezőt küld, ahelyett hogy egyetlen `answer` mezőt használna típus-specifikus formátumban.
**💾 Adatmegtakarítás:** ~40-60% payload csökkentés várható a tisztítás után!
---
## 📊 Gyors Összefoglaló Táblázat
| Mező | Használat | Cselekvés |
|------|-----------|-----------|
| `name` | ✅ Használt | Megtartani |
| `type` | ✅ Használt | Megtartani |
| `ctype` | ✅ Használt | Megtartani |
| `cards` | ✅ Használt | Megtartani |
| `description` | ❌ **Nincs a DB-ben** | **TÖRÖLNI** |
| | | |
| **Kártya mezők:** | | |
| `card.text` | ✅ Használt | Megtartani |
| `card.type` | ✅ Használt | Megtartani |
| `card.answer` | ✅ Használt | Megtartani (típus-specifikus!) |
| `card.consequence` | ✅ Használt (LUCK) | Megtartani |
| | | |
| `card.id` (frontend) | ❌ Nem releváns | **NE KÜLDJÜK** |
| `card.question` | ❌ Duplikáció | **TÖRÖLNI** (text-be) |
| `card.statement` | ❌ Duplikáció | **TÖRÖLNI** (text-be) |
| `card.options` | ❌ Felesleges | **KONVERTÁLNI** (answer-be) |
| `card.correctAnswer` | ❌ Felesleges | **KONVERTÁLNI** (answer-be) |
| `card.leftItems` | ❌ Felesleges | **KONVERTÁLNI** (answer-be) |
| `card.rightItems` | ❌ Felesleges | **KONVERTÁLNI** (answer-be) |
| `card.correctPairs` | ❌ Felesleges | **KONVERTÁLNI** (answer-be) |
| `card.acceptedAnswers` | ❌ Felesleges | **KONVERTÁLNI** (answer-be) |
| `card.hint` | ❌ Nincs implementálva | **TÖRÖLNI** |
---
## 🎯 Deck Létrehozás/Frissítés (createDeck / updateDeck)
### Backend által HASZNÁLT mezők:
```typescript
// CreateDeckCommand / UpdateDeckCommand
{
name: string, // ✅ HASZNÁLT - Pakli neve
type: number, // ✅ HASZNÁLT - 0=LUCK, 1=JOKER, 2=QUESTION
userid: string, // ✅ HASZNÁLT - Automatikusan hozzáadódik az authRequired middleware-ből
cards: any[], // ✅ HASZNÁLT - Kártyák tömbje
ctype?: number, // ✅ HASZNÁLT - 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION
state?: number, // ✅ HASZNÁLT - De csak admin állíthatja (0=ACTIVE, 1=SOFT_DELETE)
authLevel: number // ✅ HASZNÁLT - Automatikusan jön az auth middleware-ből
}
```
### Frontend által KÜLDÖTT de FELESLEGES mezők:
#### 1. **`description` mező** - ❌ NEM HASZNÁLT
**Helyek:** `DeckCreator.jsx` (line ~100-110, ~170)
```javascript
// FELESLEGES - Backend nem tárolja, nem használja
const payload = {
name: deck.name?.trim() || "Névtelen pakli",
type: typeMapping[deck.type] ?? 2,
ctype: ctypeMapping[deck.privacy] ?? 1,
cards: cleanedCards
// description: deck.description // ❌ Ez NINCS a backend sémában!
}
```
**Megjegyzés a kódban (line ~171):**
```javascript
// Note: description field is not sent to backend as it's not supported yet
```
**Javaslat:**
- Ha a `description` soha nem lesz használva → töröljük a frontend state-ből
- Ha később implementálni fogjuk → adjuk hozzá a backend DeckAggregate entitáshoz először
---
## 📇 Kártya Mezők (cards array)
### Backend Card Interface:
```typescript
export interface Card {
text: string; // ✅ KÖTELEZŐ
type?: CardType; // ✅ OPCIONÁLIS - 0=QUIZ, 1=PAIRING, 2=OWN_ANSWER, 3=TRUE_FALSE, 4=CLOSER
answer?: string | null; // ✅ OPCIONÁLIS
consequence?: Consequence | null; // ✅ OPCIONÁLIS (csak LUCK kártyáknál)
}
```
### Frontend által KÜLDÖTT de ESETLEG FELESLEGES kártya mezők:
#### A. **Duplikált mezők** (ugyanaz az adat több néven):
```javascript
// DeckCreator.jsx - cleanedCards mapping (line ~130-165)
// 1. TEXT mező duplikáció - ⚠️ REDUNDÁNS
cleanedCard.text = card.text || card.question || card.statement || ""
if (card.question !== undefined) cleanedCard.question = card.question // ❌ Felesleges?
if (card.statement !== undefined) cleanedCard.statement = card.statement // ❌ Felesleges?
// Backend csak a `text` mezőt használja!
// A `question` és `statement` valószínűleg NEM SZÜKSÉGESEK
```
**Megjegyzés:** A backend `Card` interfészben **nincs** `question` vagy `statement` mező, csak `text`.
#### B. **QUESTION típusú kártyák extra mezői** - ⚠️ ELLENŐRIZENDŐ
```javascript
// Ezek a mezők a DeckCreator.jsx-ben kerülnek hozzáadásra (line ~145-155)
if (card.question !== undefined) cleanedCard.question = card.question
if (card.statement !== undefined) cleanedCard.statement = card.statement
if (card.options !== undefined) cleanedCard.options = card.options
if (card.correctAnswer !== undefined) cleanedCard.correctAnswer = card.correctAnswer
if (card.leftItems !== undefined) cleanedCard.leftItems = card.leftItems
if (card.rightItems !== undefined) cleanedCard.rightItems = card.rightItems
if (card.correctPairs !== undefined) cleanedCard.correctPairs = card.correctPairs
if (card.acceptedAnswers !== undefined) cleanedCard.acceptedAnswers = card.acceptedAnswers
if (card.hint !== undefined) cleanedCard.hint = card.hint
```
**Backend Card interfész ezeket NEM tartalmazza:**
- ❌ `question` - Nincs a Card interface-ben
- ❌ `statement` - Nincs a Card interface-ben
- ❌ `options` - Nincs a Card interface-ben
- ❌ `correctAnswer` - Nincs a Card interface-ben
- ❌ `leftItems` - Nincs a Card interface-ben
- ❌ `rightItems` - Nincs a Card interface-ben
- ❌ `correctPairs` - Nincs a Card interface-ben
- ❌ `acceptedAnswers` - Nincs a Card interface-ben
- ❌ `hint` - Nincs a Card interface-ben
**KRITIKUS KÉRDÉS:**
- Ezek a mezők **JSON-ként tárolódnak** a `cards` mezőben?
- A backend TypeORM `@Column({ type: 'json' })` deklaráció miatt bármit el tud tárolni
- De a **Card interface** szerint csak `text`, `type`, `answer`, `consequence` mezőket használ
**Két lehetséges eset:**
1. **Ha a backend JSON mezőként tárolja de nem használja ezeket:**
- ❌ FELESLEGESEK - Adatbázis helyet pazarolnak
- Javaslat: Tisztítsuk meg a frontend-et, ne küldje őket
2. **Ha a backend valahol mégis használja (pl. game logic-ban):**
- ✅ SZÜKSÉGESEK - De akkor frissíteni kell a Card interface-t
---
## 🎮 Consequence mező - ✅ RENDBEN (de típus ellenőrzés szükséges)
```javascript
// DeckCreator.jsx (line ~160-162)
if (deck.type === 'LUCK' && card.consequence) {
cleanedCard.consequence = card.consequence
}
```
**Backend Consequence interface:**
```typescript
export interface Consequence {
type: ConsequenceType; // 0-5 közötti szám
value?: number;
}
```
**Javaslat:** Ellenőrizni kell hogy a frontend mindig valid `ConsequenceType` enum értéket küld-e (0-5).
---
## 🔍 Részletes Backend vs Frontend Mapping
### Deck Level
| Frontend Mező | Backend Mező | Használat | Megjegyzés |
|--------------|-------------|----------|-----------|
| `deck.id` | `id` | ✅ Használt | UUID |
| `deck.name` | `name` | ✅ Használt | string (max 255) |
| `deck.type` | `type` | ✅ Használt | 0/1/2 (enum) |
| `deck.privacy` | `ctype` | ✅ Használt | 0/1/2 (enum) |
| `deck.description` | - | ❌ **NEM LÉTEZIK** | **FELESLEGES** |
| `deck.cards` | `cards` | ✅ Használt | JSON array |
| `deck.creationdate` | `creationdate` | ✅ Használt | Date (readonly) |
| `deck.updatedate` | `updateDate` | ✅ Használt | Date (readonly) |
### Card Level (QUESTION típusú kártyák)
| Frontend Mező | Backend Card Interface | Használat | Megjegyzés |
|--------------|----------------------|----------|-----------|
| `card.id` | - | ❌ **Lokális azonosító** | Csak frontend-en, backenden nem releváns |
| `card.text` | `text` | ✅ Használt | Fő szöveg |
| `card.question` | - | ❓ **Ellenőrizendő** | Lehet felesleges (text duplikáció?) |
| `card.statement` | - | ❓ **Ellenőrizendő** | Lehet felesleges (text duplikáció?) |
| `card.type` / `card.subType` | `type` | ✅ Használt | CardType enum (0-4) |
| `card.answer` | `answer` | ✅ Használt | String vagy null |
| `card.options` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.correctAnswer` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.leftItems` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.rightItems` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.correctPairs` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.acceptedAnswers` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.hint` | - | ❓ **Ellenőrizendő** | Nincs a Card interface-ben |
| `card.consequence` | `consequence` | ✅ Használt | Csak LUCK típusnál |
---
## ⚠️ BACKEND GAME LOGIC VIZSGÁLAT - ✅ KÉSZ
### 1. Kártya mezők tényleges használata - ELLENŐRIZVE ✅
**Ellenőrzött fájlok:**
- ✅ `SerpentRace_Backend/src/Application/Services/CardProcessingService.ts`
- ✅ `SerpentRace_Backend/src/Application/Services/CardDrawingService.ts`
**EREDMÉNY: A backend CSAK az `answer` mezőt használja!**
**Backend Card használat:**
```typescript
export interface Card {
text: string; // ✅ Kérdés szövege
type?: CardType; // ✅ Kártya típus (0-4)
answer?: string | null; // ✅ EGYETLEN valid mező a válaszokhoz!
consequence?: Consequence | null; // ✅ Csak LUCK kártyákhoz
}
```
**Fontos:** A backend `answer` mező **típus-specifikus formátumú**:
1. **QUIZ (type: 0)**`answer` = `QuizOption[]` array:
```typescript
answer: [
{ answer: "A", text: "First option", correct: false },
{ answer: "B", text: "Second option", correct: true },
...
]
```
2. **SENTENCE_PAIRING (type: 1)**`answer` = Párosítás array:
```typescript
answer: [
{ left: "Apple", right: "Red" },
{ left: "Banana", right: "Yellow" }
]
```
3. **OWN_ANSWER (type: 2)**`answer` = String vagy String array:
```typescript
answer: ["correct answer 1", "correct answer 2"]
```
4. **TRUE_FALSE (type: 3)**`answer` = Boolean:
```typescript
answer: true // vagy false
```
5. **CLOSER (type: 4)**`answer` = Object:
```typescript
answer: { correct: 42, percent: 10 }
```
**KÖVETKEZTETÉS:**
- ❌ **A frontend által küldött `options`, `correctAnswer`, `acceptedAnswers`, `leftItems`, `rightItems`, `correctPairs` mezők MIND FELESLEGESEK!**
- ✅ **Csak az `answer` mezőt kellene küldeni, megfelelő formátumban!**
### 2. Card Type Mapping
**Frontend:**
```javascript
const cardTypeMapping = {
'quiz': 0, // QUIZ
'pairing': 1, // SENTENCE_PAIRING
'text': 2, // OWN_ANSWER
'truefalse': 3, // TRUE_FALSE
'closer': 4 // CLOSER
}
```
**Backend CardType enum:**
```typescript
export enum CardType {
QUIZ = 0,
SENTENCE_PAIRING = 1,
OWN_ANSWER = 2,
TRUE_FALSE = 3,
CLOSER = 4
}
```
**Ez HELYES** - A mapping megfelelő
### 3. Frontend kártya ID kezelés
```javascript
// DeckCreator.jsx (line ~242)
const updatedCard = {
...cardData,
id: isCreatingCard ? Date.now() : cardData.id
}
// Line ~129
if (card.id) {
cleanedCard.id = card.id
}
```
**Probléma:** A frontend `Date.now()` timestamp ID-kat generál, de a backend UUID-kat használ.
**Javaslat:**
- ❌ NE küldjük a frontend-generált `id`-t a backendnek
- A backend a create során generál UUID-t
- Update-nél a backend már ismeri az ID-t (URL parameter-ből jön)
---
## 📝 JAVASOLT TISZTÍTÁSOK
### Prioritás 1: BIZTOS FELESLEGESEK
1. **`description` mező törlése**
- Fájl: `DeckCreator.jsx`
- Sorok: ~20, ~40-45, ~100-105
- Töröljük a state-ből és ne küldjük a backendnek
2. **Frontend-generált kártya `id` ne menjen a backendre**
- Fájl: `DeckCreator.jsx`
- Sor: ~129
- Kommenteljük ki vagy töröljük: `if (card.id) cleanedCard.id = card.id`
### Prioritás 2: BIZONYÍTOTTAN FELESLEGESEK ✅
3. **Duplikált text mezők (`question`, `statement`)** - ❌ FELESLEGES
- A backend **csak `text`-et használ**
- Töröljük: `question` és `statement` mezők küldését
4. **QUESTION kártya részletes mezők - MIND FELESLEGESEK ❌**
- A backend GameService **NEM használja** ezeket:
- ❌ `options` - Felesleges (backend: `answer` array használ)
- ❌ `correctAnswer` - Felesleges (backend: `answer` array-ben `correct: true`)
- ❌ `leftItems` / `rightItems` / `correctPairs` - Felesleges (backend: `answer` array-ben `{left, right}` párok)
- ❌ `acceptedAnswers` - Felesleges (backend: `answer` string array)
- ❌ `hint` - Nincs implementálva a backenden
**HELYETTE:** Konvertáljuk ezeket megfelelő `answer` formátumra!
---
## 🔄 HELYES KONVERZIÓ - Példák
### Jelenlegi (FELESLEGES mezőkkel):
```javascript
// ❌ ROSSZ - Felesleges mezők küldése
const cleanedCard = {
text: "Mi a főváros?",
type: 0, // QUIZ
question: "Mi a főváros?", // ❌ DUPLIKÁCIÓ
options: ["Budapest", "Berlin", "Prága"], // ❌ FELESLEGES
correctAnswer: 0 // ❌ FELESLEGES
}
```
### Helyes (Optimalizált):
```javascript
// ✅ JÓ - Csak szükséges mezők
const cleanedCard = {
text: "Mi a főváros?",
type: 0, // QUIZ
answer: [
{ answer: "A", text: "Budapest", correct: true },
{ answer: "B", text: "Berlin", correct: false },
{ answer: "C", text: "Prága", correct: false }
]
}
```
### Konverziós Példák Típusonként:
#### 1. QUIZ (type: 0) - Feleletválasztós
**Frontend állapot:**
```javascript
card = {
subType: 'multiplechoice',
question: "Melyik a helyes?",
options: ["A válasz", "B válasz", "C válasz"],
correctAnswer: 1
}
```
**Helyes backend formátum:**
```javascript
cleanedCard = {
text: "Melyik a helyes?",
type: 0,
answer: [
{ answer: "A", text: "A válasz", correct: false },
{ answer: "B", text: "B válasz", correct: true }, // correctAnswer: 1
{ answer: "C", text: "C válasz", correct: false }
]
}
```
#### 2. SENTENCE_PAIRING (type: 1) - Párosítás
**Frontend állapot:**
```javascript
card = {
subType: 'matching',
question: "Párosítsd össze!",
leftItems: ["Alma", "Banán"],
rightItems: ["Piros", "Sárga"],
correctPairs: { 0: 0, 1: 1 } // leftItems[0] -> rightItems[0]
}
```
**Helyes backend formátum:**
```javascript
cleanedCard = {
text: "Párosítsd össze!",
type: 1,
answer: [
{ left: "Alma", right: "Piros" },
{ left: "Banán", right: "Sárga" }
]
}
```
#### 3. OWN_ANSWER (type: 2) - Szöveges válasz
**Frontend állapot:**
```javascript
card = {
subType: 'text',
question: "Mi a főváros?",
acceptedAnswers: ["Budapest", "budapest", "Bp"]
}
```
**Helyes backend formátum:**
```javascript
cleanedCard = {
text: "Mi a főváros?",
type: 2,
answer: ["Budapest", "budapest", "Bp"]
}
```
#### 4. TRUE_FALSE (type: 3) - Igaz/Hamis
**Frontend állapot:**
```javascript
card = {
subType: 'truefalse',
statement: "A Föld lapos.",
correctAnswer: 1, // 0=Igaz, 1=Hamis
isTrue: false
}
```
**Helyes backend formátum:**
```javascript
cleanedCard = {
text: "A Föld lapos.",
type: 3,
answer: false
}
```
#### 5. CLOSER (type: 4) - Tippelés
**Frontend állapot:**
```javascript
card = {
subType: 'closer',
question: "Hány lakosa van Budapestnek?",
correctAnswer: 1750000,
tolerance: 10 // ±10%
}
```
**Helyes backend formátum:**
```javascript
cleanedCard = {
text: "Hány lakosa van Budapestnek?",
type: 4,
answer: {
correct: 1750000,
percent: 10
}
}
```
---
## 🔧 TESZTELÉSI TERV
1. **Logolás hozzáadása a backenden:**
```typescript
// CreateDeckCommandHandler.ts, UpdateDeckCommandHandler.ts
console.log('Received card data:', cmd.cards)
console.log('Card keys:', Object.keys(cmd.cards[0]))
```
2. **Frontendről küldött payload ellenőrzése:**
```javascript
// DeckCreator.jsx - handleSaveDeck
console.log('Payload before send:', JSON.stringify(payload, null, 2))
```
3. **Adatbázisban tárolt JSON ellenőrzése:**
```sql
SELECT id, name, cards FROM Decks WHERE id = 'xyz' LIMIT 1;
```
---
## ✅ KÖVETKEZŐ LÉPÉSEK
1. ✅ **Dokumentáció elkészült** - Ez a fájl
2. ✅ **Backend game logic ellenőrzés** - KÉSZ! Csak `answer` mezőt használ
3. ⏳ **Frontend konverzió implementálás** - Következő feladat:
- Új függvény: `convertCardToBackendFormat(card, deckType)`
- Minden kártyatípushoz megfelelő `answer` formátum generálása
- Felesleges mezők eltávolítása
4. ⏳ **Tesztelés** - Minden működik-e a változások után?
---
## 🛠️ IMPLEMENTÁCIÓS TERV
### 1. Létrehozandó segédfüggvény: `cardBackendConverter.js`
```javascript
// src/utils/cardBackendConverter.js
/**
* Konvertálja a frontend kártya formátumot backend-kompatibilis formátumra
* @param {Object} card - Frontend kártya objektum
* @param {string} deckType - Pakli típusa ('LUCK', 'JOKER', 'QUESTION')
* @returns {Object} Backend-kompatibilis kártya objektum
*/
export function convertCardToBackendFormat(card, deckType) {
const baseCard = {
text: card.text || card.question || card.statement || "",
}
// CardType mapping
const cardTypeMapping = {
'quiz': 0,
'multiplechoice': 0, // Alias
'pairing': 1,
'matching': 1, // Alias
'text': 2,
'truefalse': 3,
'closer': 4
}
const cardType = cardTypeMapping[card.subType] ?? cardTypeMapping[card.subType?.toLowerCase()]
if (cardType !== undefined) {
baseCard.type = cardType
}
// Típus-specifikus answer konverzió
switch (cardType) {
case 0: // QUIZ
if (card.options && Array.isArray(card.options)) {
baseCard.answer = card.options.map((opt, idx) => ({
answer: String.fromCharCode(65 + idx), // A, B, C, D...
text: opt,
correct: idx === card.correctAnswer
}))
}
break
case 1: // SENTENCE_PAIRING
if (card.leftItems && card.rightItems && card.correctPairs) {
baseCard.answer = Object.entries(card.correctPairs).map(([leftIdx, rightIdx]) => ({
left: card.leftItems[parseInt(leftIdx)],
right: card.rightItems[parseInt(rightIdx)]
}))
}
break
case 2: // OWN_ANSWER
if (card.acceptedAnswers && Array.isArray(card.acceptedAnswers)) {
baseCard.answer = card.acceptedAnswers.filter(a => a && a.trim())
}
break
case 3: // TRUE_FALSE
if (card.correctAnswer !== undefined) {
baseCard.answer = card.correctAnswer === 0 // 0=Igaz, 1=Hamis
} else if (card.isTrue !== undefined) {
baseCard.answer = card.isTrue
}
break
case 4: // CLOSER
if (card.correctAnswer !== undefined && card.tolerance !== undefined) {
baseCard.answer = {
correct: card.correctAnswer,
percent: card.tolerance
}
}
break
}
// LUCK típusú kártyákhoz consequence
if (deckType === 'LUCK' && card.consequence) {
baseCard.consequence = card.consequence
}
return baseCard
}
```
### 2. Módosítandó fájl: `DeckCreator.jsx`
**Jelenlegi kód (line ~120-165):**
```javascript
// ❌ RÉGI - Felesleges mezők küldése
const cleanedCards = validCards.map(card => {
const cleanedCard = {}
if (card.id) cleanedCard.id = card.id
if (card.subType && cardTypeMapping[card.subType] !== undefined) {
cleanedCard.type = cardTypeMapping[card.subType]
}
cleanedCard.text = card.text || card.question || card.statement || ""
if (card.question !== undefined) cleanedCard.question = card.question // FELESLEGES
if (card.statement !== undefined) cleanedCard.statement = card.statement // FELESLEGES
if (card.options !== undefined) cleanedCard.options = card.options // FELESLEGES
// ... stb
return cleanedCard
})
```
**Új kód:**
```javascript
// ✅ ÚJ - Csak szükséges mezők
import { convertCardToBackendFormat } from '../../utils/cardBackendConverter'
const cleanedCards = validCards.map(card =>
convertCardToBackendFormat(card, deck.type)
)
```
### 3. Tesztelési checklist
- [ ] QUIZ kártyák helyes answer formátummal mentődnek
- [ ] SENTENCE_PAIRING kártyák helyes left-right párokkal mentődnek
- [ ] OWN_ANSWER kártyák acceptedAnswers array-ként mentődnek
- [ ] TRUE_FALSE kártyák boolean answer-rel mentődnek
- [ ] CLOSER kártyák {correct, percent} formátummal mentődnek
- [ ] LUCK kártyák consequence mezője megmarad
- [ ] Mentett paklik betöltése és szerkesztése működik
- [ ] Játék során kártyák feldolgozása helyes
---
**Utolsó frissítés:** 2025-11-03
**Készítette:** GitHub Copilot
**Cél:** Adatoptimalizálás és felesleges payload csökkentés
---
## 📈 VÁRHATÓ EREDMÉNYEK
### Payload méret csökkenés példa:
**ELŐTTE (jelenleg):**
```json
{
"name": "Teszt Pakli",
"type": 2,
"ctype": 1,
"description": "Ez egy leírás", // ❌ FELESLEGES
"cards": [
{
"id": 1730123456789, // ❌ FELESLEGES
"text": "Mi a főváros?",
"question": "Mi a főváros?", // ❌ DUPLIKÁCIÓ
"type": 0,
"options": ["Budapest", "Berlin", "Prága"], // ❌ FELESLEGES
"correctAnswer": 0 // ❌ FELESLEGES
}
]
}
// Méret: ~280 byte
```
**UTÁNA (optimalizált):**
```json
{
"name": "Teszt Pakli",
"type": 2,
"ctype": 1,
"cards": [
{
"text": "Mi a főváros?",
"type": 0,
"answer": [
{"answer": "A", "text": "Budapest", "correct": true},
{"answer": "B", "text": "Berlin", "correct": false},
{"answer": "C", "text": "Prága", "correct": false}
]
}
]
}
// Méret: ~190 byte
```
**💾 Megtakarítás: ~32% ebben a példában!**
---
## 🎉 VÉGSŐ ÖSSZEFOGLALÁS
### Felesleges mezők száma:
- **Deck level:** 1 mező (`description`)
- **Card level:** 9 mező (`id`, `question`, `statement`, `options`, `correctAnswer`, `leftItems`, `rightItems`, `correctPairs`, `acceptedAnswers`, `hint`)
### Összes felesleges mező: **10 db**
### Ajánlott lépések:
1. ✅ Dokumentáció áttekintése
2. 🔄 `cardBackendConverter.js` implementálása
3. 🔧 `DeckCreator.jsx` módosítása
4. ✅ Tesztelés minden kártyatípussal
5. 🚀 Deploy
**Becsült munkaidő:** 2-3 óra implementálás + 1 óra tesztelés
---
## 📞 Kérdések / Problémák esetén
Ha bármilyen kérdés merül fel az implementálás során:
1. Ellenőrizd a backend `CardProcessingService.ts` fájlt
2. Nézd meg a példákat ebben a dokumentációban
3. Teszteld lokálisan először egy kis paklival
**Fontos:** A backend JSON mezőként tárolja a `cards` array-t, ezért bármit elfogad - de csak a dokumentált mezőket használja!
+3 -3
View File
@@ -8,6 +8,7 @@ import ResetPassword from "./pages/Auth/ResetPassword"
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 Card_display from "./pages/Decks/Card_display"
import DeckCreator from "./pages/DeckCreator/DeckCreator" import DeckCreator from "./pages/DeckCreator/DeckCreator"
import CompanyHub from "./pages/Contacts/Contacts" import CompanyHub from "./pages/Contacts/Contacts"
import About from "./pages/About/About" import About from "./pages/About/About"
@@ -19,8 +20,6 @@ import ProfileCard from "./components/Userdetails/Userdetails"
import { ToastConfig } from "./components/Toastify/toastifyServices" // fontos: named import, nem default! import { ToastConfig } from "./components/Toastify/toastifyServices" // fontos: named import, nem default!
import VerifyEmailPage from "./pages/Auth/VerifyEmailPage" import VerifyEmailPage from "./pages/Auth/VerifyEmailPage"
function App() { function App() {
const [isMobile, setIsMobile] = useState(false) const [isMobile, setIsMobile] = useState(false)
@@ -63,10 +62,11 @@ function App() {
<Route path="/" element={<Landingpage />} /> <Route path="/" element={<Landingpage />} />
<Route path="/home" element={<Home />} /> <Route path="/home" element={<Home />} />
<Route path="/decks" element={<DeckManagerPage />} /> <Route path="/decks" element={<DeckManagerPage />} />
<Route path="/deck/:deckId" element={<Card_display />} />
<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="/contacts" element={<CompanyHub />} /> {/* <Route path="/contacts" element={<CompanyHub />} /> */}
<Route path="/report" element={<Reports />} /> <Route path="/report" element={<Reports />} />
</Routes> </Routes>
</Router> </Router>
+12 -1
View File
@@ -40,8 +40,19 @@ export const updateDeck = async (deckId, deck) => {
} }
} }
// Delete a deck (soft delete) (authenticated)
export const deleteDeck = async (deckId) => {
try {
const response = await apiClient.delete(`/decks/${deckId}`)
return response.data
} catch (err) {
throw err
}
}
export default { export default {
createDeck, createDeck,
getDeckById, getDeckById,
updateDeck updateDeck,
deleteDeck
} }
@@ -12,9 +12,9 @@ const Animation = ({ sizePercentage = 100 }) => {
const pathRefs = Array.from({ length: 11 }, () => useRef(null)); const pathRefs = Array.from({ length: 11 }, () => useRef(null));
return ( return (
<div> <div className="w-full flex justify-center">
{/* prettier-ignore */} {/* prettier-ignore */}
<svg className={styles.animation} width={width} height={height} viewBox="0 0 1319 198" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg className={styles.animation} width="100%" height="auto" viewBox="0 0 1319 198" fill="none" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet" style={{ maxWidth: `${width}px`, maxHeight: `${height}px` }}>
<path ref={pathRefs[0]} className={styles.path0} d="M1261.64 32.9C1272.02 32.9 1281.15 34.9576 1289.1 39.0094L1289.86 39.4078C1297.97 43.7136 1304.29 49.9037 1308.86 58.026L1308.86 58.0328L1308.87 58.0406C1313.41 65.9983 1315.74 75.4878 1315.74 86.6002C1315.74 88.8329 1315.63 91.0662 1315.41 93.3004H1240.77L1240.94 95.9625C1241.36 102.425 1243.14 107.682 1246.63 111.328L1246.67 111.368L1246.71 111.407C1250.29 114.831 1254.8 116.5 1260.04 116.5C1263.69 116.5 1266.97 115.677 1269.77 113.917C1272.15 112.419 1274.06 110.315 1275.55 107.7H1312.61C1310.88 113.608 1308.06 118.989 1304.16 123.859L1303.71 124.408L1303.71 124.413C1299.18 129.919 1293.45 134.322 1286.48 137.611L1285.8 137.925C1278.56 141.229 1270.51 142.9 1261.64 142.9C1250.94 142.9 1241.49 140.648 1233.23 136.205C1225.37 131.905 1219.12 125.83 1214.46 117.933L1214.01 117.164C1209.46 108.936 1207.14 99.1765 1207.14 87.8004C1207.14 76.4113 1209.46 66.7169 1214.01 58.6256L1214.02 58.6187L1214.02 58.6109C1218.45 50.6085 1224.53 44.4249 1232.28 40.0143L1233.04 39.5934C1241.29 35.1536 1250.8 32.9 1261.64 32.9ZM1261.44 58.9C1256.17 58.9 1251.64 60.3691 1248.04 63.4723C1244.4 66.4788 1242.18 70.8761 1241.18 76.3473L1240.63 79.3004H1280.74V76.8004C1280.74 71.5541 1279.01 67.178 1275.39 63.985L1275.04 63.6793C1271.33 60.4557 1266.74 58.9 1261.44 58.9Z" stroke="white" strokeWidth="5"/> <path ref={pathRefs[0]} className={styles.path0} d="M1261.64 32.9C1272.02 32.9 1281.15 34.9576 1289.1 39.0094L1289.86 39.4078C1297.97 43.7136 1304.29 49.9037 1308.86 58.026L1308.86 58.0328L1308.87 58.0406C1313.41 65.9983 1315.74 75.4878 1315.74 86.6002C1315.74 88.8329 1315.63 91.0662 1315.41 93.3004H1240.77L1240.94 95.9625C1241.36 102.425 1243.14 107.682 1246.63 111.328L1246.67 111.368L1246.71 111.407C1250.29 114.831 1254.8 116.5 1260.04 116.5C1263.69 116.5 1266.97 115.677 1269.77 113.917C1272.15 112.419 1274.06 110.315 1275.55 107.7H1312.61C1310.88 113.608 1308.06 118.989 1304.16 123.859L1303.71 124.408L1303.71 124.413C1299.18 129.919 1293.45 134.322 1286.48 137.611L1285.8 137.925C1278.56 141.229 1270.51 142.9 1261.64 142.9C1250.94 142.9 1241.49 140.648 1233.23 136.205C1225.37 131.905 1219.12 125.83 1214.46 117.933L1214.01 117.164C1209.46 108.936 1207.14 99.1765 1207.14 87.8004C1207.14 76.4113 1209.46 66.7169 1214.01 58.6256L1214.02 58.6187L1214.02 58.6109C1218.45 50.6085 1224.53 44.4249 1232.28 40.0143L1233.04 39.5934C1241.29 35.1536 1250.8 32.9 1261.64 32.9ZM1261.44 58.9C1256.17 58.9 1251.64 60.3691 1248.04 63.4723C1244.4 66.4788 1242.18 70.8761 1241.18 76.3473L1240.63 79.3004H1280.74V76.8004C1280.74 71.5541 1279.01 67.178 1275.39 63.985L1275.04 63.6793C1271.33 60.4557 1266.74 58.9 1261.44 58.9Z" stroke="white" strokeWidth="5"/>
<path ref={pathRefs[1]} className={styles.path1} d="M1139.95 32.9C1153.73 32.9 1165.15 36.6867 1174.38 44.1441L1174.39 44.151L1174.4 44.1578C1182.91 50.9203 1188.68 60.2478 1191.63 72.3004H1154.9C1153.61 69.0944 1151.8 66.4744 1149.4 64.5846C1146.55 62.349 1143.08 61.3004 1139.15 61.3004C1133.38 61.3004 1128.7 63.7808 1125.31 68.5533L1125.31 68.5602L1125.3 68.566C1122.08 73.1723 1120.65 79.708 1120.65 87.8004C1120.65 95.9013 1122.08 102.479 1125.28 107.202L1125.31 107.247C1128.7 112.019 1133.38 114.5 1139.15 114.5C1143.13 114.5 1146.64 113.458 1149.5 111.215C1151.9 109.324 1153.68 106.702 1154.93 103.5H1191.63C1188.77 115.027 1183.29 124.135 1175.24 130.949L1174.38 131.656C1165.15 139.113 1153.73 142.9 1139.95 142.9C1129.25 142.9 1119.8 140.648 1111.55 136.205C1103.69 131.908 1097.51 125.841 1092.97 117.958L1092.54 117.189C1087.98 108.956 1085.65 99.188 1085.65 87.8004C1085.65 76.9027 1087.83 67.4559 1092.12 59.3873L1092.54 58.6109C1096.97 50.6085 1103.05 44.4249 1110.8 40.0143L1111.55 39.5934C1119.81 35.1513 1129.25 32.9 1139.95 32.9Z" stroke="white" strokeWidth="5"/> <path ref={pathRefs[1]} className={styles.path1} d="M1139.95 32.9C1153.73 32.9 1165.15 36.6867 1174.38 44.1441L1174.39 44.151L1174.4 44.1578C1182.91 50.9203 1188.68 60.2478 1191.63 72.3004H1154.9C1153.61 69.0944 1151.8 66.4744 1149.4 64.5846C1146.55 62.349 1143.08 61.3004 1139.15 61.3004C1133.38 61.3004 1128.7 63.7808 1125.31 68.5533L1125.31 68.5602L1125.3 68.566C1122.08 73.1723 1120.65 79.708 1120.65 87.8004C1120.65 95.9013 1122.08 102.479 1125.28 107.202L1125.31 107.247C1128.7 112.019 1133.38 114.5 1139.15 114.5C1143.13 114.5 1146.64 113.458 1149.5 111.215C1151.9 109.324 1153.68 106.702 1154.93 103.5H1191.63C1188.77 115.027 1183.29 124.135 1175.24 130.949L1174.38 131.656C1165.15 139.113 1153.73 142.9 1139.95 142.9C1129.25 142.9 1119.8 140.648 1111.55 136.205C1103.69 131.908 1097.51 125.841 1092.97 117.958L1092.54 117.189C1087.98 108.956 1085.65 99.188 1085.65 87.8004C1085.65 76.9027 1087.83 67.4559 1092.12 59.3873L1092.54 58.6109C1096.97 50.6085 1103.05 44.4249 1110.8 40.0143L1111.55 39.5934C1119.81 35.1513 1129.25 32.9 1139.95 32.9Z" stroke="white" strokeWidth="5"/>
<path ref={pathRefs[2]} className={styles.path2} d="M995.014 32.9C1002.18 32.9 1008.26 34.2763 1013.33 36.9322L1013.81 37.193C1019.04 40.0563 1023.04 43.8802 1025.86 48.6695L1030.51 56.5602V34.3004H1064.71V141.5H1030.51V119.24L1025.86 127.13C1023.04 131.905 1019 135.728 1013.63 138.595L1013.61 138.607C1008.45 141.437 1002.27 142.9 995.014 142.9C986.807 142.9 979.357 140.83 972.608 136.697L971.956 136.291C965.401 132.037 960.089 125.994 956.045 118.069L955.657 117.296C951.72 108.895 949.714 99.0842 949.714 87.8004C949.714 76.5091 951.722 66.7655 955.656 58.5035L955.657 58.5045C959.747 50.1977 965.189 43.9003 971.956 39.5094C978.877 35.1054 986.542 32.9 995.014 32.9ZM1007.61 62.1002C1001.29 62.1002 995.894 64.2893 991.601 68.6617L991.217 69.0621C986.771 73.6617 984.714 80.0315 984.714 87.8004C984.714 95.4589 986.781 101.845 991.161 106.678L991.175 106.694L991.189 106.708C995.547 111.367 1001.08 113.7 1007.61 113.7C1014.02 113.7 1019.47 111.363 1023.81 106.738L1023.81 106.739C1028.38 102.021 1030.51 95.5962 1030.51 87.8004C1030.51 80.1231 1028.37 73.771 1023.81 69.0611H1023.81C1019.47 64.436 1014.01 62.1003 1007.61 62.1002Z" stroke="white" strokeWidth="5"/> <path ref={pathRefs[2]} className={styles.path2} d="M995.014 32.9C1002.18 32.9 1008.26 34.2763 1013.33 36.9322L1013.81 37.193C1019.04 40.0563 1023.04 43.8802 1025.86 48.6695L1030.51 56.5602V34.3004H1064.71V141.5H1030.51V119.24L1025.86 127.13C1023.04 131.905 1019 135.728 1013.63 138.595L1013.61 138.607C1008.45 141.437 1002.27 142.9 995.014 142.9C986.807 142.9 979.357 140.83 972.608 136.697L971.956 136.291C965.401 132.037 960.089 125.994 956.045 118.069L955.657 117.296C951.72 108.895 949.714 99.0842 949.714 87.8004C949.714 76.5091 951.722 66.7655 955.656 58.5035L955.657 58.5045C959.747 50.1977 965.189 43.9003 971.956 39.5094C978.877 35.1054 986.542 32.9 995.014 32.9ZM1007.61 62.1002C1001.29 62.1002 995.894 64.2893 991.601 68.6617L991.217 69.0621C986.771 73.6617 984.714 80.0315 984.714 87.8004C984.714 95.4589 986.781 101.845 991.161 106.678L991.175 106.694L991.189 106.708C995.547 111.367 1001.08 113.7 1007.61 113.7C1014.02 113.7 1019.47 111.363 1023.81 106.738L1023.81 106.739C1028.38 102.021 1030.51 95.5962 1030.51 87.8004C1030.51 80.1231 1028.37 73.771 1023.81 69.0611H1023.81C1019.47 64.436 1014.01 62.1003 1007.61 62.1002Z" stroke="white" strokeWidth="5"/>
@@ -2,7 +2,7 @@
// Deck alapadatok szerkesztése és mentés // Deck alapadatok szerkesztése és mentés
import React, { useState, useRef, useEffect } 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, FaTrash } 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)" },
@@ -15,7 +15,7 @@ const privacyOptions = [
{ value: "public", label: "Publikus", icon: FaGlobe } { value: "public", label: "Publikus", icon: FaGlobe }
] ]
export default function DeckHeader({ deck, onUpdate, onSave, onBack }) { export default function DeckHeader({ deck, onUpdate, onSave, onBack, onDelete }) {
const [isTypeDropdownOpen, setIsTypeDropdownOpen] = useState(false); const [isTypeDropdownOpen, setIsTypeDropdownOpen] = useState(false);
const [isPrivacyDropdownOpen, setIsPrivacyDropdownOpen] = useState(false); const [isPrivacyDropdownOpen, setIsPrivacyDropdownOpen] = useState(false);
const typeDropdownRef = useRef(null); const typeDropdownRef = useRef(null);
@@ -64,6 +64,17 @@ export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {
</h1> </h1>
</div> </div>
<div className="flex items-center gap-3">
{deck.id && (
<button
onClick={onDelete}
className="flex items-center gap-2 px-6 py-2 rounded-xl bg-red-600 hover:bg-red-700 text-white font-semibold transition-all duration-200 hover:scale-105 shadow-lg"
>
<FaTrash />
Törlés
</button>
)}
<button <button
onClick={onSave} onClick={onSave}
className="flex items-center gap-2 px-6 py-2 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" className="flex items-center gap-2 px-6 py-2 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"
@@ -72,6 +83,7 @@ export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {
Mentés Mentés
</button> </button>
</div> </div>
</div>
{/* Main Content Row */} {/* Main Content Row */}
<div className="space-y-4"> <div className="space-y-4">
@@ -4,29 +4,17 @@
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])
@@ -43,36 +31,6 @@ 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)",
@@ -186,100 +144,6 @@ 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>
) )
} }
@@ -150,20 +150,23 @@ export default function LuckCardEditor({ card, onChange }) {
</div> </div>
</div> </div>
{/* Consequence Value - csak ha előre/hátra lépés */} {/* Consequence Value - csak kör kihagyás és extra kör */}
{(cardData.consequence?.type === 0 || cardData.consequence?.type === 1) && ( {(cardData.consequence?.type === 2 || cardData.consequence?.type === 3) && (
<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">
Mezők száma {cardData.consequence?.type === 2 ? 'Körök kihagyása' : 'Extra körök száma'}
</label> </label>
<input <input
type="number" type="number"
min="1" min="1"
max="10" max="5"
value={cardData.consequence?.value ?? 1} value={cardData.consequence?.value ?? 1}
onChange={(e) => updateConsequence('value', parseInt(e.target.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" 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 className="text-xs text-[color:var(--color-text-muted)] mt-1">
Érték: 1-5 között
</div>
</div> </div>
)} )}
</div> </div>
@@ -20,14 +20,6 @@ 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) => {
@@ -92,26 +84,6 @@ export default function TaskCardEditor({ card, onChange }) {
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ó */}
@@ -544,100 +516,6 @@ export default function TaskCardEditor({ card, onChange }) {
</div> </div>
</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>
) )
} }
@@ -33,10 +33,84 @@ const Footer = () => {
return ( return (
<footer <footer
ref={footerRef} ref={footerRef}
className="relative bg-zinc-900 text-zinc-400 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-6 md: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 px-4">
{/* Mobile: Logo középen, majd grid alatta */}
<div className="flex flex-col items-center md:hidden gap-6 mb-6">
<div className="flex flex-col items-center">
<button
onClick={goLanding}
className="hover:scale-105 hover:brightness-110 transition-transform"
>
<Logo size={80} />
</button>
<button
onClick={goLanding}
className="font-extrabold text-lg mt-2 tracking-wide text-white hover:text-green-500 transition-colors"
>
SerpentRace
</button>
</div>
</div>
{/* Mobile: 2 oszlopos grid */}
<div className="grid grid-cols-2 gap-6 md:hidden mb-6">
{/* Oldalak */}
<div className="flex flex-col gap-1">
<span className="text-base font-semibold text-green-600 underline underline-offset-4 mb-2 drop-shadow-sm">
Oldalak
</span>
<button
onClick={goLanding}
className="text-left text-sm hover:underline hover:text-green-500 transition-colors"
>
Főoldal
</button>
<button
onClick={goAbout}
className="text-left text-sm hover:underline hover:text-green-500 transition-colors"
>
Rólunk
</button>
</div>
{/* Közösség */}
<div className="flex flex-col gap-1">
<span className="text-base font-semibold text-green-600 underline underline-offset-4 mb-2 drop-shadow-sm">
Közösség
</span>
<a
href="https://discord.gg/"
target="_blank"
rel="noopener noreferrer"
className="text-sm hover:underline hover:text-green-500"
>
Discord
</a>
<a
href="https://github.com/"
target="_blank"
rel="noopener noreferrer"
className="text-sm hover:underline hover:text-green-500"
>
GitHub
</a>
</div>
</div>
{/* Mobile: Elérhetőség teljes széles */}
<div className="flex flex-col gap-1 md:hidden mb-6">
<span className="text-base font-semibold text-green-600 underline underline-offset-4 mb-2 drop-shadow-sm">
Elérhetőség
</span>
<span className="text-sm opacity-85">Email: info@serpentrace.hu</span>
<span className="text-sm opacity-85">Telefon: +36 30 123 4567</span>
</div>
{/* Desktop: Original flex layout */}
<div className="hidden md:flex flex-wrap justify-between items-start gap-8">
{/* Logó */} {/* Logó */}
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<button <button
@@ -70,12 +144,6 @@ const Footer = () => {
> >
Rólunk Rólunk
</button> </button>
<button
onClick={goContacts}
className="text-left hover:underline hover:text-green-500 transition-colors"
>
Kapcsolat
</button>
</div> </div>
{/* Közösség */} {/* Közösség */}
@@ -110,6 +178,7 @@ const Footer = () => {
<span className="opacity-85">Telefon: +36 30 123 4567</span> <span className="opacity-85">Telefon: +36 30 123 4567</span>
</div> </div>
</div> </div>
</div>
<div className="text-center mt-8 text-sm opacity-70"> <div className="text-center mt-8 text-sm opacity-70">
© {new Date().getFullYear()} SerpentRace. Minden jog fenntartva. © {new Date().getFullYear()} SerpentRace. Minden jog fenntartva.
@@ -7,28 +7,32 @@ 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 { isAuthenticated } from "../../hooks/useRequireAuth" // <-- added import
import { useNavigate } from "react-router-dom" // <-- NEW import { useNavigate } from "react-router-dom" // <-- NEW
import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate" // <-- NEW
const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onNavigateToContacts }) => { // 🔧 HIBA JAVÍTVA: függvénydefiníció hozzáadva
const auth = isAuthenticated() // <-- check without redirect const LandingPage = () => {
const navigate = useNavigate() // <-- NEW const { goLanding, goAbout, goHome, goLogin, goAuth } = HandleNavigate()
const auth = isAuthenticated() // a hitelesítési állapot meghatározásához
return ( return (
<div className="w-full"> <div className="w-full">
{/* Hero Section */} {/* Hero Section */}
<motion.section <motion.section
className="min-h-[80vh] flex flex-col items-center justify-center text-center px-4 py-20" className="min-h-[80vh] flex flex-col items-center justify-center text-center px-4 sm:px-6 py-12 sm:py-16 md:py-20"
initial={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
> >
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto w-full">
{/* Animált logo és cím */} {/* Animált logo és cím */}
<div className="mb-8"> <div className="mb-6 sm:mb-8 flex justify-center">
<div className="w-full max-w-[90%] sm:max-w-[70%] md:max-w-full">
<SerpentRaceAnimation sizePercentage={70} /> <SerpentRaceAnimation sizePercentage={70} />
</div> </div>
</div>
<motion.h1 <motion.h1
className="text-3xl md:text-5xl font-bold text-white mb-4 leading-tight" className="text-2xl sm:text-3xl md:text-5xl font-bold text-white mb-3 sm:mb-4 leading-tight px-2"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.4 }} transition={{ duration: 0.7, delay: 0.4 }}
@@ -37,7 +41,7 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
</motion.h1> </motion.h1>
<motion.p <motion.p
className="text-lg md:text-xl text-gray-300 mb-4 max-w-3xl mx-auto leading-relaxed" className="text-base sm:text-lg md:text-xl text-gray-300 mb-3 sm:mb-4 max-w-3xl mx-auto leading-relaxed px-2"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.6 }} transition={{ duration: 0.7, delay: 0.6 }}
@@ -45,8 +49,9 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
A SerpentRace egy társasjáték, ahol új barátokra lelhetsz, közösséget építhetsz és tanulhatsz A SerpentRace egy társasjáték, ahol új barátokra lelhetsz, közösséget építhetsz és tanulhatsz
mindezt szórakozva! mindezt szórakozva!
</motion.p> </motion.p>
<motion.div <motion.div
className="text-xl md:text-2xl font-bold text-emerald-400 mb-10" className="text-lg sm:text-xl md:text-2xl font-bold text-emerald-400 mb-6 sm:mb-8 md:mb-10 px-2"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.8 }} transition={{ duration: 0.7, delay: 0.8 }}
@@ -55,7 +60,7 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
</motion.div> </motion.div>
<motion.div <motion.div
className="flex flex-col sm:flex-row gap-4 justify-center items-center" className="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center items-center px-2"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 1 }} transition={{ duration: 0.7, delay: 1 }}
@@ -63,12 +68,12 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
{/* If not authenticated show Login/Register; if authenticated show Home button */} {/* If not authenticated show Login/Register; if authenticated show Home button */}
{!auth ? ( {!auth ? (
<> <>
<ButtonGreen text="Bejelentkezés" onClick={onNavigateToPlay} width="w-60" /> <ButtonGreen text="Bejelentkezés" onClick={goLogin} width="w-full sm:w-60" />
<ButtonGreen text="Regisztráció" onClick={onNavigateToAuth} width="w-60" /> <ButtonGreen text="Regisztráció" onClick={goAuth} width="w-full sm:w-60" />
<ButtonGreen text="Játék" onClick={onNavigateToGame} width="w-60" /> <ButtonGreen text="Játék" onClick={goHome} width="w-full sm:w-60" />
</> </>
) : ( ) : (
<ButtonGreen text="Játék" onClick={onNavigateToGame} width="w-60" /> <ButtonGreen text="Játék" onClick={goHome} width="w-full sm:w-60" />
)} )}
</motion.div> </motion.div>
</div> </div>
@@ -76,7 +81,7 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
{/* Features Section */} {/* Features Section */}
<motion.section <motion.section
className="py-20 px-4" className="py-12 sm:py-16 md:py-20 px-4 sm:px-6"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }} whileInView={{ opacity: 1 }}
viewport={{ once: true, amount: 0.2 }} viewport={{ once: true, amount: 0.2 }}
@@ -84,7 +89,7 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
> >
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<motion.h2 <motion.h2
className="text-2xl md:text-3xl font-bold text-white text-center mb-12" className="text-xl sm:text-2xl md:text-3xl font-bold text-white text-center mb-8 sm:mb-10 md:mb-12 px-2"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
@@ -93,19 +98,19 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
Miért a SerpentRace a legjobb választás? Miért a SerpentRace a legjobb választás?
</motion.h2> </motion.h2>
<div className="grid md:grid-cols-3 gap-8"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 sm:gap-8">
{/* Feature 1 */} {/* Feature 1 */}
<motion.div <motion.div
className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 text-center" className="bg-white/10 backdrop-blur-lg rounded-xl sm:rounded-2xl p-6 sm:p-8 text-center"
initial={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.7, delay: 0.3 }} transition={{ duration: 0.7, delay: 0.3 }}
> >
<div className="w-16 h-16 mx-auto mb-6 bg-emerald-500 rounded-full flex items-center justify-center"> <div className="w-12 h-12 sm:w-16 sm:h-16 mx-auto mb-4 sm:mb-6 bg-emerald-500 rounded-full flex items-center justify-center">
<FaUsers className="w-8 h-8 text-white" /> <FaUsers className="w-6 h-6 sm:w-8 sm:h-8 text-white" />
</div> </div>
<h3 className="text-lg font-semibold text-white mb-2">Közösségi élmény</h3> <h3 className="text-base sm:text-lg font-semibold text-white mb-2">Közösségi élmény</h3>
<p className="text-gray-300 text-sm"> <p className="text-gray-300 text-sm">
Ismerkedj, nevess, tanulj! A SerpentRace összehozza a társaságot, legyen szó baráti Ismerkedj, nevess, tanulj! A SerpentRace összehozza a társaságot, legyen szó baráti
összejövetelről vagy csapatépítésről. összejövetelről vagy csapatépítésről.
@@ -114,16 +119,16 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
{/* Feature 2 */} {/* Feature 2 */}
<motion.div <motion.div
className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 text-center" className="bg-white/10 backdrop-blur-lg rounded-xl sm:rounded-2xl p-6 sm:p-8 text-center"
initial={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.7, delay: 0.5 }} transition={{ duration: 0.7, delay: 0.5 }}
> >
<div className="w-16 h-16 mx-auto mb-6 bg-emerald-500 rounded-full flex items-center justify-center"> <div className="w-12 h-12 sm:w-16 sm:h-16 mx-auto mb-4 sm:mb-6 bg-emerald-500 rounded-full flex items-center justify-center">
<FaPaintBrush className="w-8 h-8 text-white" /> <FaPaintBrush className="w-6 h-6 sm:w-8 sm:h-8 text-white" />
</div> </div>
<h3 className="text-lg font-semibold text-white mb-2">Személyre szabható</h3> <h3 className="text-base sm:text-lg font-semibold text-white mb-2">Személyre szabható</h3>
<p className="text-gray-300 text-sm"> <p className="text-gray-300 text-sm">
Kérdéskártyák, szabályok, design minden a te igényeidhez igazítható, akár céges brandinggel Kérdéskártyák, szabályok, design minden a te igényeidhez igazítható, akár céges brandinggel
is! is!
@@ -132,16 +137,16 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
{/* Feature 3 */} {/* Feature 3 */}
<motion.div <motion.div
className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 text-center" className="bg-white/10 backdrop-blur-lg rounded-xl sm:rounded-2xl p-6 sm:p-8 text-center"
initial={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.7, delay: 0.7 }} transition={{ duration: 0.7, delay: 0.7 }}
> >
<div className="w-16 h-16 mx-auto mb-6 bg-emerald-500 rounded-full flex items-center justify-center"> <div className="w-12 h-12 sm:w-16 sm:h-16 mx-auto mb-4 sm:mb-6 bg-emerald-500 rounded-full flex items-center justify-center">
<FaHeadset className="w-8 h-8 text-white" /> <FaHeadset className="w-6 h-6 sm:w-8 sm:h-8 text-white" />
</div> </div>
<h3 className="text-lg font-semibold text-white mb-2">Folyamatos támogatás</h3> <h3 className="text-base sm:text-lg font-semibold text-white mb-2">Folyamatos támogatás</h3>
<p className="text-gray-300 text-sm"> <p className="text-gray-300 text-sm">
Gyors, segítőkész ügyfélszolgálat ha bármilyen kérdésed vagy problémád van, mindig Gyors, segítőkész ügyfélszolgálat ha bármilyen kérdésed vagy problémád van, mindig
számíthatsz ránk! számíthatsz ránk!
@@ -153,7 +158,7 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
{/* Call to Action Section */} {/* Call to Action Section */}
<motion.section <motion.section
className="py-20 px-4" className="py-12 sm:py-16 md:py-20 px-4 sm:px-6"
initial={{ opacity: 0, y: 40 }} initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.2 }} viewport={{ once: true, amount: 0.2 }}
@@ -161,25 +166,26 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
> >
<div className="max-w-4xl mx-auto text-center"> <div className="max-w-4xl mx-auto text-center">
<motion.div <motion.div
className="bg-gradient-to-r from-emerald-500/20 to-green-500/20 backdrop-blur-lg rounded-3xl p-12" className="bg-gradient-to-r from-emerald-500/20 to-green-500/20 backdrop-blur-lg rounded-2xl sm:rounded-3xl p-6 sm:p-8 md:p-12"
initial={{ opacity: 0, scale: 0.95 }} initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }} whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }} viewport={{ once: true }}
transition={{ duration: 0.7, delay: 0.3 }} transition={{ duration: 0.7, delay: 0.3 }}
> >
<h2 className="text-2xl md:text-3xl font-bold text-white mb-4"> <h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-white mb-3 sm:mb-4 px-2">
Próbáld ki te is a SerpentRace-t! Próbáld ki te is a SerpentRace-t!
</h2> </h2>
<p className="text-lg text-gray-300 mb-6"> <p className="text-base sm:text-lg text-gray-300 mb-4 sm:mb-6 px-2">
Legyél részese egy új közösségi élménynek, vagy rendeld meg saját, személyre szabott Legyél részese egy új közösségi élménynek, vagy rendeld meg saját, személyre szabott
társasjátékodat mi mindenben segítünk! társasjátékodat mi mindenben segítünk!
</p> </p>
<ButtonGreen <ButtonGreen
text="Kapcsolatfelvétel" text="Kapcsolatfelvétel"
onClick={onNavigateToContacts} onClick={goAbout}
className="px-12 py-4 text-xl font-bold" className="px-8 sm:px-12 py-3 sm:py-4 text-lg sm:text-xl font-bold"
width="w-full sm:w-auto"
/> />
</motion.div> </motion.div>
</div> </div>
@@ -38,19 +38,20 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
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 overflow-hidden" className="w-[95%] max-w-6xl mx-auto my-8 md:my-16 flex flex-col md:flex-row items-center justify-center rounded-2xl md: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%)",
}} }}
> >
{/* Bal oldali animáció/kép */} {/* Bal oldali animáció/kép */}
<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-6 md:py-10 md:pl-10">
<div className="w-[200px] h-[200px] sm:w-[300px] sm:h-[300px] md:w-[420px] md:h-[420px]">
<LogoCard <LogoCard
imageSrc={logoImg} imageSrc={logoImg}
containerHeight="420px" containerHeight="100%"
containerWidth="420px" containerWidth="100%"
imageHeight="420px" imageHeight="100%"
imageWidth="420px" imageWidth="100%"
rotateAmplitude={7} rotateAmplitude={7}
scaleOnHover={1.03} scaleOnHover={1.03}
showMobileWarning={false} showMobileWarning={false}
@@ -58,23 +59,24 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
displayOverlayContent={false} displayOverlayContent={false}
/> />
</div> </div>
</div>
{/* Jobb oldali panel */} {/* Jobb oldali panel */}
<div className="flex-1 w-full flex items-center justify-center px-6 md:px-12 py-8"> <div className="flex-1 w-full flex items-center justify-center px-4 sm:px-6 md:px-12 py-6 md:py-8">
<div <div
className="w-full max-w-md rounded-2xl p-6 md:p-8 flex flex-col gap-6" className="w-full max-w-md rounded-xl md:rounded-2xl p-4 sm:p-6 md:p-8 flex flex-col gap-4 md:gap-6"
style={{ background: "rgba(0,0,0,0.15)", backdropFilter: "blur(6px)" }} style={{ background: "rgba(0,0,0,0.15)", backdropFilter: "blur(6px)" }}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{username ? ( {username ? (
<div className="flex items-center gap-3"> <div className="flex items-center gap-2 md:gap-3">
<div <div
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold" className="w-8 h-8 md:w-10 md:h-10 rounded-full flex items-center justify-center text-xs md:text-sm font-semibold"
style={{ background: "rgba(34,197,94,0.12)", color: "var(--color-mint)" }} style={{ background: "rgba(34,197,94,0.12)", color: "var(--color-mint)" }}
> >
{initials} {initials}
</div> </div>
<div className="text-[32px]" style={{ color: "var(--color-muted, #cbd5e1)" }}> <div className="text-xl sm:text-2xl md:text-[32px]" style={{ color: "var(--color-muted, #cbd5e1)" }}>
<span className="font-medium" style={{ color: "var(--color-text, #fff)" }}> <span className="font-medium" style={{ color: "var(--color-text, #fff)" }}>
{username} {username}
</span> </span>
@@ -82,7 +84,7 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
</div> </div>
) : ( ) : (
<div className="w-full"> <div className="w-full">
<div className="font-semibold mb-3 text-text">Nincs bejelentkezve játssz vendégként:</div> <div className="font-semibold mb-2 md:mb-3 text-sm md:text-base text-text">Nincs bejelentkezve játssz vendégként:</div>
<InputBoxDark <InputBoxDark
type="text" type="text"
placeholder="Nickname..." placeholder="Nickname..."
@@ -99,7 +101,7 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
</div> </div>
<div> <div>
<h2 className="font-semibold mb-3 text-text">Csatlakozás játékhoz</h2> <h2 className="font-semibold mb-2 md:mb-3 text-sm md:text-base text-text">Csatlakozás játékhoz</h2>
<div className={`${error ? "border border-error rounded-lg p-2" : ""}`}> <div className={`${error ? "border border-error rounded-lg p-2" : ""}`}>
<InputBoxDark <InputBoxDark
type="text" type="text"
@@ -110,15 +112,15 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
/> />
</div> </div>
{error && <div className="text-xs mt-2 text-error">{error}</div>} {error && <div className="text-xs mt-2 text-error">{error}</div>}
<div className="mt-4"> <div className="mt-3 md: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>
{username ? ( {username ? (
<div className="border-t border-white/10 pt-4"> <div className="border-t border-white/10 pt-3 md:pt-4">
{username && ( {username && (
<div> <div>
<h3 className="font-semibold mb-3 text-text">Új játék létrehozása</h3> <h3 className="font-semibold mb-2 md:mb-3 text-sm md:text-base 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>
)} )}
@@ -2,17 +2,16 @@ import React, { useState } from "react"
import Logo from "../../assets/pictures/Logo" import Logo from "../../assets/pictures/Logo"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate" // importáld a navigációs hookot import HandleNavigate from "../../utils/HandleNavigate/HandleNavigate" // importáld a navigációs hookot
import { FaSignOutAlt, FaChartBar, FaUser, FaBars } from "react-icons/fa"
const navLinkClass = const navLinkClass = "px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10"
"px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10"
const navLinkClassPlay = const navLinkClassPlay =
"px-4 py-2 rounded-lg text-white bg-white/12 hover:bg-white/20 transition-all duration-200" "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 isLoggedIn = Boolean( const [userMenuOpen, setUserMenuOpen] = useState(false)
localStorage.getItem("authLevel") && localStorage.getItem("username") const isLoggedIn = Boolean(localStorage.getItem("authLevel") && localStorage.getItem("username"))
)
// Használjuk a HandleNavigate hookot // Használjuk a HandleNavigate hookot
const { goLanding, goAbout, goHome, goLogin, goContacts } = HandleNavigate() const { goLanding, goAbout, goHome, goLogin, goContacts } = HandleNavigate()
@@ -25,16 +24,23 @@ const Navbar = () => {
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-0"> {/* removed mx-auto and px-* classes; use full width + responsive padding-inline using clamp */}
<div
className="w-full"
style={{
/* minimális padding 12px, növel a képernyővel, max 30px */
paddingInline: "clamp(12px, calc((100vw - 1024px) / 2), 80px)",
}}
>
<div className="flex justify-between h-16 items-center"> <div className="flex justify-between h-16 items-center">
{/* Logo + Brand */} {/* Logo + Brand */}
<div className="flex-shrink-0 flex items-center gap-2"> <div className="flex-shrink-0 flex items-center gap-2">
<button onClick={goLanding} className="flex items-center mt-1 h-9"> <button onClick={goLanding} className=" cursor-pointer flex items-center mt-1 h-9">
<Logo size={36} /> <Logo size={36} />
</button> </button>
<button <button
onClick={goLanding} onClick={goLanding}
className="flex items-center h-9 text-white font-bold text-2xl tracking-tight" className="cursor-pointer flex items-center h-9 text-white font-bold text-2xl tracking-tight"
> >
SerpentRace SerpentRace
</button> </button>
@@ -43,22 +49,16 @@ const Navbar = () => {
{/* Desktop Menu */} {/* Desktop Menu */}
<div className="hidden md:flex items-center space-x-6"> <div className="hidden md:flex items-center space-x-6">
{/* Bal oldali linkek */} {/* Bal oldali linkek */}
<button onClick={goLanding} className={navLinkClass}>
Főoldal
</button>
<button onClick={goAbout} className={navLinkClass}> <button onClick={goAbout} className={navLinkClass}>
Rólunk Rólunk
</button> </button>
<button onClick={goContacts} className={navLinkClass}>
Kapcsolat
</button>
{/* Csak bejelentkezve */} {/* Csak bejelentkezve */}
{isLoggedIn && ( {isLoggedIn && (
<> <>
<Link to="/decks" className={navLinkClass}>Paklik</Link> <Link to="/decks" className={navLinkClass}>
<Link to="/report" className={navLinkClass}>Statisztikák</Link> Paklik
<Link to="/profile" className={navLinkClass}>Profil</Link> </Link>
</> </>
)} )}
@@ -88,26 +88,54 @@ const Navbar = () => {
</Link> </Link>
</div> </div>
) : ( ) : (
// Ha be van jelentkezve: mutatunk egy kis lenyíló gombot (ikon), ami tartalmazza a Profil, Statisztikák és Kijelentkezés linkeket
<div className="relative">
<button <button
onClick={handleLogout} onClick={() => setUserMenuOpen((s) => !s)}
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" 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 flex items-center gap-2"
title="Kijelentkezés" title="Felhasználói menü"
> >
<svg <FaBars className="h-5 w-5" />
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> </button>
{userMenuOpen && (
<div className="absolute right-0 mt-2 w-44 bg-white/5 backdrop-blur-sm border border-gray-700 rounded-lg shadow-lg overflow-hidden z-40">
<button
onClick={() => {
setUserMenuOpen(false)
// navigálás statisztikák
goLanding // fallback, majd ha kell: goReport
}}
className="w-full flex items-center gap-3 px-4 py-2 hover:bg-white/10 text-white"
>
<FaChartBar className="w-4 h-4" />
<span className="text-sm">Statisztikák</span>
</button>
<button
onClick={() => {
setUserMenuOpen(false)
// profil
window.location.href = "/profile"
}}
className="w-full flex items-center gap-3 px-4 py-2 hover:bg-white/10 text-white"
>
<FaUser className="w-4 h-4" />
<span className="text-sm">Profil</span>
</button>
<div className="h-px bg-white/5" />
<button
onClick={() => {
setUserMenuOpen(false)
handleLogout()
}}
className="w-full flex items-center gap-3 px-4 py-2 hover:bg-white/10 text-white"
>
<FaSignOutAlt className="w-4 h-4" />
<span className="text-sm">Kijelentkezés</span>
</button>
</div>
)}
</div>
)} )}
</div> </div>
@@ -133,12 +161,7 @@ const Navbar = () => {
d="M6 18L18 6M6 6l12 12" d="M6 18L18 6M6 6l12 12"
/> />
) : ( ) : (
<path <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8h16M4 16h16" />
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 8h16M4 16h16"
/>
)} )}
</svg> </svg>
</button> </button>
@@ -149,24 +172,50 @@ const Navbar = () => {
{/* 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">
<button onClick={() => { goLanding(); setMenuOpen(false) }} className={navLinkClass}>Főoldal</button> <button
<button onClick={() => { goAbout(); setMenuOpen(false) }} className={navLinkClass}>Rólunk</button> onClick={() => {
<button onClick={() => { goContacts(); setMenuOpen(false) }} className={navLinkClass}>Kapcsolat</button> goAbout()
setMenuOpen(false)
}}
className={navLinkClass}
>
Rólunk
</button>
<button
onClick={() => {
goContacts()
setMenuOpen(false)
}}
className={navLinkClass}
>
Kapcsolat
</button>
{isLoggedIn && ( {isLoggedIn && (
<> <>
<Link to="/decks" onClick={() => setMenuOpen(false)} className={navLinkClass}>Paklik</Link> <Link to="/decks" onClick={() => setMenuOpen(false)} className={navLinkClass}>
<Link to="/report" onClick={() => setMenuOpen(false)} className={navLinkClass}>Statisztikák</Link> Paklik
<Link to="/profile" onClick={() => setMenuOpen(false)} className={navLinkClass}>Profil</Link> </Link>
</> </>
)} )}
<button onClick={() => { goHome(); setMenuOpen(false) }} className={navLinkClassPlay}>Játék</button> <button
onClick={() => {
goHome()
setMenuOpen(false)
}}
className={navLinkClassPlay}
>
Játék
</button>
{!isLoggedIn ? ( {!isLoggedIn ? (
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<button <button
onClick={() => { goLogin(); setMenuOpen(false) }} onClick={() => {
goLogin()
setMenuOpen(false)
}}
className="block px-3 py-2 rounded-lg hover:bg-white/20 text-white font-semibold transition-all" className="block px-3 py-2 rounded-lg hover:bg-white/20 text-white font-semibold transition-all"
> >
Bejelentkezés Bejelentkezés
@@ -184,31 +233,17 @@ const Navbar = () => {
</Link> </Link>
</div> </div>
) : ( ) : (
<div className="flex justify-end px-2 pb-2">
<button <button
onClick={() => { onClick={() => {
handleLogout() handleLogout()
setMenuOpen(false) 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" className="flex items-center gap-2 px-3 py-2 rounded-lg bg-red-600 hover:bg-red-700 text-white transition-all"
title="Kijelentkezés" title="Kijelentkezés"
> >
<svg <FaSignOutAlt className="h-4 w-4" />
xmlns="http://www.w3.org/2000/svg" <span>Kijelentkezés</span>
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> </button>
</div>
)} )}
</div> </div>
)} )}
@@ -1,4 +1,4 @@
import React, { useEffect } from "react" import React, { useEffect, useState } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { import {
FaUser, FaUser,
@@ -11,9 +11,11 @@ import {
FaTimes, FaTimes,
FaEdit FaEdit
} from "react-icons/fa" } from "react-icons/fa"
import { getUserProfile } from "../../api/userApi"
export default function DeckInfoPopUp({ deck, onClose }) { export default function DeckInfoPopUp({ deck, onClose }) {
const navigate = useNavigate() const navigate = useNavigate()
const [currentUser, setCurrentUser] = useState(null)
if (!deck) return null if (!deck) return null
@@ -32,6 +34,25 @@ export default function DeckInfoPopUp({ deck, onClose }) {
} }
}, []) }, [])
// Load current user to decide if Edit button should be shown
useEffect(() => {
let mounted = true
const loadUser = async () => {
try {
const data = await getUserProfile()
console.log('👤 Loaded current user:', data)
if (mounted) setCurrentUser(data)
} catch (e) {
// silently ignore - edit button will be hidden for anonymous
console.warn('Could not fetch current user profile for DeckInfoPopUp:', e)
}
}
loadUser()
return () => { mounted = false }
}, [])
// Backend enum mapping // Backend enum mapping
const deckTypeMapping = { const deckTypeMapping = {
0: { label: "Szerencse", color: "var(--color-luck)" }, // LUCK 0: { label: "Szerencse", color: "var(--color-luck)" }, // LUCK
@@ -106,8 +127,19 @@ export default function DeckInfoPopUp({ deck, onClose }) {
} }
const handleOpenDeck = () => { const handleOpenDeck = () => {
// TODO: Megnyitás funkció - később implementálható // Get the deck ID from raw data
alert("⚠️ A pakli megnyitás funkció még fejlesztés alatt áll!") const deckId = rawData.id || deck.id
if (!deckId) {
alert("⚠️ Hiba: A pakli azonosítója nem található!")
return
}
// Navigate to card display page
navigate(`/deck/${deckId}`)
// Close the popup
onClose()
} }
const handleEditDeck = () => { const handleEditDeck = () => {
@@ -126,6 +158,50 @@ export default function DeckInfoPopUp({ deck, onClose }) {
onClose() onClose()
} }
// Determine whether the current user can edit this deck
// Option 1: Use backend's 'editable' flag if available (ShortDeckDto)
// Option 2: Check userid field (DetailDeckDto) or compare user names
// Check if user is admin (state === 5)
const isAdmin = currentUser ? Number(currentUser.state) === 5 : false
// Check if deck is editable (backend provides this in ShortDeckDto)
const backendEditableFlag = rawData.editable === true
// Fallback: Check if user is the owner by userid (DetailDeckDto) or username
const deckOwnerId = rawData.userid // Only available in DetailDeckDto
const deckCreatorName = rawData.creator // Available in ShortDeckDto (username)
const isOwnerById = currentUser && deckOwnerId
? (String(currentUser.id) === String(deckOwnerId))
: false
const isOwnerByName = currentUser && deckCreatorName
? (currentUser.username === deckCreatorName)
: false
// User can edit if:
// 1. Backend says it's editable (ShortDeckDto has editable flag)
// 2. User is the owner (by ID or username)
// 3. User is an admin (state === 5)
const canEdit = backendEditableFlag || isOwnerById || isOwnerByName || isAdmin
// Debug: Check permission logic
console.log('🔍 Permission Check:', {
'Has currentUser?': !!currentUser,
'currentUser.id': currentUser?.id,
'currentUser.username': currentUser?.username,
'currentUser.state': currentUser?.state,
'deckOwnerId (userid)': deckOwnerId,
'deckCreatorName': deckCreatorName,
'backendEditableFlag': backendEditableFlag,
'isOwnerById': isOwnerById,
'isOwnerByName': isOwnerByName,
'isAdmin': isAdmin,
'canEdit': canEdit,
'rawData': rawData
})
return ( return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 backdrop-blur-sm"> <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 backdrop-blur-sm">
<div <div
@@ -254,7 +330,7 @@ export default function DeckInfoPopUp({ deck, onClose }) {
{/* Action buttons */} {/* Action buttons */}
<div className="mt-5 pt-4 border-t border-[color:var(--color-surface-selected)]"> <div className="mt-5 pt-4 border-t border-[color:var(--color-surface-selected)]">
<div className="grid grid-cols-2 gap-3"> <div className={`grid gap-3 ${canEdit ? 'grid-cols-2' : 'grid-cols-1'}`}>
{/* Open button */} {/* Open button */}
<button <button
className="px-4 py-2.5 rounded-xl font-semibold text-[color:var(--color-text-inverse)] transition-all duration-200 hover:scale-105 shadow-lg bg-[color:var(--color-success)] hover:bg-[color:var(--color-success)]/80" className="px-4 py-2.5 rounded-xl font-semibold text-[color:var(--color-text-inverse)] transition-all duration-200 hover:scale-105 shadow-lg bg-[color:var(--color-success)] hover:bg-[color:var(--color-success)]/80"
@@ -263,7 +339,8 @@ export default function DeckInfoPopUp({ deck, onClose }) {
Megnyitás Megnyitás
</button> </button>
{/* Edit button */} {/* Edit button - only visible to owner or admin (state === 5) */}
{canEdit && (
<button <button
className="px-4 py-2.5 rounded-xl font-semibold text-[color:var(--color-text)] border border-[color:var(--color-surface-selected)] transition-all duration-200 hover:scale-105 hover:bg-[color:var(--color-surface-selected)] flex items-center justify-center gap-2" className="px-4 py-2.5 rounded-xl font-semibold text-[color:var(--color-text)] border border-[color:var(--color-surface-selected)] transition-all duration-200 hover:scale-105 hover:bg-[color:var(--color-surface-selected)] flex items-center justify-center gap-2"
onClick={handleEditDeck} onClick={handleEditDeck}
@@ -271,6 +348,7 @@ export default function DeckInfoPopUp({ deck, onClose }) {
<FaEdit className="text-sm" /> <FaEdit className="text-sm" />
Szerkesztés Szerkesztés
</button> </button>
)}
</div> </div>
</div> </div>
</div> </div>
+241 -49
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react" import React, { useState } from "react"
import Navbar from "../../components/Navbar/Navbar" import Navbar from "../../components/Navbar/Navbar"
import Footer from "../../components/Footer/Footer" import Footer from "../../components/Footer/Footer"
import Background from "../../assets/backgrounds/Background.jsx" import Background from "../../assets/backgrounds/Background.jsx"
@@ -9,10 +9,53 @@ import Zsola from "../../assets/pictures/zsola.JPG"
import Donat from "../../assets/pictures/donat.JPG" import Donat from "../../assets/pictures/donat.JPG"
import Turo from "../../assets/pictures/turo.JPG" import Turo from "../../assets/pictures/turo.JPG"
import Piskor from "../../assets/pictures/piskor.JPG" import Piskor from "../../assets/pictures/piskor.JPG"
// ÚJ: kész ikonok használata
import { FaLightbulb, FaUsers, FaShieldAlt } from "react-icons/fa"
// ÚJ: framer-motion használata a Landing-hez hasonló megjelenésért
import { motion } from "framer-motion"
///// ÚJ: központosított motion beállítások /////
const baseMotion = {
initial: { opacity: 0, y: 20 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, amount: 0.2 },
transition: { duration: 0.8 },
}
const getMotionProps = (overrides = {}) => {
const { transition = {}, viewport = {}, ...rest } = overrides
return {
...baseMotion,
...rest,
transition: { ...baseMotion.transition, ...transition },
viewport: { ...baseMotion.viewport, ...viewport },
}
}
///// ÚJ: variánsok a szülő-gyermek animációhoz (stagger) /////
// container: használható olyan szekciókra, ahol a children együttesen animálódjanak
const containerVariant = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.06,
// delayChildren beállítható egyedi késleltetéshez
},
},
}
// item: alap animáció minden gyermek elemhez
const itemVariant = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6 } },
}
const About = () => { const About = () => {
const [visible, setVisible] = useState(false) // Új: kontakt űrlap state + státusz
const sectionRef = useRef(null) const [contactName, setContactName] = useState("")
const [contactEmail, setContactEmail] = useState("")
const [contactMessage, setContactMessage] = useState("")
const [submitting, setSubmitting] = useState(false)
const [success, setSuccess] = useState("")
const teamMembers = [ const teamMembers = [
{ {
@@ -52,20 +95,35 @@ const About = () => {
}, },
] ]
useEffect(() => { // Új: egyszerű onSubmit kezelő (nem változott logika)
const observer = new IntersectionObserver( const handleContactSubmit = (e) => {
([entry]) => { e.preventDefault()
if (entry.isIntersecting) setVisible(true) if (!contactName.trim() || !contactEmail.trim() || !contactMessage.trim()) {
}, setSuccess("Kérlek tölts ki minden mezőt.")
{ threshold: 0.3 } return
) }
if (sectionRef.current) observer.observe(sectionRef.current) // nagyon egyszerű email ellenőrzés
return () => observer.disconnect() const emailRe = /\S+@\S+\.\S+/
}, []) if (!emailRe.test(contactEmail)) {
setSuccess("Kérlek adj meg érvényes email címet.")
return
}
setSubmitting(true)
setSuccess("")
// Itt lehetne API hívás; most csak szimuláljuk
setTimeout(() => {
setSubmitting(false)
setContactName("")
setContactEmail("")
setContactMessage("")
setSuccess("Köszönjük, üzenetedet megkaptuk!")
setTimeout(() => setSuccess(""), 5000)
}, 900)
}
return ( return (
<div className="flex flex-col min-h-screen overflow-y-auto relative"> <div className="flex flex-col min-h-screen overflow-y-auto relative">
{/* Háttér fix pozíció, a teljes képernyőre */} {/* Háttér fix pozíció, a teljes képernyőre */}
<div className="fixed top-0 left-0 w-full h-full z-[-10]"> <div className="fixed top-0 left-0 w-full h-full z-[-10]">
<Background /> <Background />
@@ -78,7 +136,6 @@ const About = () => {
{/* Tartalom */} {/* Tartalom */}
<main className="flex-grow text-white px-6 pt-16 mt-0 mb-20"> <main className="flex-grow text-white px-6 pt-16 mt-0 mb-20">
{/* Vissza gomb */} {/* Vissza gomb */}
<div className="fixed top-4 left-4 z-50 group"> <div className="fixed top-4 left-4 z-50 group">
<div className="absolute top-full mt-1 left-1/2 -translate-x-1/2 scale-0 group-hover:scale-100 transition transform bg-zinc-800 text-sm text-white px-3 py-1 rounded shadow-lg"> <div className="absolute top-full mt-1 left-1/2 -translate-x-1/2 scale-0 group-hover:scale-100 transition transform bg-zinc-800 text-sm text-white px-3 py-1 rounded shadow-lg">
@@ -86,56 +143,109 @@ const About = () => {
</div> </div>
</div> </div>
<section {/* Általános konténer — framer-motion mint a Landingnél */}
ref={sectionRef} <motion.section className="max-w-5xl mx-auto" {...getMotionProps({ transition: { duration: 0.7 } })}>
className={`max-w-5xl mx-auto transition-all duration-1000 ease-out ${
visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
}`}
>
{/* Rólunk cím */} {/* Rólunk cím */}
<h1 className="mt-24 text-5xl font-extrabold text-green-300 mb-10 text-center tracking-wide drop-shadow-lg"> <motion.h1
className="mt-24 text-5xl font-extrabold text-green-300 mb-10 text-center tracking-wide drop-shadow-lg"
{...getMotionProps({ transition: { duration: 0.8, delay: 0.1 } })}
>
<span className="inline-block animate-pulse mr-2"></span> Rólunk <span className="inline-block animate-pulse mr-2"></span> Rólunk
</h1> </motion.h1>
{/* Leírás */} {/* Leírás */}
<p className="text-lg leading-relaxed text-zinc-200 mb-10 text-center max-w-3xl mx-auto"> <motion.p
Célunk, hogy egy innovatív, közösségorientált platformot építsünk, ahol a versenyzés, játék és technológia találkozik. Elhivatott csapatunk minden nap azon dolgozik, hogy élményt és értéket nyújtson a felhasználóinknak. className="text-lg leading-relaxed text-zinc-200 mb-10 text-center max-w-3xl mx-auto"
</p> {...getMotionProps({ transition: { duration: 0.8, delay: 0.2 } })}
>
Célunk, hogy egy innovatív, közösségorientált platformot építsünk, ahol a versenyzés, játék és
technológia találkozik. Elhivatott csapatunk minden nap azon dolgozik, hogy élményt és értéket
nyújtson a felhasználóinknak.
</motion.p>
{/* Küldetésünk */} {/* Küldetésünk — most szülő variánssal, hogy a kártyák ne triggereljenek külön és ne ugorjanak */}
<div className="mt-12"> <motion.div
className="mt-12"
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
variants={{
hidden: {},
visible: { transition: { staggerChildren: 0.08, delayChildren: 0.12 } },
}}
>
<h2 className="text-2xl font-bold text-green-300 mb-4">Küldetésünk</h2> <h2 className="text-2xl font-bold text-green-300 mb-4">Küldetésünk</h2>
<div className="grid md:grid-cols-3 gap-6">
<div className="bg-zinc-800 rounded-xl p-6 shadow-lg hover:scale-105 transition">
<h3 className="text-xl font-semibold mb-2">Innováció</h3>
<p className="text-zinc-300">Folyamatosan fejlesztjük rendszereinket a legmodernebb technológiákkal.</p>
</div>
<div className="bg-zinc-800 rounded-xl p-6 shadow-lg hover:scale-105 transition">
<h3 className="text-xl font-semibold mb-2">Közösség</h3>
<p className="text-zinc-300">Fontos számunkra, hogy egy összetartó, aktív közösséget építsünk ki.</p>
</div>
<div className="bg-zinc-800 rounded-xl p-6 shadow-lg hover:scale-105 transition">
<h3 className="text-xl font-semibold mb-2">Minőség</h3>
<p className="text-zinc-300">Minden részletre figyelünk a felhasználói élmény és biztonság érdekében.</p>
</div>
</div>
</div>
{/* Csapat */} {/* Új: stilizált kártyák (egyenként animálva a szülőből) */}
<div className="mt-16"> <div className="grid gap-6 md:grid-cols-3">
<motion.div
className="group bg-zinc-900/40 border border-gray-700 rounded-2xl p-6 shadow-lg transform transition hover:-translate-y-2"
variants={itemVariant}
>
<div className="w-16 h-16 mx-auto rounded-full bg-gradient-to-br from-emerald-400 to-green-600 flex items-center justify-center mb-4">
{/* ICON: Lightbulb */}
<FaLightbulb className="w-8 h-8 text-black" />
</div>
<h3 className="text-lg font-semibold text-center mb-2">Innováció</h3>
<p className="text-zinc-300 text-center">
Modern megoldásokkal gyorsítjuk a fejlődést moduláris, skálázható rendszereket építünk.
</p>
</motion.div>
<motion.div
className="group bg-zinc-900/40 border border-gray-700 rounded-2xl p-6 shadow-lg transform transition hover:-translate-y-2"
variants={itemVariant}
>
<div className="w-16 h-16 mx-auto rounded-full bg-gradient-to-br from-sky-400 to-blue-600 flex items-center justify-center mb-4">
{/* ICON: Users */}
<FaUsers className="w-8 h-8 text-black" />
</div>
<h3 className="text-lg font-semibold text-center mb-2">Közösség</h3>
<p className="text-zinc-300 text-center">
Közösségépítés és együttműködéseszközök, amik bevonják és motiválják a felhasználókat.
</p>
</motion.div>
<motion.div
className="group bg-zinc-900/40 border border-gray-700 rounded-2xl p-6 shadow-lg transform transition hover:-translate-y-2"
variants={itemVariant}
>
<div className="w-16 h-16 mx-auto rounded-full bg-gradient-to-br from-yellow-400 to-orange-500 flex items-center justify-center mb-4">
{/* ICON: Shield/Quality */}
<FaShieldAlt className="w-8 h-8 text-black" />
</div>
<h3 className="text-lg font-semibold text-center mb-2">Minőség</h3>
<p className="text-zinc-300 text-center">
Biztonság, megbízhatóság és gondosan tervezett UX minden részlet számít.
</p>
</motion.div>
</div>
</motion.div>
{/* Csapat — a szülő kezeli a gyermekek animációját (stagger), így nincs külön "belülről" trigger */}
<motion.div
className="mt-16"
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
variants={{
hidden: {},
visible: { transition: { staggerChildren: 0.06, delayChildren: 0.25 } },
}}
>
<h2 className="text-2xl font-bold text-green-300 mb-6">Csapatunk</h2> <h2 className="text-2xl font-bold text-green-300 mb-6">Csapatunk</h2>
<div className="grid md:grid-cols-3 gap-8"> <div className="grid md:grid-cols-3 gap-8">
{teamMembers.map((member, i) => { {teamMembers.map((member, i) => {
const isLast = i === teamMembers.length - 1 const isLast = i === teamMembers.length - 1
const itemsInLastRow = teamMembers.length % 3 const itemsInLastRow = teamMembers.length % 3
const shouldCenter = itemsInLastRow === 1 && isLast const shouldCenter = itemsInLastRow === 1 && isLast
return ( return (
<div <motion.div
key={i} key={i}
className={`flex flex-col items-center text-center bg-zinc-800 p-6 rounded-xl shadow-lg hover:shadow-green-400/20 hover:scale-105 transition ${ className={`flex flex-col items-center text-center bg-zinc-800 p-6 rounded-xl shadow-lg hover:shadow-green-400/20 hover:scale-105 transition ${
shouldCenter ? "md:col-start-2" : "" shouldCenter ? "md:col-start-2" : ""
}`} }`}
variants={itemVariant}
> >
<img <img
src={member.photo} src={member.photo}
@@ -144,12 +254,94 @@ const About = () => {
/> />
<h4 className="text-lg font-bold">{member.name}</h4> <h4 className="text-lg font-bold">{member.name}</h4>
<p className="text-zinc-400">{member.role}</p> <p className="text-zinc-400">{member.role}</p>
</div> </motion.div>
) )
})} })}
</div> </div>
</motion.div>
{/* Új: Kapcsolat szekció — egységes megjelenés (animálva) */}
<motion.section
id="contact"
className="mt-16 bg-transparent"
{...getMotionProps({ transition: { duration: 0.8, delay: 0.25 } })}
>
<div className="max-w-5xl mx-auto bg-white/5 rounded-2xl border border-gray-700 p-8 shadow-lg">
<h2 className="text-3xl font-bold mb-6 text-center border-b-4 border-emerald-400 pb-2 text-white">
Kapcsolat
</h2>
<div className="grid md:grid-cols-2 gap-8">
{/* Bal: elérhetőségek */}
<div className="flex flex-col justify-center gap-6">
<div className="bg-zinc-800 rounded-xl p-5 shadow-md flex items-start gap-4">
<div className="bg-emerald-500 text-black rounded-lg p-3 text-xl flex-shrink-0"></div>
<div>
<div className="font-semibold">Email</div>
<div className="text-zinc-300">hello@serpentrace.hu</div>
</div> </div>
</section> </div>
<div className="bg-zinc-800 rounded-xl p-5 shadow-md flex items-start gap-4">
<div className="bg-emerald-500 text-black rounded-lg p-3 text-xl flex-shrink-0">📞</div>
<div>
<div className="font-semibold">Telefon</div>
<div className="text-zinc-300">+36 20 123 4567</div>
</div>
</div>
<div className="bg-zinc-800 rounded-xl p-5 shadow-md flex items-start gap-4">
<div className="bg-emerald-500 text-black rounded-lg p-3 text-xl flex-shrink-0">📍</div>
<div>
<div className="font-semibold">Iroda</div>
<div className="text-zinc-300">Budapest, Magyarország</div>
</div>
</div>
<p className="text-zinc-400 mt-2 text-sm">
Általános megkeresésekre 48 órán belül válaszolunk. Ha céges együttműködésről van szó,
kérjük, írj részletesen a projektedről.
</p>
</div>
{/* Jobb: űrlap */}
<form onSubmit={handleContactSubmit} className="space-y-4">
<input
value={contactName}
onChange={(e) => setContactName(e.target.value)}
type="text"
placeholder="Név"
className="w-full bg-white/10 text-white placeholder-white px-4 py-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-400"
/>
<input
value={contactEmail}
onChange={(e) => setContactEmail(e.target.value)}
type="email"
placeholder="Email"
className="w-full bg-white/10 text-white placeholder-white px-4 py-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-400"
/>
<textarea
value={contactMessage}
onChange={(e) => setContactMessage(e.target.value)}
placeholder="Üzenet"
rows="5"
className="w-full bg-white/10 text-white placeholder-white px-4 py-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-400"
/>
<div className="flex items-center gap-4">
<button
type="submit"
disabled={submitting}
className="bg-emerald-500 hover:bg-emerald-400 text-black px-6 py-3 rounded-lg font-bold transition disabled:opacity-60"
>
{submitting ? "Küldés..." : "Küldés"}
</button>
{success && <div className="text-sm text-zinc-300">{success}</div>}
</div>
</form>
</div>
</div>
</motion.section>
</motion.section>
</main> </main>
{/* Footer (nem scrollozható alá) */} {/* Footer (nem scrollozható alá) */}
@@ -7,7 +7,7 @@ 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, getDeckById, updateDeck } from '../../api/deckApi' import { createDeck, getDeckById, updateDeck, deleteDeck } from '../../api/deckApi'
import { notifySuccess, notifyError, notifyWarning } from "../../components/Toastify/toastifyServices" import { notifySuccess, notifyError, notifyWarning } from "../../components/Toastify/toastifyServices"
export default function DeckCreator() { export default function DeckCreator() {
@@ -29,6 +29,7 @@ export default function DeckCreator() {
const [isCreatingCard, setIsCreatingCard] = useState(false) const [isCreatingCard, setIsCreatingCard] = useState(false)
const [newCardType, setNewCardType] = useState(null) const [newCardType, setNewCardType] = useState(null)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
// Betöltés API-ból // Betöltés API-ból
useEffect(() => { useEffect(() => {
@@ -66,13 +67,23 @@ export default function DeckCreator() {
2: 'organization' 2: 'organization'
} }
// Process cards: convert type field from number to string
const processedCards = (deckData.cards || []).map(card => {
// A kártya type mezője a deck type-ját tükrözi (backend így küldi)
// Ezért a deck type alapján állítjuk be
return {
...card,
type: typeMapping[deckData.type] || 'QUESTION'
}
})
setDeck({ setDeck({
id: deckData.id, id: deckData.id,
name: deckData.name || "Névtelen pakli", name: deckData.name || "Névtelen pakli",
type: typeMapping[deckData.type] || 'QUESTION', type: typeMapping[deckData.type] || 'QUESTION',
privacy: ctypeMapping[deckData.ctype] || 'private', privacy: ctypeMapping[deckData.ctype] || 'private',
description: deckData.description || "", description: deckData.description || "",
cards: deckData.cards || [], cards: processedCards,
creationdate: deckData.creationdate, creationdate: deckData.creationdate,
updatedate: deckData.updatedate updatedate: deckData.updatedate
}) })
@@ -93,6 +104,71 @@ export default function DeckCreator() {
const handleSaveDeck = async () => { const handleSaveDeck = async () => {
try { try {
// Szűrjük ki a nem megfelelő típusú kártyákat
const validCards = deck.cards.filter(card => card.type === deck.type)
const invalidCardsCount = deck.cards.length - validCards.length
// Ha voltak érvénytelen kártyák, frissítsük a state-et
if (invalidCardsCount > 0) {
setDeck(prev => ({
...prev,
cards: validCards
}))
// Értesítés a törölt kártyákról
notifyWarning(`${invalidCardsCount} db nem megfelelő típusú kártya törölve a mentés előtt.`)
}
// Tisztítsuk meg a kártyákat - konvertáljuk a backend által várt formátumra
const cleanedCards = validCards.map(card => {
// Card subType mapping to backend CardType enum
const cardTypeMapping = {
'quiz': 0, // QUIZ
'pairing': 1, // SENTENCE_PAIRING
'text': 2, // OWN_ANSWER
'truefalse': 3, // TRUE_FALSE
'closer': 4 // CLOSER
}
// Kezdjük az ID-val (ha van)
const cleanedCard = {}
if (card.id) {
cleanedCard.id = card.id
}
// Ha van subType (QUESTION típusú kártyáknál), akkor add hozzá a type mezőt
if (card.subType && cardTypeMapping[card.subType] !== undefined) {
cleanedCard.type = cardTypeMapping[card.subType]
}
// Text mező - kötelező, különböző forrásokból jöhet
cleanedCard.text = card.text || card.question || card.statement || ""
// Egyéb frontend mezők, amiket a backend is elfogad
if (card.question !== undefined) cleanedCard.question = card.question
if (card.statement !== undefined) cleanedCard.statement = card.statement
if (card.options !== undefined) cleanedCard.options = card.options
if (card.correctAnswer !== undefined) cleanedCard.correctAnswer = card.correctAnswer
if (card.leftItems !== undefined) cleanedCard.leftItems = card.leftItems
if (card.rightItems !== undefined) cleanedCard.rightItems = card.rightItems
if (card.correctPairs !== undefined) cleanedCard.correctPairs = card.correctPairs
if (card.acceptedAnswers !== undefined) cleanedCard.acceptedAnswers = card.acceptedAnswers
if (card.hint !== undefined) cleanedCard.hint = card.hint
// Answer mező (ha van)
if (card.answer !== undefined && card.answer !== null) {
cleanedCard.answer = card.answer
}
// Csak LUCK típusú kártyáknál add hozzá a consequence-t
if (deck.type === 'LUCK' && card.consequence) {
cleanedCard.consequence = card.consequence
}
return cleanedCard
})
// Típus konverzió backendhez // Típus konverzió backendhez
const typeMapping = { const typeMapping = {
'LUCK': 0, 'LUCK': 0,
@@ -110,7 +186,7 @@ export default function DeckCreator() {
name: deck.name?.trim() || "Névtelen pakli", name: deck.name?.trim() || "Névtelen pakli",
type: typeMapping[deck.type] ?? 2, type: typeMapping[deck.type] ?? 2,
ctype: ctypeMapping[deck.privacy] ?? 1, ctype: ctypeMapping[deck.privacy] ?? 1,
cards: deck.cards || [] cards: cleanedCards
} }
// Note: description field is not sent to backend as it's not supported yet // Note: description field is not sent to backend as it's not supported yet
@@ -164,6 +240,36 @@ export default function DeckCreator() {
navigate("/decks") navigate("/decks")
} }
const handleDeleteDeck = () => {
if (!deck.id) {
notifyWarning('Nincs mit törölni - a pakli még nincs elmentve!')
return
}
setShowDeleteModal(true)
}
const handleConfirmDelete = async () => {
try {
await deleteDeck(deck.id)
setShowDeleteModal(false)
notifySuccess('Pakli sikeresen törölve!')
navigate('/decks')
} catch (error) {
console.error('Pakli törlési hiba:', error)
const errorMessage = error?.response?.data?.error
|| error?.response?.data?.message
|| error?.message
|| 'Ismeretlen hiba történt'
notifyError('Hiba történt a törlés során: ' + errorMessage)
setShowDeleteModal(false)
}
}
const handleCancelDelete = () => {
setShowDeleteModal(false)
}
const handleCreateCard = (cardType) => { const handleCreateCard = (cardType) => {
setNewCardType(cardType) setNewCardType(cardType)
setIsCreatingCard(true) setIsCreatingCard(true)
@@ -184,17 +290,15 @@ export default function DeckCreator() {
return return
} }
const defaultConsequence = { type: 0, value: 1 } // Alapértelmezett consequence csak LUCK típusú kártyákhoz
const defaultWrongConsequence = { type: 1, value: 1 }
const updatedCard = { const updatedCard = {
...cardData, ...cardData,
id: isCreatingCard ? Date.now() : cardData.id, id: isCreatingCard ? Date.now() : cardData.id
consequence: cardData.consequence || defaultConsequence, }
...(cardData.type === 'QUESTION' || cardData.type === 'JOKER' || cardData.type === 'PAIRING'
? { wrongConsequence: cardData.wrongConsequence || defaultWrongConsequence } // Csak LUCK típusú kártyákhoz add hozzá a consequence-t
: {} if (cardData.type === 'LUCK') {
) updatedCard.consequence = cardData.consequence || { type: 0, value: 1 }
} }
let wasInvalidCardDeleted = false let wasInvalidCardDeleted = false
@@ -278,6 +382,7 @@ export default function DeckCreator() {
onUpdate={handleDeckUpdate} onUpdate={handleDeckUpdate}
onSave={handleSaveDeck} onSave={handleSaveDeck}
onBack={handleBack} onBack={handleBack}
onDelete={handleDeleteDeck}
/> />
{/* Main Content */} {/* Main Content */}
@@ -313,6 +418,35 @@ export default function DeckCreator() {
</div> </div>
</main> </main>
)} )}
{/* Delete Confirmation Modal */}
{showDeleteModal && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div className="bg-[color:var(--color-surface)] rounded-xl shadow-xl p-6 w-96 text-center animate-fadeIn">
<div className="text-4xl mb-4">🗑</div>
<h3 className="text-lg font-semibold mb-4 text-[color:var(--color-text)]">
Biztosan törölni szeretnéd a(z) "{deck.name}" paklit?
</h3>
<p className="text-sm text-[color:var(--color-text-muted)] mb-6">
Ez a művelet nem visszavonható!
</p>
<div className="flex justify-center gap-4">
<button
onClick={handleConfirmDelete}
className="bg-red-600 text-white px-6 py-2 rounded-lg hover:bg-red-700 transition font-semibold"
>
Igen, törlöm
</button>
<button
onClick={handleCancelDelete}
className="bg-[color:var(--color-background)] text-[color:var(--color-text)] px-6 py-2 rounded-lg hover:bg-[color:var(--color-surface-selected)] transition"
>
Mégse
</button>
</div>
</div>
</div>
)}
</div> </div>
) )
} }
@@ -0,0 +1,811 @@
import React, { useState, useEffect } from "react"
import { useParams, useNavigate } from "react-router-dom"
import {
FaArrowLeft,
FaFilter,
FaArrowUp,
FaArrowDown,
FaSortAlphaDown,
FaSortAlphaUp,
FaQuestionCircle,
FaChevronLeft,
FaChevronRight,
} from "react-icons/fa"
import Navbar from "../../components/Navbar/Navbar"
import SearchBox from "../../components/Search/SearchBox"
import PopUp from "../../components/PopUp/PopUp"
import { getDeckById } from "../../api/deckApi"
const Card_display = () => {
const { deckId } = useParams()
const navigate = useNavigate()
const [deck, setDeck] = useState(null)
const [cards, setCards] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [search, setSearch] = useState("")
const [sortBy, setSortBy] = useState("index")
const [showSortHelp, setShowSortHelp] = useState(false)
const [itemsPerPage, setItemsPerPage] = useState(20)
const [currentPage, setCurrentPage] = useState(1)
const [flippedCards, setFlippedCards] = useState(new Set()) // Track which cards are flipped
// Load deck and parse cards
useEffect(() => {
let mounted = true
const load = async () => {
setLoading(true)
setError(null)
try {
const result = await getDeckById(deckId)
if (!mounted) return
console.log('Loaded deck:', result)
setDeck(result)
// Parse cards from JSON if it's a string
let parsedCards = []
if (result.cards) {
if (typeof result.cards === 'string') {
try {
parsedCards = JSON.parse(result.cards)
} catch (e) {
console.error('Failed to parse cards JSON:', e)
}
} else if (Array.isArray(result.cards)) {
parsedCards = result.cards
}
}
console.log('Parsed cards:', parsedCards)
console.log('First card structure:', parsedCards[0])
setCards(parsedCards)
} catch (err) {
console.error('Failed to load deck', err)
if (!mounted) return
setError(err.message || 'Hiba történt a pakli betöltése közben.')
} finally {
if (mounted) setLoading(false)
}
}
load()
return () => { mounted = false }
}, [deckId])
// Filter logic
let filteredCards = cards.filter((card) => {
if (!search) return true
const searchLower = search.toLowerCase()
// Check question, statement, and options
const questionText = card.question || card.statement || ''
const questionMatch = questionText.toLowerCase().includes(searchLower)
const answersMatch = Array.isArray(card.options)
? card.options.some(opt => opt && opt.toLowerCase().includes(searchLower))
: Array.isArray(card.answers)
? card.answers.some(a => a && a.toLowerCase().includes(searchLower))
: false
return questionMatch || answersMatch
})
// Sort logic
filteredCards = [...filteredCards].sort((a, b) => {
if (sortBy === "index") {
// Keep original order
return 0
} else if (sortBy === "question-asc") {
const aText = a.question || a.statement || ''
const bText = b.question || b.statement || ''
return aText.localeCompare(bText)
} else if (sortBy === "question-desc") {
const aText = a.question || a.statement || ''
const bText = b.question || b.statement || ''
return bText.localeCompare(aText)
} else if (sortBy === "answers-asc") {
const aCount = Array.isArray(a.options) ? a.options.length : Array.isArray(a.answers) ? a.answers.length : 0
const bCount = Array.isArray(b.options) ? b.options.length : Array.isArray(b.answers) ? b.answers.length : 0
return aCount - bCount
} else if (sortBy === "answers-desc") {
const aCount = Array.isArray(a.options) ? a.options.length : Array.isArray(a.answers) ? a.answers.length : 0
const bCount = Array.isArray(b.options) ? b.options.length : Array.isArray(b.answers) ? b.answers.length : 0
return bCount - aCount
}
return 0
})
// Pagination logic
const totalCards = filteredCards.length
const totalPages = Math.ceil(totalCards / itemsPerPage)
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const paginatedCards = filteredCards.slice(startIndex, endIndex)
// Reset to page 1 when filters or items per page change
useEffect(() => {
setCurrentPage(1)
}, [search, sortBy, itemsPerPage])
const deckTypes = {
0: { label: "Szerencse", color: "var(--color-luck)" },
1: { label: "Joker", color: "var(--color-fun)" },
2: { label: "Kérdés", color: "var(--color-question)" },
}
// Card subtype Hungarian labels - UPDATED based on actual data
const cardSubTypeLabels = {
// String types (from DeckCreator)
"truefalse": "Igaz/Hamis",
"multiplechoice": "Feleletválasztós",
"text": "Szöveges válasz",
"number": "Számos válasz",
"order": "Sorbarendezés",
"matching": "Párosítás",
"fill": "Kiegészítés",
"QUESTION": "Kérdés",
"LUCK": "Szerencse",
"JOKER": "Joker",
"joker": "Joker",
"luck": "Szerencse",
// If backend converts to different numbers, map them:
"0": "Igaz/Hamis", // truefalse = 0
"1": "Feleletválasztós", // multiplechoice = 1
"2": "Szöveges válasz", // text = 2
"3": "Igaz/Hamis", // type 3 = truefalse (alternate encoding)
"4": "Sorbarendezés", // order = 4
"5": "Párosítás", // matching = 5
"6": "Kiegészítés", // fill = 6
0: "Igaz/Hamis",
1: "Feleletválasztós",
2: "Szöveges válasz",
3: "Igaz/Hamis", // type 3 detected
4: "Sorbarendezés",
5: "Párosítás",
6: "Kiegészítés"
}
const currentDeckType = deck ? (deckTypes[deck.type] || { label: "Ismeretlen", color: "var(--color-success)" }) : null
const toggleCardFlip = (cardId) => {
setFlippedCards(prev => {
const newSet = new Set(prev)
if (newSet.has(cardId)) {
newSet.delete(cardId)
} else {
newSet.add(cardId)
}
return newSet
})
}
return (
<div className="w-full min-h-screen bg-[color:var(--color-background)] flex flex-col">
<Navbar />
<main className="flex-1 w-full max-w-[1200px] mx-auto px-4 py-10">
{/* Header with back button */}
<div className="flex items-center gap-4 mb-6">
<button
onClick={() => navigate('/decks')}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-[color:var(--color-surface)] text-[color:var(--color-text)] hover:bg-[color:var(--color-surface-selected)] transition-all duration-200 shadow"
>
<FaArrowLeft />
Vissza a paklikhoz
</button>
{deck && (
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-[color:var(--color-text)]">{deck.name}</h1>
{currentDeckType && (
<span
className="inline-block px-3 py-1 rounded-full text-sm font-bold"
style={{
background: currentDeckType.color,
color: "var(--color-text-inverse)",
}}
>
{currentDeckType.label}
</span>
)}
</div>
)}
</div>
{/* Loading / Error states */}
{loading && (
<div className="text-center text-[color:var(--color-text-muted)] py-10">
Betöltés...
</div>
)}
{error && (
<div className="text-center text-[color:var(--color-error)] py-10">
{error}
</div>
)}
{/* Filters and controls */}
{!loading && !error && (
<>
<div className="flex flex-col md:flex-row gap-3 justify-between items-center mb-6 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">
<SearchBox
value={search}
onChange={(e) => setSearch(e.target.value)}
width={300}
placeholder="Keresés kérdésben vagy válaszokban..."
className="mr-4"
/>
<span className="text-[color:var(--color-text)] font-semibold mr-2 ml-2 flex items-center gap-1">
Rendezés:
<button
type="button"
className="ml-1 text-[color:var(--color-success)] hover:text-[color:var(--color-text)] focus:outline-none"
onClick={() => setShowSortHelp(true)}
aria-label="Rendezési magyarázat megnyitása"
style={{ fontSize: 18, lineHeight: 1 }}
>
<FaQuestionCircle />
</button>
</span>
<select
className="px-3 py-1 rounded-lg bg-[color:var(--color-success)]/10 hover:bg-[color:var(--color-success)]/30 text-[color:var(--color-text)] border-none focus:ring-2 focus:ring-[color:var(--color-success)] outline-none"
style={{ backgroundColor: "rgba(0, 255, 0, 0.10)" }}
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
aria-label="Rendezés"
>
<option
value="index"
style={{ backgroundColor: "var(--color-surface)", color: "var(--color-text)" }}
>
Eredeti sorrend
</option>
<option
value="question-asc"
style={{ backgroundColor: "var(--color-surface)", color: "var(--color-text)" }}
>
Kérdés AZ
</option>
<option
value="question-desc"
style={{ backgroundColor: "var(--color-surface)", color: "var(--color-text)" }}
>
Kérdés ZA
</option>
<option
value="answers-asc"
style={{ backgroundColor: "var(--color-surface)", color: "var(--color-text)" }}
>
Válaszok száma
</option>
<option
value="answers-desc"
style={{ backgroundColor: "var(--color-surface)", color: "var(--color-text)" }}
>
Válaszok száma
</option>
</select>
</div>
</div>
{showSortHelp && (
<PopUp onClose={() => setShowSortHelp(false)}>
<h2 className="text-lg font-bold mb-4">Rendezési lehetőségek magyarázata</h2>
<ul className="space-y-2 text-[color:var(--color-night)]">
<li>
<span className="font-bold">Eredeti sorrend</span> A kártyák eredeti sorrendben jelennek meg
</li>
<li>
<span className="font-bold">Kérdés AZ</span> Kérdések ABC sorrendben (A-tól Z-ig)
</li>
<li>
<span className="font-bold">Kérdés ZA</span> Kérdések fordított ABC sorrendben (Z-től A-ig)
</li>
<li>
<span className="font-bold">Válaszok száma </span> Kevesebb választól a több válasz felé
</li>
<li>
<span className="font-bold">Válaszok száma </span> Több választól a kevesebb válasz felé
</li>
</ul>
<button
className="mt-6 px-4 py-2 rounded-lg bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] font-semibold hover:bg-[color:var(--color-success)]/80 transition"
onClick={() => setShowSortHelp(false)}
>
Bezárás
</button>
</PopUp>
)}
{/* 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={10}>10</option>
<option value={20}>20</option>
<option value={30}>30</option>
<option value={50}>50</option>
</select>
</div>
<div className="text-[color:var(--color-text-muted)] text-sm">
{totalCards > 0 ? (
<>
{startIndex + 1}-{Math.min(endIndex, totalCards)} / {totalCards} kártya
</>
) : (
<>0 kártya</>
)}
</div>
</div>
{/* Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{totalCards === 0 && (
<div className="col-span-full text-center text-[color:var(--color-text-muted)] py-10">
Nincsenek kártyák ebben a pakliban.
</div>
)}
{paginatedCards.map((card, idx) => {
const cardIndex = startIndex + idx + 1
const questionText = card.text || card.question || card.statement || 'Kérdés hiányzik'
// Get answers based on card type
let answerOptions = []
let correctAnswerIndex = card.correctAnswer
// Normalize subType (can be string or number or undefined)
const subType = card.subType ? String(card.subType).toLowerCase() : 'undefined'
// Detect card type by fields if subType is missing
let detectedType = subType
if (subType === 'undefined' || subType === 'null') {
// First check deck type - if deck is JOKER or LUCK type, cards inherit that
if (deck.type === 1) {
// Deck type 1 = Joker deck
detectedType = 'joker'
} else if (deck.type === 0) {
// Deck type 0 = Luck deck
detectedType = 'luck'
} else if (card.type !== undefined) {
// Check by card.type field (string or numeric)
const cardType = typeof card.type === 'string' ? card.type.toLowerCase() : card.type
if (cardType === 'joker' || card.type === 'JOKER') {
// Joker card
detectedType = 'joker'
} else if (cardType === 'luck' || card.type === 'LUCK') {
// Luck card
detectedType = 'luck'
} else if (card.type === 3) {
// type 3 = True/False
detectedType = 'truefalse'
} else if (card.type === 2) {
// type 2 = Text answer
detectedType = 'text'
}
} else if (card.leftItems && card.rightItems && card.correctPairs) {
// Has leftItems, rightItems AND correctPairs = matching
detectedType = 'matching'
} else if (card.acceptedAnswers && card.acceptedAnswers.length > 0 && card.acceptedAnswers[0] && card.acceptedAnswers[0].trim()) {
// Only detect as text if acceptedAnswers has non-empty values
detectedType = 'text'
} else if (card.isTrue !== undefined) {
detectedType = 'truefalse'
} else if (card.options && Array.isArray(card.options) && card.options.some(opt => opt && opt.trim())) {
// Has non-empty options - must be multiple choice
detectedType = 'multiplechoice'
}
}
// Extract consequence info for JOKER and LUCK cards
let consequenceText = null
if ((detectedType === 'joker' || detectedType === 'luck') && card.consequence) {
const consequenceLabels = {
0: 'Lépj előre',
1: 'Lépj hátra',
2: 'Kör kihagyás',
3: 'Extra kör',
5: 'Vissza a starthoz'
}
const consequenceType = consequenceLabels[card.consequence.type] || 'Ismeretlen hatás'
const consequenceValue = card.consequence.value
if (consequenceValue && [0, 1].includes(card.consequence.type)) {
consequenceText = `${consequenceType} ${consequenceValue} mezőt`
} else if (consequenceValue && [2, 3].includes(card.consequence.type)) {
consequenceText = `${consequenceType} (${consequenceValue} kör)`
} else {
consequenceText = consequenceType
}
}
if (detectedType === 'truefalse' || detectedType === '0') {
// True/False cards
answerOptions = ['Igaz', 'Hamis']
// correctAnswer: 0 = Igaz, 1 = Hamis (based on user feedback)
correctAnswerIndex = card.correctAnswer !== undefined ? card.correctAnswer : (card.isTrue ? 0 : 1)
} else if ((detectedType === 'text' || detectedType === '2') && card.acceptedAnswers && Array.isArray(card.acceptedAnswers)) {
// Text-based cards with accepted answers
answerOptions = card.acceptedAnswers
correctAnswerIndex = -1 // All accepted answers are correct
} else if (detectedType === 'matching' || detectedType === '5') {
// Matching cards - pairs
if (card.leftItems && card.rightItems && card.correctPairs) {
// Build pairs from correctPairs object
const pairs = []
for (const [leftIdx, rightIdx] of Object.entries(card.correctPairs)) {
const left = card.leftItems[parseInt(leftIdx)]
const right = card.rightItems[parseInt(rightIdx)]
if (left && right) {
pairs.push(`${left}${right}`)
}
}
answerOptions = pairs
correctAnswerIndex = -1 // All pairs are correct
}
} else if ((detectedType === 'multiplechoice' || detectedType === '1') && card.options && Array.isArray(card.options)) {
// Multiple choice - filter out empty options
answerOptions = card.options.filter(opt => opt && opt.trim())
correctAnswerIndex = card.correctAnswer
} else if (card.options && Array.isArray(card.options)) {
// Other types with options
answerOptions = card.options.filter(opt => opt && opt.trim())
} else if (card.answers && Array.isArray(card.answers)) {
// Other card types with answers array
answerOptions = card.answers.filter(opt => opt && opt.trim())
} else if (card.acceptedAnswers && Array.isArray(card.acceptedAnswers)) {
// Fallback for accepted answers
answerOptions = card.acceptedAnswers
correctAnswerIndex = -1
}
const answerCount = answerOptions.length
const cardId = card.id || idx
const isFlipped = flippedCards.has(cardId)
return (
<div
key={cardId}
className="relative h-80"
style={{ perspective: "1000px" }}
>
{detectedType === 'joker' ? (
// Joker card - no flip, just show the task
<div
className="w-full h-full bg-[color:var(--color-card)] rounded-2xl p-6 shadow-lg border-l-4 flex flex-col"
style={{
borderLeftColor: "var(--color-fun)",
}}
>
<div className="flex items-center justify-between mb-3">
<span className="text-[color:var(--color-text-muted)] text-sm font-medium">
Kártya #{cardIndex}
</span>
<span
className="inline-block px-2 py-1 rounded-full text-xs font-bold"
style={{
background: "var(--color-fun)",
color: "var(--color-text-inverse)",
}}
>
🃏 JOKER
</span>
</div>
<div className="flex-1 flex flex-col items-center justify-center">
<div className="text-6xl mb-4">🃏</div>
<div className="text-[color:var(--color-text)] text-center text-lg font-medium bg-[color:var(--color-fun)]/20 rounded-lg px-6 py-4 border-2 border-[color:var(--color-fun)]">
{questionText}
</div>
</div>
<div className="pt-3 border-t border-[color:var(--color-surface-selected)] text-xs text-[color:var(--color-text-muted)] text-center">
<div>Típus: <span className="font-semibold">Joker</span></div>
</div>
</div>
) : detectedType === 'luck' ? (
// Luck card - no flip, show text and consequence
<div
className="w-full h-full bg-[color:var(--color-card)] rounded-2xl p-6 shadow-lg border-l-4 flex flex-col"
style={{
borderLeftColor: "var(--color-luck)",
}}
>
<div className="flex items-center justify-between mb-3">
<span className="text-[color:var(--color-text-muted)] text-sm font-medium">
Kártya #{cardIndex}
</span>
<span
className="inline-block px-2 py-1 rounded-full text-xs font-bold"
style={{
background: "var(--color-luck)",
color: "var(--color-text-inverse)",
}}
>
🎲 SZERENCSE
</span>
</div>
<div className="flex-1 flex flex-col items-center justify-center">
<div className="text-6xl mb-4">🎲</div>
<div className="text-[color:var(--color-text)] text-center text-lg font-medium bg-[color:var(--color-luck)]/20 rounded-lg px-6 py-4 border-2 border-[color:var(--color-luck)] mb-4">
{questionText}
</div>
{consequenceText && (
<div className="text-[color:var(--color-text)] text-center">
<div className="text-xl font-bold bg-[color:var(--color-luck)]/30 rounded-lg px-6 py-3 border-2 border-[color:var(--color-luck)]">
{consequenceText}
</div>
</div>
)}
</div>
<div className="pt-3 border-t border-[color:var(--color-surface-selected)] text-xs text-[color:var(--color-text-muted)] text-center">
<div>Típus: <span className="font-semibold">Szerencse</span></div>
</div>
</div>
) : (
<div
className={`relative w-full h-full transition-transform duration-500 ${detectedType !== 'joker' && detectedType !== 'luck' ? 'cursor-pointer' : ''}`}
style={{
transformStyle: "preserve-3d",
transform: isFlipped ? "rotateY(180deg)" : "rotateY(0deg)"
}}
onClick={detectedType !== 'joker' && detectedType !== 'luck' ? () => toggleCardFlip(cardId) : undefined}
>
{/* Front side - Question */}
<div
className="absolute w-full h-full bg-[color:var(--color-card)] rounded-2xl p-6 shadow-lg border-l-4"
style={{
borderLeftColor: currentDeckType?.color || "var(--color-success)",
backfaceVisibility: "hidden"
}}
>
<div className="flex items-center justify-between mb-3">
<span className="text-[color:var(--color-text-muted)] text-sm font-medium">
Kártya #{cardIndex}
</span>
{detectedType !== 'joker' && detectedType !== 'luck' && (
<span
className="inline-block px-2 py-1 rounded-full text-xs font-bold"
style={{
background: currentDeckType?.color || "var(--color-success)",
color: "var(--color-text-inverse)",
}}
>
{answerCount} válasz
</span>
)}
{detectedType === 'joker' && (
<span
className="inline-block px-2 py-1 rounded-full text-xs font-bold"
style={{
background: "var(--color-fun)",
color: "var(--color-text-inverse)",
}}
>
🃏 JOKER
</span>
)}
{detectedType === 'luck' && (
<span
className="inline-block px-2 py-1 rounded-full text-xs font-bold"
style={{
background: "var(--color-luck)",
color: "var(--color-text-inverse)",
}}
>
🎲 SZERENCSE
</span>
)}
</div>
<h3 className="text-lg font-bold text-[color:var(--color-text)] mb-3">
{questionText}
</h3>
{/* Type info only */}
<div className="absolute bottom-6 left-6 right-6 pt-3 border-t border-[color:var(--color-surface-selected)] text-xs text-[color:var(--color-text-muted)]">
<div>Típus: <span className="font-semibold">
{cardSubTypeLabels[detectedType] || cardSubTypeLabels[card.subType] || cardSubTypeLabels[card.type] || detectedType || 'Ismeretlen'}
</span></div>
<div className="text-center mt-3 text-[color:var(--color-text-muted)] italic">
Kattints a megoldáshoz
</div>
</div>
</div>
{/* Back side - Answer */}
<div
className="absolute w-full h-full bg-[color:var(--color-card)] rounded-2xl p-6 shadow-lg border-l-4 overflow-y-auto"
style={{
borderLeftColor: currentDeckType?.color || "var(--color-success)",
backfaceVisibility: "hidden",
transform: "rotateY(180deg)"
}}
>
<div className="flex items-center justify-between mb-3">
<span className="text-[color:var(--color-text-muted)] text-sm font-medium">
{detectedType === 'joker' || detectedType === 'luck' ? 'Kártya hatás' : 'Megoldás'}
</span>
<span
className="inline-block px-2 py-1 rounded-full text-xs font-bold"
style={{
background: currentDeckType?.color || "var(--color-success)",
color: "var(--color-text-inverse)",
}}
>
{detectedType === 'joker' || detectedType === 'luck' ? (detectedType === 'joker' ? '🃏 JOKER' : '🎲 SZERENCSE') : `${answerCount} válasz`}
</span>
</div>
{detectedType === 'joker' ? (
// Joker card - just show the task/challenge
<div className="flex flex-col items-center justify-center h-full py-8">
<div className="text-6xl mb-4">🃏</div>
<div className="text-[color:var(--color-text)] text-center text-lg font-medium bg-[color:var(--color-fun)]/20 rounded-lg px-6 py-4 border-2 border-[color:var(--color-fun)]">
{questionText}
</div>
<div className="text-[color:var(--color-text-muted)] text-sm mt-4 text-center italic">
A játékmester dönti el a teljesítést
</div>
</div>
) : detectedType === 'luck' ? (
// Luck card - show consequence
<div className="flex flex-col items-center justify-center h-full py-8">
<div className="text-6xl mb-4">🎲</div>
{consequenceText && (
<div className="text-[color:var(--color-text)] text-center">
<div className="text-2xl font-bold mb-4 bg-[color:var(--color-luck)]/20 rounded-lg px-6 py-3 border-2 border-[color:var(--color-luck)]">
{consequenceText}
</div>
</div>
)}
<div className="text-[color:var(--color-text-muted)] text-sm mt-2 text-center italic">
Azonnal végrehajt
</div>
</div>
) : answerCount > 0 ? (
<div className="space-y-2">
<div className="text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Helyes válasz:
</div>
{detectedType === 'truefalse' || detectedType === '0' ? (
// True/False - show only the correct answer
<div className="text-[color:var(--color-text)] text-lg font-bold bg-[color:var(--color-success)]/20 rounded-lg px-4 py-3 border-l-4 border-[color:var(--color-success)]">
{card.isTrue ? 'Igaz' : 'Hamis'}
</div>
) : detectedType === 'matching' || detectedType === '5' ? (
// Matching - show all correct pairs
<ul className="space-y-2">
{answerOptions.map((pair, idx) => (
<li
key={idx}
className="text-[color:var(--color-text)] text-sm bg-[color:var(--color-success)]/20 rounded-lg px-3 py-2 border-l-2 border-[color:var(--color-success)] font-semibold"
>
{pair}
</li>
))}
</ul>
) : (detectedType === 'text' || detectedType === '2') && card.acceptedAnswers && Array.isArray(card.acceptedAnswers) ? (
// Text answers - show all accepted answers
<ul className="space-y-1">
{answerOptions.map((answer, ansIdx) => (
<li
key={ansIdx}
className="text-[color:var(--color-text)] text-sm bg-[color:var(--color-success)]/20 rounded-lg px-3 py-2 border-l-2 border-[color:var(--color-success)] font-semibold"
>
{answer}
</li>
))}
</ul>
) : (
// Multiple choice - show only the correct answer
correctAnswerIndex !== undefined && correctAnswerIndex !== -1 && answerOptions[correctAnswerIndex] ? (
<div className="text-[color:var(--color-text)] text-lg font-bold bg-[color:var(--color-success)]/20 rounded-lg px-4 py-3 border-l-4 border-[color:var(--color-success)]">
{answerOptions[correctAnswerIndex]}
</div>
) : (
<div className="text-center text-[color:var(--color-text-muted)] py-4">
Nincs megadva helyes válasz
</div>
)
)}
</div>
) : (
<div className="text-center text-[color:var(--color-text-muted)] py-10">
Nincs elérhető válasz
</div>
)}
<div className="absolute bottom-6 left-6 right-6 text-center text-xs text-[color:var(--color-text-muted)] italic">
Kattints a kérdéshez
</div>
</div>
</div>
)}
</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
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>
)}
</>
)}
</main>
</div>
)
}
export default Card_display
@@ -31,7 +31,7 @@ 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 min-h-[calc(100vh-64px)] flex mt-[64px] flex-col items-center justify-center"> <main className="flex-1 min-h-[calc(100vh-64px)] flex mt-[64px] flex-col items-center justify-center px-2 sm:px-4">
<PlayMenu <PlayMenu
onJoinGame={handleJoinGame} onJoinGame={handleJoinGame}
onCreateGame={handleCreateGame} onCreateGame={handleCreateGame}