25 Commits

Author SHA1 Message Date
mategergely33 5194308f7c deckkezeles, es deckek eltarolasa 2025-10-20 17:26:27 +02:00
mategergely33 75f2b215a1 deckek elmentodnek sqlbe 2025-10-15 18:13:53 +02:00
Walke 367524d611 Merge pull request 'home check 4 localstorage' (#47) from authlocalstorage into main
Reviewed-on: #47
2025-10-15 15:41:02 +00:00
Walke 86bf2675eb home check 4 localstorage 2025-10-15 17:40:02 +02:00
Walke 2c190dc874 Merge pull request 'authlocalstorage' (#46) from authlocalstorage into main
Reviewed-on: #46
2025-10-15 15:34:04 +00:00
Walke 36db09e5e7 elrakja az elrakni valot is 2025-10-15 17:32:04 +02:00
Donat f7885dc440 Merge pull request 'backend' (#45) from merge_branch into main
Reviewed-on: #45
2025-10-15 15:06:51 +00:00
magdo a9c2f63adc Merge branch 'main' into merge_branch 2025-10-15 17:05:59 +02:00
magdo bec9d83ef3 backend 2025-10-15 17:01:52 +02:00
Walke cf68530fc2 loginnal redirect ha jo a return plusz local storageban eltarolom a tokent 2025-10-15 16:32:18 +02:00
Walke f2b154d491 Merge pull request 'FooterFix' (#44) from footerFIx into main
Reviewed-on: #44
2025-10-15 13:41:40 +00:00
Walke 1e10a93e32 FooterFix 2025-10-15 15:40:25 +02:00
mategergely33 a5dd9003c1 userflow_fix 2025-10-15 15:13:53 +02:00
mategergely33 1db1776217 Merge pull request 'Registration redirect frontend fix' (#41) from registration into main
Reviewed-on: #41
2025-09-30 11:39:46 +00:00
Walke 87dc8ffff4 Registration redirect frontend fix 2025-09-29 21:53:52 +02:00
Donat 04a87b8293 Merge pull request 'last_bugfix' (#40) from merge_branch into main
Reviewed-on: #40
2025-09-29 18:36:57 +00:00
Donat a25807aca1 last_bugfix 2025-09-29 20:36:35 +02:00
Donat 9e88eba43f Merge pull request 'bugfix' (#39) from merge_branch into main
Reviewed-on: #39
2025-09-29 11:46:04 +00:00
Donat 14a94ea03f bugfix 2025-09-29 13:45:25 +02:00
Donat e392ade3f8 Merge pull request 'fixed merge conflicts' (#38) from merge_branch into main
Reviewed-on: #38
2025-09-26 15:02:10 +00:00
Donat 8980d98394 fixed merge conflicts 2025-09-26 17:01:45 +02:00
Donat 8f6634b03f fixed merge conflicts 2025-09-26 16:59:55 +02:00
Donat c690fb602e Merge pull request 'example frontend-backend communication' (#37) from backend_complete into main
Reviewed-on: #37
2025-09-24 18:21:05 +00:00
Donat bba4044eaf example frontend-backend communication 2025-09-24 20:19:58 +02:00
Donat b4d31f3660 Merge pull request 'backend_complete' (#35) from backend_complete into main
Reviewed-on: #35
2025-09-22 09:28:12 +00:00
45 changed files with 783 additions and 801 deletions
+7
View File
@@ -32,3 +32,10 @@ MINIO_USE_SSL=false
MAX_SPECIAL_FIELDS_PERCENTAGE=67
MAX_GENERATION_TIME_SECONDS=20
GENERATION_ERROR_TOLERANCE=15
# EMAIL SERVICE CONFIGURATION
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=your_email@domain.com
EMAIL_PASS=your_email_password
EMAIL_FROM=noreply@serpentrace.com
+3
View File
@@ -1,3 +1,6 @@
/* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.483\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.483/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */
/*!
* /**
* * Copyright (c) Meta Platforms, Inc. and affiliates.
+3
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env node
/* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.483\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.483/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
+1 -1
View File
@@ -45,7 +45,7 @@ app.use(loggingService.requestLoggingMiddleware());
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowedOrigins = ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:8080'];
const allowedOrigins = ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:8080', process.env.FRONTEND_URL];
if (!origin || allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin || '*');
@@ -195,7 +195,6 @@ deckRouter.get('/:id', authRequired, async (req, res) => {
}
});
deckRouter.patch('/:id', authRequired, async (req, res) => {
deckRouter.patch('/:id', authRequired, async (req, res) => {
try {
const deckId = req.params.id;
@@ -29,7 +29,7 @@ userRouter.post('/login',
const result = await container.loginCommandHandler.execute({ username, password }, res);
if (result) {
logAuth('User login successful', result.user.id, { username: result.user.username }, req, res);
logAuth('User login successful', undefined, { username: result.user.username }, req, res);
res.json(result);
} else {
throw new Error(`Login failed: ${result}`);
@@ -80,7 +80,6 @@ userRouter.post('/create',
const result = await container.createUserCommandHandler.execute(req.body);
logRequest('User created successfully', req, res, {
userId: result.id,
username: result.username
});
@@ -198,7 +197,6 @@ userRouter.post('/logout', authRequired, async (req, res) => {
return ErrorResponseService.sendInternalServerError(res);
}
});
<<<<<<< HEAD
// Refresh token endpoint
userRouter.post('/refresh-token', async (req, res) => {
@@ -337,6 +335,4 @@ userRouter.post('/reset-password',
}
});
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
export default userRouter;
@@ -100,7 +100,6 @@
* type: string
* format: email
*
<<<<<<< HEAD
* ForgotPasswordRequest:
* type: object
* required:
@@ -132,8 +131,6 @@
* message:
* type: string
*
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
* Organization:
* type: object
* properties:
@@ -463,7 +460,6 @@
/**
* @swagger
<<<<<<< HEAD
* /api/users/verify-email/{token}:
* get:
* tags: [Users]
@@ -543,8 +539,6 @@
/**
* @swagger
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
* /api/organizations/search:
* get:
* tags: [Organizations]
@@ -5,9 +5,7 @@ import { BaseMapper } from './BaseMapper';
export class UserMapper {
static toShortDto(user: UserAggregate): ShortUserDto {
return {
id: user.id,
username: user.username,
state: user.state,
authLevel: (user.state === UserState.ADMIN ? 1 : 0) as 0 | 1,
};
}
@@ -10,9 +10,7 @@ export interface UpdateUserDto {
}
export interface ShortUserDto {
id: string;
username: string;
state: number;
authLevel: 0 | 1;
}
@@ -1,32 +1,17 @@
import { GameField, BoardData } from '../../Domain/Game/GameAggregate';
import { logOther, logError } from '../Services/Logger';
<<<<<<< HEAD
=======
interface TargetField {
fieldNumber: number;
distance: number;
}
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
interface SpecialFieldInfo {
position: number;
type: 'positive' | 'negative' | 'luck';
}
export class BoardGenerationService {
<<<<<<< HEAD
=======
private readonly MAX_GENERATION_TIME = parseInt(process.env.MAX_GENERATION_TIME_SECONDS || '20') * 1000;
private readonly ERROR_TOLERANCE = parseInt(process.env.GENERATION_ERROR_TOLERANCE || '15');
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
async generateBoard(
positiveFieldCount: number,
negativeFieldCount: number,
luckFieldCount: number
): Promise<BoardData> {
<<<<<<< HEAD
// Pattern-based approach has 100% success rate, no retry needed
const result = this.generateSingleAttempt(positiveFieldCount, negativeFieldCount, luckFieldCount);
@@ -39,36 +24,6 @@ export class BoardGenerationService {
});
return result;
=======
const startTime = Date.now();
let bestAttempt: BoardData | null = null;
let attemptCount = 0;
while (Date.now() - startTime < this.MAX_GENERATION_TIME) {
attemptCount++;
try {
const attempt = this.generateSingleAttempt(positiveFieldCount, negativeFieldCount, luckFieldCount);
if (attempt.totalErrorRate <= this.ERROR_TOLERANCE) {
logOther(`Board generation successful on attempt ${attemptCount}. Error rate: ${attempt.totalErrorRate}%`);
return attempt;
}
if (!bestAttempt || attempt.totalErrorRate < bestAttempt.totalErrorRate) {
bestAttempt = attempt;
}
logOther(`Attempt ${attemptCount}: Error rate ${attempt.totalErrorRate}% (target: ${this.ERROR_TOLERANCE}%)`);
} catch (error) {
logError(`Board generation attempt ${attemptCount} failed:`, error as Error);
}
}
logOther(`Using best attempt with error rate: ${bestAttempt?.totalErrorRate || 100}%`);
return bestAttempt || this.generateFallbackBoard(positiveFieldCount, negativeFieldCount, luckFieldCount);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
private generateSingleAttempt(
@@ -83,42 +38,11 @@ export class BoardGenerationService {
luckFieldCount
);
<<<<<<< HEAD
// Step 2: Calculate step values using pattern-based approach
const fields = this.calculatePatternBasedStepValues(specialFieldPositions);
return {
fields
=======
// Step 2: Select target fields for each special field (6 targets per field for dice 1-6)
const targetFieldsMap = this.selectTargetFields(specialFieldPositions);
// Step 3: Create border with strategic placement
const border = this.createStrategicBorder(targetFieldsMap);
// Step 4: Calculate step values based on border positions
const fields = this.calculateStepValues(specialFieldPositions, targetFieldsMap, border);
// Step 5: Validate against 20-30 rule and calculate error rate
const validationResults = this.validateBoardGeneration(fields, border);
// Log generation statistics
logOther('Board generation attempt completed', {
totalFields: fields.length,
specialFields: fields.filter(f => f.type !== 'regular').length,
positiveFields: fields.filter(f => f.type === 'positive').length,
negativeFields: fields.filter(f => f.type === 'negative').length,
luckFields: fields.filter(f => f.type === 'luck').length,
errorRate: validationResults.errorRate,
targetCount: Array.from(targetFieldsMap.values()).reduce((sum, targets) => sum + targets.length, 0)
});
return {
fields,
border,
validationResults: validationResults.validationResults,
totalErrorRate: validationResults.errorRate
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
};
}
@@ -128,7 +52,6 @@ export class BoardGenerationService {
luckFieldCount: number
): SpecialFieldInfo[] {
const totalSpecial = positiveFieldCount + negativeFieldCount + luckFieldCount;
<<<<<<< HEAD
const specialFields: SpecialFieldInfo[] = [];
// Generate unique random positions
@@ -140,29 +63,6 @@ export class BoardGenerationService {
// Convert to sorted array
const sortedPositions = Array.from(positions).sort((a, b) => a - b);
=======
const positions: number[] = [];
const specialFields: SpecialFieldInfo[] = [];
// Random placement with retry for good distribution
let attempts = 0;
while (positions.length < totalSpecial && attempts < 100) {
const position = Math.floor(Math.random() * 100) + 1; // 1-100
if (!positions.includes(position)) {
// Check minimum distance from existing positions
const tooClose = positions.some(existingPos => Math.abs(existingPos - position) < 3);
if (!tooClose || attempts > 50) { // Relax distance requirement after many attempts
positions.push(position);
}
}
attempts++;
}
// Sort positions and assign types
positions.sort((a, b) => a - b);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
// Distribute types randomly
const types: ('positive' | 'negative' | 'luck')[] = [
@@ -177,11 +77,7 @@ export class BoardGenerationService {
[types[i], types[j]] = [types[j], types[i]];
}
<<<<<<< HEAD
sortedPositions.forEach((position, index) => {
=======
positions.forEach((position, index) => {
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
specialFields.push({
position,
type: types[index] || 'positive'
@@ -191,156 +87,14 @@ export class BoardGenerationService {
return specialFields;
}
<<<<<<< HEAD
private calculatePatternBasedStepValues(specialFields: SpecialFieldInfo[]): GameField[] {
=======
private selectTargetFields(specialFields: SpecialFieldInfo[]): Map<number, TargetField[]> {
const targetFieldsMap = new Map<number, TargetField[]>();
specialFields.forEach(field => {
if (field.type === 'luck') {
// Luck fields don't need target calculations
targetFieldsMap.set(field.position, []);
return;
}
const targets: TargetField[] = [];
const usedTargets = new Set<number>();
// Generate 6 different target fields (for dice 1-6) with 20-30 rule compliance
for (let i = 0; i < 6; i++) {
let targetField: number;
let distance: number;
let attempts = 0;
do {
// Determine max distance based on field position (20-30 rule)
let maxDistance: number;
let maxBackward: number;
if (field.position <= 85) {
maxDistance = 20;
maxBackward = 20;
} else {
maxDistance = 20; // forward
maxBackward = 30; // backward
}
// Create variety in distances within the allowed range
const distanceType = Math.random();
if (distanceType < 0.5) {
// Close distance (50% chance) - 1 to 1/3 of max
distance = Math.floor(Math.random() * Math.floor(maxDistance / 3)) + 1;
} else {
// Far distance (50% chance) - 1/3 to max
distance = Math.floor(Math.random() * (maxDistance - Math.floor(maxDistance / 3))) + Math.floor(maxDistance / 3);
}
// Randomly choose forward or backward
if (Math.random() < 0.5) {
distance = -Math.min(distance, maxBackward);
} else {
distance = Math.min(distance, maxDistance);
}
targetField = field.position + distance;
// Ensure target is within valid range
if (targetField < 1) targetField = 1;
if (targetField > 100) targetField = 100;
// Recalculate actual distance after clamping
distance = Math.abs(targetField - field.position);
attempts++;
} while (usedTargets.has(targetField) && attempts < 30);
if (!usedTargets.has(targetField)) {
usedTargets.add(targetField);
targets.push({
fieldNumber: targetField,
distance: Math.abs(targetField - field.position)
});
} else {
// Fallback: use a nearby valid target
let fallbackTarget = field.position + (i - 3); // Create some variety around current position
if (fallbackTarget < 1) fallbackTarget = 1;
if (fallbackTarget > 100) fallbackTarget = 100;
targets.push({
fieldNumber: fallbackTarget,
distance: Math.abs(fallbackTarget - field.position)
});
}
}
targetFieldsMap.set(field.position, targets);
});
return targetFieldsMap;
}
private createStrategicBorder(targetFieldsMap: Map<number, TargetField[]>): number[] {
// Collect all target field numbers
const targetNumbers = new Set<number>();
targetFieldsMap.forEach(targets => {
targets.forEach(target => targetNumbers.add(target.fieldNumber));
});
// Create array of all numbers 1-100
const allNumbers = Array.from({ length: 100 }, (_, i) => i + 1);
// Separate target numbers from remaining numbers
const remainingNumbers = allNumbers.filter(num => !targetNumbers.has(num));
// Shuffle remaining numbers
for (let i = remainingNumbers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[remainingNumbers[i], remainingNumbers[j]] = [remainingNumbers[j], remainingNumbers[i]];
}
// Create border with strategic placement
const border: number[] = [];
const targetArray = Array.from(targetNumbers);
// Encourage overlap by placing target numbers first, then fill with random
let targetIndex = 0;
let remainingIndex = 0;
for (let i = 0; i < 100; i++) {
// Alternate between target numbers and remaining numbers, but favor targets when available
if (targetIndex < targetArray.length && (remainingIndex >= remainingNumbers.length || Math.random() < 0.6)) {
border.push(targetArray[targetIndex]);
targetIndex++;
} else if (remainingIndex < remainingNumbers.length) {
border.push(remainingNumbers[remainingIndex]);
remainingIndex++;
} else {
// Fallback - should not happen if logic is correct
border.push((i % 100) + 1);
}
}
return border;
}
private calculateStepValues(
specialFields: SpecialFieldInfo[],
targetFieldsMap: Map<number, TargetField[]>,
border: number[]
): GameField[] {
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
// Initialize all fields as regular
const fields: GameField[] = Array.from({ length: 100 }, (_, i) => ({
position: i + 1,
type: 'regular' as const
}));
<<<<<<< HEAD
// Update special fields with pattern-based step values
=======
// Update special fields with calculated step values
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
specialFields.forEach(specialField => {
const fieldIndex = specialField.position - 1; // Convert to 0-based index
fields[fieldIndex].type = specialField.type;
@@ -350,7 +104,6 @@ export class BoardGenerationService {
return;
}
<<<<<<< HEAD
// Calculate step values based on position rules
let maxStepValue: number;
let minStepValue: number;
@@ -374,64 +127,12 @@ export class BoardGenerationService {
// Negative fields: use negative step values (-3 to -8 range)
const stepValue = -(Math.floor(Math.random() * 6) + 3); // -3 to -8
fields[fieldIndex].stepValue = Math.max(stepValue, minStepValue);
=======
const targets = targetFieldsMap.get(specialField.position) || [];
if (targets.length === 0) return;
// NEW APPROACH: Calculate step value that will land on first target with dice=1
// This ensures we have a baseline that works, then dice 2-6 will hit other targets
const firstTarget = targets[0];
const targetIndexInBorder = border.indexOf(firstTarget.fieldNumber);
if (targetIndexInBorder !== -1) {
// Start from field position in border (field position = border index + 1, but we want 0-based)
const startBorderIndex = (specialField.position - 1) % border.length;
// Calculate step value needed to reach target with dice=1
let stepValue: number;
if (specialField.type === 'positive') {
// For positive: move right to target, then +1 more for dice=1
stepValue = targetIndexInBorder - startBorderIndex - 1; // -1 for dice offset
// Handle wrap-around
if (stepValue < 0) {
stepValue += border.length;
}
} else {
// For negative: move left to target, then -1 more for dice=1
stepValue = startBorderIndex - targetIndexInBorder + 1; // +1 for dice offset
// Handle wrap-around
if (stepValue > border.length) {
stepValue -= border.length;
}
// Make negative for negative fields
stepValue = -stepValue;
}
fields[fieldIndex].stepValue = stepValue;
// Debug logging for step value calculation
logOther(`Calculated step value for ${specialField.type} field at position ${specialField.position}`, {
targetField: firstTarget.fieldNumber,
targetIndexInBorder,
startBorderIndex,
calculatedStepValue: stepValue,
fieldType: specialField.type
});
} else {
// Fallback if target not found in border (shouldn't happen)
fields[fieldIndex].stepValue = specialField.type === 'positive' ? 1 : -1;
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
});
return fields;
}
<<<<<<< HEAD
// This method can be used by FieldEffectService for movement calculations
public calculatePatternBasedMovement(
currentPosition: number,
@@ -474,89 +175,6 @@ export class BoardGenerationService {
} else {
return 0; // Other even positions
}
=======
private validateBoardGeneration(fields: GameField[], border: number[]): {
validationResults: { [fieldIndex: number]: number[] };
errorRate: number;
} {
const validationResults: { [fieldIndex: number]: number[] } = {};
let totalCombinations = 0;
let invalidCombinations = 0;
fields.forEach((field, fieldIndex) => {
if (field.type !== 'positive' && field.type !== 'negative') {
return; // Skip non-special fields
}
const diceOutcomes: number[] = [];
for (let diceValue = 1; diceValue <= 6; diceValue++) {
totalCombinations++;
try {
const result = this.calculateBorderMovement(
field.position,
field.stepValue || 0,
diceValue,
border,
field.type === 'positive'
);
// Validate 20-30 rule
const distance = Math.abs(result - field.position);
const isValid = this.validate20_30Rule(field.position, result, distance);
if (isValid) {
diceOutcomes.push(result);
} else {
diceOutcomes.push(-1); // Mark as invalid
invalidCombinations++;
}
} catch (error) {
diceOutcomes.push(-1); // Mark as invalid
invalidCombinations++;
}
}
validationResults[fieldIndex] = diceOutcomes;
});
const errorRate = totalCombinations > 0 ? (invalidCombinations / totalCombinations) * 100 : 0;
return {
validationResults,
errorRate: Math.round(errorRate * 100) / 100 // Round to 2 decimal places
};
}
private calculateBorderMovement(
currentPosition: number,
stepValue: number,
diceValue: number,
border: number[],
isPositive: boolean
): number {
// Step 1: Find border index for current field (field position corresponds to border index)
let borderIndex = (currentPosition - 1) % border.length; // Convert to 0-based, handle wraparound
// Step 2: Apply field step value (handle negative step values for negative fields)
if (isPositive) {
borderIndex = (borderIndex + Math.abs(stepValue)) % border.length;
} else {
// For negative fields, stepValue is already negative, so we subtract it (which adds its absolute value)
borderIndex = (borderIndex - stepValue + border.length) % border.length;
}
// Step 3: Apply dice value
if (isPositive) {
borderIndex = (borderIndex + diceValue) % border.length;
} else {
borderIndex = (borderIndex - diceValue + border.length) % border.length;
}
// Step 4: Return the field number at final border position
return border[borderIndex];
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
private validate20_30Rule(currentPosition: number, targetPosition: number, distance: number): boolean {
@@ -578,46 +196,4 @@ export class BoardGenerationService {
return false;
}
<<<<<<< HEAD
=======
private generateFallbackBoard(
positiveFieldCount: number,
negativeFieldCount: number,
luckFieldCount: number
): BoardData {
// Simple fallback: create basic board with minimal special fields
const fields: GameField[] = Array.from({ length: 100 }, (_, i) => ({
position: i + 1,
type: 'regular' as const
}));
// Add a few special fields with safe step values
let specialCount = 0;
for (let i = 10; i < 90 && specialCount < positiveFieldCount + negativeFieldCount; i += 10) {
if (specialCount < positiveFieldCount) {
fields[i].type = 'positive';
fields[i].stepValue = 1;
} else {
fields[i].type = 'negative';
fields[i].stepValue = -1;
}
specialCount++;
}
// Simple border: shuffled 1-100
const border = Array.from({ length: 100 }, (_, i) => i + 1);
for (let i = border.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[border[i], border[j]] = [border[j], border[i]];
}
return {
fields,
border,
validationResults: {},
totalErrorRate: 100 // Mark as fallback
};
}
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
@@ -37,11 +37,7 @@ export class GenerateBoardCommandHandler {
);
const executionTime = Date.now() - startTime;
<<<<<<< HEAD
logOther(`Board generation completed for game ${cmd.gameId} in ${executionTime}ms using pattern-based approach`);
=======
logOther(`Board generation completed for game ${cmd.gameId} in ${executionTime}ms. Error rate: ${boardData.totalErrorRate}%`);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
} catch (error) {
logError(`Board generation failed for game ${cmd.gameId}:`, error as Error);
@@ -50,12 +46,6 @@ export class GenerateBoardCommandHandler {
const errorData: BoardData = {
gameId: cmd.gameId,
fields: [],
<<<<<<< HEAD
=======
border: [],
validationResults: {},
totalErrorRate: 100,
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
generationComplete: false,
error: error instanceof Error ? error.message : 'Unknown error',
generatedAt: new Date()
@@ -151,7 +151,6 @@ export class JoinGameCommandHandler {
isOnline: true
};
<<<<<<< HEAD
// Check if player name is already in use by a different player
const existingPlayerWithName = gameData.currentPlayers.find(
p => p.playerName === command.playerName && p.playerId !== command.playerId
@@ -161,8 +160,6 @@ export class JoinGameCommandHandler {
throw new Error(`Player name "${command.playerName}" is already in use in this game`);
}
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
// Update players list (remove if exists, then add)
gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== command.playerId);
gameData.currentPlayers.push(newPlayer);
@@ -173,12 +170,6 @@ export class JoinGameCommandHandler {
// Store updated data in Redis with TTL (24 hours)
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60);
<<<<<<< HEAD
=======
// Add player to active players set
await this.redisService.setAdd(`active_players:${game.id}`, command.playerId);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
logOther('Game data updated in Redis', {
gameId: game.id,
gameCode: game.gamecode,
@@ -219,10 +210,6 @@ export class JoinGameCommandHandler {
gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== playerId);
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60);
<<<<<<< HEAD
=======
await this.redisService.setRemove(`active_players:${gameId}`, playerId);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
} catch (error) {
logError('Failed to remove player from Redis', error instanceof Error ? error : new Error(String(error)));
@@ -64,11 +64,7 @@ export class StartGameCommandHandler {
gamecode,
maxplayers: command.maxplayers,
logintype: command.logintype,
<<<<<<< HEAD
createdby: command.userid!,
=======
createdby: command.userid || null,
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
orgid: command.orgid || null,
gamedecks,
players: [],
@@ -28,11 +28,7 @@ export interface ActiveGamePlayData {
turnSequence: string[]; // Ordered array of player IDs based on turnOrder
websocketRoom: string;
gamePhase: 'starting' | 'playing' | 'paused' | 'finished';
<<<<<<< HEAD
boardData: BoardData; // Generated board with fields
=======
boardData: BoardData; // Generated board with fields and border
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
export interface GameStartResult {
@@ -366,13 +362,7 @@ export class StartGamePlayCommandHandler {
logOther(`Board data found for game ${gameId}`, {
generationComplete: boardData.generationComplete,
hasError: !!boardData.error,
<<<<<<< HEAD
fieldsCount: boardData.fields ? boardData.fields.length : 0
=======
fieldsCount: boardData.fields ? boardData.fields.length : 0,
borderLength: boardData.border ? boardData.border.length : 0,
totalErrorRate: boardData.totalErrorRate
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
});
if (boardData.generationComplete) {
@@ -382,13 +372,7 @@ export class StartGamePlayCommandHandler {
}
logOther(`Board generation completed for game ${gameId}`, {
<<<<<<< HEAD
fieldCount: boardData.fields.length,
=======
errorRate: boardData.totalErrorRate,
fieldCount: boardData.fields.length,
borderLength: boardData.border.length,
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
waitTime: Date.now() - startTime
});
@@ -23,9 +23,9 @@ async function isTokenBlacklisted(token: string): Promise<boolean> {
/**
* Extract token from request (cookie or Authorization header)
*/
function extractToken(req: Request): string | null {
function extractToken(req: Request, type: 'auth' | 'refresh'): string | null {
// First try to get token from cookie
const cookieToken = req.cookies['auth_token'];
const cookieToken = req.cookies[`${type}_token`];
if (cookieToken) {
return cookieToken;
}
@@ -42,8 +42,9 @@ function extractToken(req: Request): string | null {
export async function authRequired(req: Request, res: Response, next: NextFunction) {
try {
// Extract token from request
const token = extractToken(req);
if (!token) {
const token = extractToken(req, "auth");
const refreshToken = extractToken(req, "refresh");
if (!token || !refreshToken) {
logAuth('Authentication failed - No token provided', undefined, {
ip: req.ip,
userAgent: req.get ? req.get('User-Agent') : 'unknown',
@@ -79,11 +80,7 @@ export async function authRequired(req: Request, res: Response, next: NextFuncti
orgId: payload.orgId
}, req);
<<<<<<< HEAD
const refreshed = jwtService.refreshIfNeeded(payload, res, req);
=======
const refreshed = jwtService.refreshIfNeeded(payload, res);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
if (refreshed) {
logAuth('Token refreshed', payload.userId, undefined, req);
}
@@ -99,8 +96,9 @@ export async function authRequired(req: Request, res: Response, next: NextFuncti
export async function adminRequired(req: Request, res: Response, next: NextFunction) {
try {
// Extract token from request
const token = extractToken(req);
if (!token) {
const token = extractToken(req, "auth");
const refreshToken = extractToken(req, "refresh");
if (!token || !refreshToken) {
logWarning('Admin access denied - No token provided', {
ip: req.ip,
path: req.path
@@ -136,11 +134,7 @@ export async function adminRequired(req: Request, res: Response, next: NextFunct
orgId: payload.orgId
}, req);
<<<<<<< HEAD
const refreshed = jwtService.refreshIfNeeded(payload, res, req);
=======
const refreshed = jwtService.refreshIfNeeded(payload, res);
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
if (refreshed) {
logAuth('Admin token refreshed', payload.userId, undefined, req);
}
@@ -60,12 +60,9 @@ import { EmailService } from './EmailService';
import { GameTokenService } from './GameTokenService';
import { ContactEmailService } from './ContactEmailService';
import { DeckImportExportService } from './DeckImportExportService';
<<<<<<< HEAD
import { FieldEffectService } from './FieldEffectService';
import { CardDrawingService } from './CardDrawingService';
import { GamemasterService } from './GamemasterService';
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
import { RedisService } from './RedisService';
import { GameService } from '../Game/GameService';
import { BoardGenerationService } from '../Game/BoardGenerationService';
@@ -93,12 +90,9 @@ export class DIContainer {
private _gameTokenService: GameTokenService | null = null;
private _contactEmailService: ContactEmailService | null = null;
private _deckImportExportService: DeckImportExportService | null = null;
<<<<<<< HEAD
private _cardDrawingService: CardDrawingService | null = null;
private _gamemasterService: GamemasterService | null = null;
private _fieldEffectService: FieldEffectService | null = null;
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
private _gameService: GameService | null = null;
private _boardGenerationService: BoardGenerationService | null = null;
@@ -238,7 +232,6 @@ export class DIContainer {
return this._deckImportExportService;
}
<<<<<<< HEAD
public get cardDrawingService(): CardDrawingService {
if (!this._cardDrawingService) {
this._cardDrawingService = new CardDrawingService();
@@ -263,8 +256,6 @@ export class DIContainer {
return this._fieldEffectService;
}
=======
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
public get gameService(): GameService {
if (!this._gameService) {
this._gameService = new GameService();
@@ -281,9 +281,7 @@ export class JWTService {
} else {
// For cookie auth, create token pair and set cookies
const newTokenPair = this.create(freshPayload, res);
res.setHeader('X-New-Access-Token', newTokenPair.accessToken);
res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken);
res.setHeader('X-Token-Refreshed', 'true');
this.setTokenCookies(res, newTokenPair);
}
return true;
@@ -262,14 +262,6 @@ export class WebSocketService {
socket.on('chat:archive:delete', (data: DeleteChatArchiveData) => this.handleDeleteChatArchive(socket, data));
socket.on('message:delete', (data: DeleteMessageData) => this.handleDeleteMessage(socket, data));
<<<<<<< HEAD
=======
// Game event handlers (prepared for future implementation)
socket.on('game:join', (data: JoinGameRoomData) => this.handleJoinGameRoom(socket, data));
socket.on('game:leave', (data: LeaveGameRoomData) => this.handleLeaveGameRoom(socket, data));
socket.on('game:action', (data: GameActionData) => this.handleGameAction(socket, data));
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
socket.on('disconnect', () => this.handleDisconnection(socket));
}
@@ -6,6 +6,5 @@ export interface CreateUserCommand {
lname: string;
code?: string;
orgid?: string;
type: string;
phone?: string;
}
@@ -11,8 +11,8 @@ import { Response } from 'express';
export interface LoginResponse {
user: ShortUserDto;
token: string;
refreshToken: string;
token?: string;
refreshToken?: string;
requiresOrgReauth?: boolean;
orgLoginUrl?: string;
organizationName?: string;
@@ -114,10 +114,12 @@ export class LoginCommandHandler {
const responseObj = res || mockRes;
// Check if client prefers Bearer token authentication
const prefersBearerAuth = res && (
const isWebClient = res?.req?.headers['origin'] || res?.req?.headers['referer'];
const explicitBearerRequest = res?.req?.headers['x-auth-method'] === 'bearer';
const prefersBearerAuth = res && !isWebClient && (
res.req?.headers['authorization'] !== undefined ||
res.req?.headers['x-auth-method'] === 'bearer' ||
res.req?.headers['accept']?.includes('application/json')
explicitBearerRequest
);
let tokenPair: any;
@@ -168,12 +170,19 @@ export class LoginCommandHandler {
organizationName,
totalLoginTime: Date.now() - startTime
});
const response: LoginResponse = {
let response: LoginResponse;
if (prefersBearerAuth){
response = {
user: UserMapper.toShortDto(user),
token: tokenPair.accessToken,
refreshToken: tokenPair.refreshToken
};
}
else {
response = {
user: UserMapper.toShortDto(user)
};
}
if (requiresOrgReauth) {
response.requiresOrgReauth = true;
@@ -17,7 +17,6 @@ export class LogoutCommandHandler {
try {
logAuth('Logout process started', userId);
<<<<<<< HEAD
// 1. Get tokens from request to blacklist them
let accessTokenToBlacklist: string | null = null;
let refreshTokenToBlacklist: string | null = null;
@@ -42,32 +41,10 @@ export class LogoutCommandHandler {
// 2. Blacklist both access and refresh tokens in Redis
if (accessTokenToBlacklist && req) {
try {
=======
// 1. Get token from request to blacklist it
let tokenToBlacklist: string | null = null;
if (req) {
// Extract token from cookie
tokenToBlacklist = req.cookies['auth_token'];
// Also check Authorization header as fallback
if (!tokenToBlacklist && req.headers.authorization) {
const authHeader = req.headers.authorization;
if (authHeader.startsWith('Bearer ')) {
tokenToBlacklist = authHeader.substring(7);
}
}
}
// 2. Blacklist the current JWT token in Redis (if available)
if (tokenToBlacklist && req) {
try {
// Store token in blacklist with expiration matching token expiry
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
const decoded = this.jwtService.verify(req);
if (decoded && decoded.exp) {
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
if (ttl > 0) {
<<<<<<< HEAD
await this.redisService.setWithExpiry(`blacklist:${accessTokenToBlacklist}`, 'true', ttl);
logAuth('Access token blacklisted', userId, { tokenExpiry: ttl });
}
@@ -97,24 +74,6 @@ export class LogoutCommandHandler {
if (req) {
this.jwtService.logout(req, res);
}
=======
await this.redisService.setWithExpiry(`blacklist:${tokenToBlacklist}`, 'true', ttl);
logAuth('JWT token blacklisted', userId, { tokenExpiry: ttl });
}
}
} catch (error) {
logWarning('Failed to blacklist token', { userId, error: (error as Error).message });
}
}
// 3. Clear authentication cookie
res.clearCookie('auth_token', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/'
});
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
// 4. Remove user from active sessions in Redis
try {
@@ -7,7 +7,6 @@ export interface UpdateUserCommand {
fname?: string;
lname?: string;
code?: string;
type?: string;
phone?: string;
state?: number;
}
@@ -31,13 +31,7 @@ export enum ConsequenceType {
MOVE_BACKWARD = 1,
LOSE_TURN = 2,
EXTRA_TURN = 3,
<<<<<<< HEAD
GO_TO_START = 5
=======
SWAP_POSITION = 4,
GO_TO_START = 5,
TURN_AGAIN = 6
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
}
export interface Consequence {
@@ -1,9 +1,5 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
<<<<<<< HEAD
import { Consequence, CardType } from '../Deck/DeckAggregate';
=======
import { Consequence } from '../Deck/DeckAggregate';
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
export enum GameState {
WAITING = 0,
@@ -27,12 +23,8 @@ export enum DeckType {
export interface GameCard {
cardid: string;
question?: string;
<<<<<<< HEAD
answer?: any; // Support complex answer structures (string, object, array)
type?: CardType; // Card type for validation logic
=======
answer?: string;
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
consequence?: Consequence | null;
played?: boolean;
playerid?: string;
@@ -58,7 +50,6 @@ export class GameAggregate {
@Column({ type: 'int', default: LoginType.PUBLIC })
logintype!: LoginType;
<<<<<<< HEAD
@Column({ type: 'int', default: 50 })
boardsize!: number;
@@ -72,18 +63,6 @@ export class GameAggregate {
gamedecks!: GameDeck[];
@Column({ type: 'uuid', array: true, default: () => "'{}'", name: 'playerids' })
=======
@Column({ type: 'varchar', length: 255, nullable: true })
createdby!: string | null;
@Column({ type: 'varchar', length: 255, nullable: true })
orgid!: string | null;
@Column({ type: 'json' })
gamedecks!: GameDeck[];
@Column({ type: 'json', default: () => "'[]'" })
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
players!: string[];
@Column({ type: 'boolean', default: false })
@@ -92,37 +71,22 @@ export class GameAggregate {
@Column({ type: 'boolean', default: false })
finished!: boolean;
<<<<<<< HEAD
@Column({ type: 'uuid', nullable: true, name: 'winnerid' })
=======
@Column({ type: 'varchar', length: 255, nullable: true })
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
winner!: string | null;
@Column({ type: 'int', default: GameState.WAITING })
state!: GameState;
<<<<<<< HEAD
@CreateDateColumn({ name: 'createDate' })
=======
@CreateDateColumn({ name: 'create_date' })
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
createdate!: Date;
@Column({ type: 'timestamp', nullable: true, name: 'start_date' })
startdate!: Date | null;
<<<<<<< HEAD
@Column({ type: 'timestamp', nullable: true, name: 'finishDate' })
enddate!: Date | null;
@UpdateDateColumn({ name: 'updateDate' })
=======
@Column({ type: 'timestamp', nullable: true, name: 'end_date' })
enddate!: Date | null;
@UpdateDateColumn({ name: 'update_date' })
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
updatedate!: Date;
}
@@ -136,12 +100,6 @@ export interface GameField {
export interface BoardData {
gameId?: string;
fields: GameField[];
<<<<<<< HEAD
=======
border: number[];
validationResults: { [fieldIndex: number]: number[] };
totalErrorRate: number;
>>>>>>> 83fad59878db015ec8d86bdec1ecbbca0baddfd2
generationComplete?: boolean;
generatedAt?: Date;
error?: string;
+2 -1
View File
@@ -7,6 +7,7 @@
NODE_ENV=development
PORT=3000
APP_BASE_URL=http://localhost:3000
FRONTEND_URL=http://localhost:5173
# DATABASE CONFIGURATION (PostgreSQL)
DB_HOST=postgres
@@ -41,7 +42,7 @@ EMAIL_PORT=465
EMAIL_SECURE=true
EMAIL_USER=noreply@serpentrace.hu
EMAIL_PASS=ZUx720ece&Cin&F{
EMAIL_FROM=noreply@serpentrace.com
EMAIL_FROM=noreply@serpentrace.hu
# CHAT SYSTEM CONFIGURATION
CHAT_INACTIVITY_TIMEOUT_MINUTES=30
+1 -1
View File
@@ -83,7 +83,7 @@ services:
POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
volumes:
- postgres_dev_data:/var/lib/postgresql/data
- ./sql_dump_with_test_data.sql:/docker-entrypoint-initdb.d/init.sql:ro
- ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- serpentrace-network
healthcheck:
+2 -1
View File
@@ -13,6 +13,7 @@ services:
environment:
- NODE_ENV=development
- PORT=3000
- FRONTEND_URL=http://localhost:5173
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=serpentrace
@@ -115,7 +116,7 @@ services:
POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
volumes:
- postgres_dev_data:/var/lib/postgresql/data
- ./sql_dump_with_test_data.sql:/docker-entrypoint-initdb.d/init.sql:ro
- ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- serpentrace-network
healthcheck:
@@ -48,7 +48,6 @@ CREATE TABLE "Users" (
"lname" character varying(100) NOT NULL,
"token" character varying(255),
"TokenExpires" TIMESTAMP,
"type" character varying(50) NOT NULL,
"phone" character varying(20),
"state" integer NOT NULL DEFAULT 0,
"regdate" TIMESTAMP NOT NULL DEFAULT now(),
@@ -154,11 +153,11 @@ INSERT INTO "Organizations" ("id", "name", "contactfname", "contactlname", "cont
('33333333-3333-3333-3333-333333333333', 'Healthcare Corp', 'Michael', 'Brown', '+1-555-0003', 'michael.brown@healthcorp.com', 0, '2024-03-10 14:20:00', '2024-03-10 14:20:00', NULL, 0, 10);
-- Users Test Data
INSERT INTO "Users" ("id", "orgid", "username", "password", "email", "fname", "lname", "token", "TokenExpires", "type", "phone", "state", "regdate", "updatedate", "Orglogindate") VALUES
INSERT INTO "Users" ("id", "orgid", "username", "password", "email", "fname", "lname", "token", "TokenExpires", "phone", "state", "regdate", "updatedate", "Orglogindate") VALUES
-- Regular users
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', NULL, 'john_doe', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'john.doe@email.com', 'John', 'Doe', NULL, NULL, 'personal', '+1-555-1001', 1, '2024-01-20 11:00:00', '2024-01-20 11:00:00', NULL),
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-1111-1111-1111-111111111111', 'jane_premium', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'jane.smith@email.com', 'Jane', 'Smith', NULL, NULL, 'premium', '+1-555-1002', 2, '2024-01-25 12:30:00', '2024-01-25 12:30:00', '2024-01-25 12:30:00'),
('cccccccc-cccc-cccc-cccc-cccccccccccc', '22222222-2222-2222-2222-222222222222', 'teacher_bob', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'bob.teacher@eduinst.edu', 'Bob', 'Teacher', NULL, NULL, 'premium', '+1-555-1003', 2, '2024-02-05 09:15:00', '2024-02-05 09:15:00', '2024-02-05 09:15:00'),
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', NULL, 'john_doe', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'john.doe@email.com', 'John', 'Doe', NULL, NULL, '+1-555-1001', 1, '2024-01-20 11:00:00', '2024-01-20 11:00:00', NULL),
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-1111-1111-1111-111111111111', 'jane_premium', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'jane.smith@email.com', 'Jane', 'Smith', NULL, NULL, '+1-555-1002', 2, '2024-01-25 12:30:00', '2024-01-25 12:30:00', '2024-01-25 12:30:00'),
('cccccccc-cccc-cccc-cccc-cccccccccccc', '22222222-2222-2222-2222-222222222222', 'teacher_bob', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'bob.teacher@eduinst.edu', 'Bob', 'Teacher', NULL, NULL, '+1-555-1003', 2, '2024-02-05 09:15:00', '2024-02-05 09:15:00', '2024-02-05 09:15:00'),
-- Admin user
('dddddddd-dddd-dddd-dddd-dddddddddddd', NULL, 'admin_user', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'admin@serpentrace.com', 'Admin', 'User', NULL, NULL, 'admin', '+1-555-9999', 5, '2024-01-01 08:00:00', '2024-01-01 08:00:00', NULL),
-- Unverified user
+280
View File
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@tailwindcss/vite": "^4.1.7",
"axios": "^1.12.2",
"framer-motion": "^12.19.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
@@ -1678,6 +1679,23 @@
"dev": true,
"license": "Python-2.0"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1728,6 +1746,19 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1805,6 +1836,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1865,6 +1908,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -1874,6 +1926,20 @@
"node": ">=8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.155",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
@@ -1894,6 +1960,51 @@
"node": ">=10.13.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
@@ -2216,6 +2327,42 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/framer-motion": {
"version": "12.19.1",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.19.1.tgz",
@@ -2257,6 +2404,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -2267,6 +2423,43 @@
"node": ">=6.9.0"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -2293,6 +2486,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -2309,6 +2514,45 @@
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2745,6 +2989,36 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -2986,6 +3260,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+1
View File
@@ -11,6 +11,7 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.1.7",
"axios": "^1.12.2",
"framer-motion": "^12.19.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
+25
View File
@@ -0,0 +1,25 @@
import { apiClient } from './userApi'
// Create a new deck in the backend
export const createDeck = async (deck) => {
try {
const response = await apiClient.post('/decks', deck)
return response.data
} catch (err) {
throw err
}
}
// Get paginated decks (authenticated)
export const getDecksPage = async (from = 0, to = 49) => {
try {
const response = await apiClient.get(`/decks/page/${from}/${to}`)
return response.data
} catch (err) {
throw err
}
}
export default {
createDeck
}
+57
View File
@@ -0,0 +1,57 @@
import axios from "axios"
export const API_CONFIG = {
baseURL: (import.meta.env.VITE_API_URL ? import.meta.env.VITE_API_URL : '') + "/api",
wsURL: "http://localhost:3000",
timeout: 10000,
retryAttempts: 3,
}
export const apiClient = axios.create({
baseURL: API_CONFIG.baseURL,
timeout: API_CONFIG.timeout,
withCredentials: true, // Important for cookie-based auth
headers: {
'Content-Type': 'application/json'
}
});
//login
export const login = async (username, password) => {
try {
const response = await apiClient.post("/users/login", { username, password })
return response
} catch (error) {
throw error
}
}
//register
export const register = async (username, email, password, fname, lname, phone) => {
try {
const response = await apiClient.post("/users/create", { username, email, password, fname, lname, phone })
return response
} catch (error) {
throw error
}
}
//verify email
export const verifyEmail = async (token) => {
try {
const response = await apiClient.get(`/users/verify-email/${token}`)
return response.data
} catch (error) {
throw error
}
}
// Get current user's game statistics
export const getUserStats = async () => {
try {
const response = await apiClient.get("/users/me/stats")
return response.data
} catch (error) {
throw error
}
}
@@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from "react"
import { Link } from "react-router-dom"
import Logo from "../../assets/pictures/Logo"
const ArrowUpIcon = () => <span style={{ fontSize: "1.25rem" }}></span>
const Footer = () => {
@@ -35,54 +34,59 @@ const Footer = () => {
return (
<footer
ref={footerRef}
className={`relative bg-zinc-900 text-white border-t-2 border-zinc-800 mt-auto py-8 transition-all duration-700 ease-out ${
isVisible ? "opacity-100 scale-100 translate-y-0" : "opacity-0 scale-95 translate-y-10"
}`}
className="relative bg-zinc-900 text-white border-t-2 border-zinc-800 mt-auto py-8"
style={{ transformOrigin: "bottom center" }}
>
<style>
{`
.footer-animate {
transition: opacity 0.8s ease, transform 0.8s ease;
}
`}
</style>
<div className="max-w-6xl mx-auto flex flex-wrap justify-between items-start gap-8 px-4">
{/* Logó */}
<div className="flex flex-col items-center footer-animate">
<a
href="/"
className="transition-transform duration-500 hover:scale-105 hover:brightness-125"
>
<div className="flex flex-col items-center">
<a href="/" className="hover:scale-105 hover:brightness-125">
<Logo size={100} />
</a>
<span className="font-extrabold text-xl mt-2 tracking-wide">SerpentRace</span>
</div>
{/* Oldalak */}
<div className="flex flex-col gap-1 footer-animate">
<div className="flex flex-col gap-1">
<span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm">
Oldalak
</span>
<a href="/" className="hover:underline hover:text-green-400 transition">Főoldal</a>
<a href="/about" className="hover:underline hover:text-green-400 transition">
<a href="/" className="hover:underline hover:text-green-400">
Főoldal
</a>
<a href="/about" className="hover:underline hover:text-green-400">
Rólunk
</a>
<a href="/contact" className="hover:underline hover:text-green-400 transition">Kapcsolat</a>
<a href="/contact" className="hover:underline hover:text-green-400">
Kapcsolat
</a>
</div>
{/* Közösség */}
<div className="flex flex-col gap-1 footer-animate">
<div className="flex flex-col gap-1">
<span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm">
Közösség
</span>
<a href="https://discord.gg/" target="_blank" rel="noopener noreferrer" className="hover:underline hover:text-green-400 transition">Discord</a>
<a href="https://github.com/" target="_blank" rel="noopener noreferrer" className="hover:underline hover:text-green-400 transition">GitHub</a>
<a
href="https://discord.gg/"
target="_blank"
rel="noopener noreferrer"
className="hover:underline hover:text-green-400"
>
Discord
</a>
<a
href="https://github.com/"
target="_blank"
rel="noopener noreferrer"
className="hover:underline hover:text-green-400"
>
GitHub
</a>
</div>
{/* Elérhetőség */}
<div className="flex flex-col gap-1 footer-animate">
<div className="flex flex-col gap-1">
<span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm">
Elérhetőség
</span>
@@ -91,7 +95,7 @@ const Footer = () => {
</div>
</div>
<div className="text-center mt-8 text-sm opacity-70 footer-animate">
<div className="text-center mt-8 text-sm opacity-70">
© {new Date().getFullYear()} SerpentRace. Minden jog fenntartva.
</div>
@@ -99,7 +103,7 @@ const Footer = () => {
{isVisible && (
<button
onClick={scrollToTop}
className="fixed bottom-6 right-6 bg-green-500 hover:bg-green-600 text-white p-3 rounded-full shadow-lg transition transform hover:scale-110"
className="fixed bottom-6 right-6 bg-green-500 hover:bg-green-600 text-white p-3 rounded-full shadow-lg hover:scale-110"
aria-label="Ugrás az oldal tetejére"
>
<ArrowUpIcon />
@@ -1,4 +1,4 @@
import React, { useState } from "react"
import React, { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom"
import {
FaPlus,
@@ -20,19 +20,7 @@ const deckTypes = [
{ label: "Fun", color: "var(--color-fun)" },
]
const mockDecks = [
// Just for visual mockup
{ id: 1, name: "Party Luck", type: "Luck", created: "2025-07-01", origin: "Vállalati" },
{ id: 2, name: "Quiz Night", type: "Question", created: "2025-07-02", origin: "Saját" },
{ id: 3, name: "Fun Times", type: "Fun", created: "2025-07-03", origin: "Vállalati" },
{ id: 4, name: "Corporate Challenge", type: "Question", created: "2025-07-04", origin: "Vállalati" },
{ id: 5, name: "Randomizer", type: "Luck", created: "2025-07-05", origin: "Saját" },
{ id: 6, name: "Afterwork luck", type: "Luck", created: "2025-07-06", origin: "Saját" },
{ id: 7, name: "Serpent Quiz", type: "Question", created: "2025-07-07", origin: "Vállalati" },
{ id: 8, name: "Green Fortune", type: "Luck", created: "2025-07-08", origin: "Vállalati" },
{ id: 9, name: "Team Builder", type: "Fun", created: "2025-07-09", origin: "Saját" },
{ id: 10, name: "Knowledge Race", type: "Question", created: "2025-07-10", origin: "Saját" },
]
// initial state will be fetched from backend
const origins = ["Mind", "Vállalati", "Saját"]
@@ -82,9 +70,39 @@ const DeckManager = () => {
const [search, setSearch] = useState("")
const [showSortHelp, setShowSortHelp] = useState(false)
const [selectedDeck, setSelectedDeck] = useState(null)
const [decks, setDecks] = useState([])
const [loading, setLoading] = useState(false)
// Filter logic (mock)
let filteredDecks = mockDecks.filter((deck) => {
useEffect(() => {
let mounted = true
const load = async () => {
setLoading(true)
try {
const result = await import('../../api/deckApi').then(m => m.getDecksPage(0, 49))
if (!mounted) return
// map backend deck shape to UI shape
const mapped = result.decks.map(d => ({
id: d.id,
name: d.name,
type: d.type === 2 ? 'Question' : d.type === 1 ? 'Joker' : 'Luck',
created: d.creationdate ? new Date(d.creationdate).toLocaleDateString() : '',
origin: d.ctype === 2 ? 'Vállalati' : d.ctype === 0 ? 'Mind' : 'Saját',
raw: d
}))
setDecks(mapped)
} catch (err) {
console.error('Failed to load decks', err)
} finally {
setLoading(false)
}
}
load()
return () => { mounted = false }
}, [])
// Filter logic
const sourceDecks = decks
let filteredDecks = sourceDecks.filter((deck) => {
const typeMatch = selectedType === "All" || deck.type === selectedType
const originMatch = selectedOrigin === "Mind" || deck.origin === selectedOrigin
const searchMatch = !search || deck.name.toLowerCase().includes(search.toLowerCase())
@@ -255,8 +273,14 @@ const DeckManager = () => {
<FaPlus style={{ color: "var(--color-success)" }} className="text-5xl mb-2" />
<span className="text-[color:var(--color-text)] font-semibold">Új pakli létrehozása</span>
</div>
{/* Existing Decks (Mockup) */}
{filteredDecks.map((deck) => {
{/* Existing Decks (from backend) */}
{loading && (
<div className="col-span-full text-center text-[color:var(--color-text-muted)]">Betöltés...</div>
)}
{!loading && filteredDecks.length === 0 && (
<div className="col-span-full text-center text-[color:var(--color-text-muted)]">Nincsenek mentett paklik.</div>
)}
{!loading && filteredDecks.map((deck) => {
const deckType = deckTypes.find((t) => t.label === deck.type)
const borderColor = deckType ? deckType.color : "var(--color-success)"
return (
@@ -55,7 +55,7 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth }) => {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 1 }}
>
<ButtonGreen text="Játék" onClick={onNavigateToPlay} width="w-60" />
<ButtonGreen text="Bejelntekezés" onClick={onNavigateToPlay} width="w-60" />
<ButtonGreen text="Regisztráció" onClick={onNavigateToAuth} width="w-60" />
</motion.div>
</div>
@@ -14,6 +14,10 @@ import {
export default function DeckInfoPopUp({ deck, onClose }) {
if (!deck) return null
// Debug: Log the deck structure to see what we're working with
console.log('Deck in popup:', deck)
console.log('Cards:', deck.cards)
// Scroll blokkolás amikor a popup nyitva van
useEffect(() => {
// Scroll letiltása
@@ -33,13 +37,25 @@ export default function DeckInfoPopUp({ deck, onClose }) {
const currentDeckType = deckTypes[deck.type] || { label: deck.type, color: "var(--color-success)" }
// Mock data - ezeket majd a backend adatokra cseréljük
// Use real deck data with safe fallbacks
const creator = deck.creatorName || deck.creator || (deck.user && deck.user.name) || "Ismeretlen"
const privacy = deck.origin === "Vállalati" || deck.ctype === 1 || deck.privacy === 'public' ? "Publikus" : "Privát"
// Get data from raw if available
const rawData = deck.raw || deck
// Use played number from raw data for answers count
const questionsCount = rawData.cardCount || 0
const answersCount = rawData.playedNumber || 0
console.log('Calculated counts:', { questionsCount, answersCount, rawData })
const mockData = {
creator: "John Doe",
privacy: deck.origin === "Vállalati" ? "Publikus" : "Privát",
questionsCount: Math.floor(Math.random() * 50) + 10,
answersCount: Math.floor(Math.random() * 200) + 50,
...deck
...deck,
creator,
privacy,
questionsCount,
answersCount
}
const formatDate = (dateString) => {
@@ -1,23 +1,25 @@
// src/pages/Auth/AuthLogin.jsx
// Kártya amelyiken a bejelentkezés és regisztráció van
import { motion, AnimatePresence } from "framer-motion";
import Animation from "../../assets/SerpentRace_Animation/SerpentRace_Animation";
import LoginForm from "./LoginForm";
import RegisterForm from "./RegisterForm";
import Logo from "../../assets/pictures/Logo";
import { motion, AnimatePresence } from "framer-motion"
import Animation from "../../assets/SerpentRace_Animation/SerpentRace_Animation"
import LoginForm from "./LoginForm"
import RegisterForm from "./RegisterForm"
import Logo from "../../assets/pictures/Logo"
export default function AuthCard({ isRegistering, setIsRegistering }) {
return (
<motion.div
initial={{ height: "auto" }}
animate={{ height: isRegistering ? "600px" : "385px" }}
animate={{ height: isRegistering ? "750px" : "385px" }}
transition={{ duration: 0.5, ease: "easeInOut" }}
className="absolute flex max-w-4xl w-full bg-white rounded-2xl shadow-lg overflow-hidden"
>
{/* Bal oldali kép és szöveg */}
<div
className={`transition-all duration-500 ${isRegistering ? 'w-0 p-0' : 'w-2/5 p-8'} flex flex-col justify-center items-center bg-gradient-to-r from-mint-darker to-mint text-white `}
className={`transition-all duration-500 ${
isRegistering ? "w-0 p-0" : "w-2/5 p-8"
} flex flex-col justify-center items-center bg-gradient-to-r from-mint-darker to-mint text-white `}
>
<Logo size={100} />
<div className="h-6" />
@@ -29,18 +31,14 @@ export default function AuthCard({ isRegistering, setIsRegistering }) {
{/* Jobb oldali űrlap */}
<div className="w-full p-10 relative">
<AnimatePresence mode="wait">
{isRegistering ? <RegisterForm /> : <LoginForm />}
</AnimatePresence>
<AnimatePresence mode="wait">{isRegistering ? <RegisterForm /> : <LoginForm />}</AnimatePresence>
<span
className="text-secondary cursor-pointer hover:underline mt-4 block text-center"
onClick={() => setIsRegistering(!isRegistering)}
>
{isRegistering
? "Már van fiókod? Jelentkezz be itt!"
: "Nincs még fiókod? Regisztrálj itt!"}
{isRegistering ? "Már van fiókod? Jelentkezz be itt!" : "Nincs még fiókod? Regisztrálj itt!"}
</span>
</div>
</motion.div>
);
)
}
@@ -1,58 +1,94 @@
// src/pages/Auth/EmailVerification.jsx
// Rublikák a kód beírásához, email ellenőrzéshez
import { useState, useRef } from "react";
import Background from "../../assets/backgrounds/Background";
import { motion } from "framer-motion";
import Button from "../../components/Buttons/Button";
import { useState, useRef, useEffect } from "react"
import Background from "../../assets/backgrounds/Background"
import { motion } from "framer-motion"
import Button from "../../components/Buttons/Button"
import { useLocation } from "react-router-dom"
export default function EmailVerification() {
const [code, setCode] = useState(Array(6).fill(""));
const inputRefs = useRef([]);
const [code, setCode] = useState(Array(6).fill(""))
const inputRefs = useRef([])
const location = useLocation()
const [showSuccess, setShowSuccess] = useState(false)
const [error, setError] = useState("")
useEffect(() => {
if (location.state && location.state.success) {
setShowSuccess(true)
setTimeout(() => setShowSuccess(false), 1500)
}
}, [location.state])
const handleChange = (e, index) => {
const { value } = e.target;
const { value } = e.target
if (/^\d*$/.test(value) && value.length <= 1) {
const newCode = [...code];
newCode[index] = value;
setCode(newCode);
const newCode = [...code]
newCode[index] = value
setCode(newCode)
if (value && index < 5) {
inputRefs.current[index + 1].focus();
inputRefs.current[index + 1].focus()
}
}
}
};
const handleKeyDown = (e, index) => {
if (e.key === "Backspace" && !code[index] && index > 0) {
inputRefs.current[index - 1].focus();
inputRefs.current[index - 1].focus()
} else if (e.key === "ArrowLeft" && index > 0) {
inputRefs.current[index - 1].focus();
inputRefs.current[index - 1].focus()
} else if (e.key === "ArrowRight" && index < 5) {
inputRefs.current[index + 1].focus();
inputRefs.current[index + 1].focus()
} else if (/^\d$/.test(e.key) && code[index]) {
e.preventDefault();
const newCode = [...code];
newCode[index] = e.key;
setCode(newCode);
e.preventDefault()
const newCode = [...code]
newCode[index] = e.key
setCode(newCode)
if (index < 5) {
setTimeout(() => {
inputRefs.current[index + 1].focus();
}, 0);
inputRefs.current[index + 1].focus()
}, 0)
}
}
}
};
const handleSubmit = (e) => {
e.preventDefault();
console.log("Kód:", code.join(""));
// Backend API
};
const handleSubmit = async (e) => {
e.preventDefault()
setError("")
const token = code.join("")
if (token.length !== 6) {
setError("A kód 6 számjegyből áll.")
return
}
try {
const res = await fetch(`/verify-email/${token}`)
const data = await res.json()
if (data.success) {
setShowSuccess(true)
setTimeout(() => setShowSuccess(false), 2000)
} else {
setError(data.message || "Sikertelen ellenőrzés.")
}
} catch (err) {
setError("Hiba történt az ellenőrzés során.")
}
}
return (
<div className="relative flex items-center justify-center min-h-screen bg-gray-100 p-0 font-poppins">
<Background />
{showSuccess && (
<div className="fixed top-6 left-1/2 -translate-x-1/2 bg-green-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300">
Sikeres email ellenőrzés!
</div>
)}
{error && (
<div className="fixed top-20 left-1/2 -translate-x-1/2 bg-red-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300">
{error}
</div>
)}
<motion.div
initial={{ height: "auto" }}
animate={{ height: "300px" }}
@@ -73,7 +109,9 @@ export default function EmailVerification() {
onChange={(e) => handleChange(e, index)}
onKeyDown={(e) => handleKeyDown(e, index)}
ref={(el) => (inputRefs.current[index] = el)}
className={`w-12 h-12 px-2 py-3 border rounded-lg focus:ring-4 focus:ring-indigo-400 text-gray-700 placeholder-gray-400 bg-gray-50 text-center text-2xl tracking-widest ${!digit ? 'placeholder-opacity-100' : 'placeholder-opacity-0'}`}
className={`w-12 h-12 px-2 py-3 border rounded-lg focus:ring-4 focus:ring-indigo-400 text-gray-700 placeholder-gray-400 bg-gray-50 text-center text-2xl tracking-widest ${
!digit ? "placeholder-opacity-100" : "placeholder-opacity-0"
}`}
// nem tudom, hogy hogyan jobb
// placeholder="_"
maxLength="1"
@@ -85,5 +123,5 @@ export default function EmailVerification() {
</div>
</motion.div>
</div>
);
)
}
@@ -1,34 +1,72 @@
// src/pages/Auth/LoginForm.jsx
// Bejelentkezési űrlap
import InputBox from "../../components/Inputs/InputBox";
import Button from "../../components/Buttons/Button";
import { motion } from "framer-motion";
import { useState } from "react";
import InputBox from "../../components/Inputs/InputBox"
import Button from "../../components/Buttons/Button"
import { motion } from "framer-motion"
import { useState, useEffect } from "react"
import { useLocation, useNavigate } from "react-router-dom"
import { login } from "../../api/userApi"
export default function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [error, setError] = useState("")
const location = useLocation()
const navigate = useNavigate()
const [showSuccess, setShowSuccess] = useState(false)
const [showErrorPopup, setShowErrorPopup] = useState(false)
useEffect(() => {
if (location.state && location.state.success) {
setShowSuccess(true)
setTimeout(() => setShowSuccess(false), 4000)
}
}, [location.state])
function validateEmail(email) {
return /\S+@\S+\.\S+/.test(email);
return /\S+@\S+\.\S+/.test(email)
}
const handleSubmit = (e) => {
e.preventDefault();
setError("");
e.preventDefault()
setError("")
setShowErrorPopup(false)
if (!email || !password) {
setError("Minden mező kitöltése kötelező.");
return;
setError("Minden mező kitöltése kötelező.")
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
return
}
if (!validateEmail(email)) {
setError("Hibás email formátum.");
return;
setError("Hibás email formátum.")
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
return
}
// Backend API
console.log("Bejelentkezés:", { email, password });
};
login(email, password)
.then((response) => {
console.log(response)
// Csak a response.status-t ellenőrizd!
if (response && response.status === 200) {
if (response.data && response.data.user) {
localStorage.setItem("username", response.data.user.username)
localStorage.setItem("authLevel", response.data.user.authLevel)
}
navigate("/home")
} else {
setError("Hibás bejelentkezési adatok.")
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
}
})
.catch((error) => {
setError("Hibás bejelentkezési adatok.")
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
})
}
return (
<motion.div
@@ -39,24 +77,31 @@ export default function LoginForm() {
transition={{ duration: 0.25 }}
>
<h2 className="text-4xl font-extrabold text-center mb-6 text-gray-800 tracking-wide">Bejelentkezés</h2>
{error && (
<div className="mb-4 text-red-600 text-center font-semibold">{error}</div>
{showSuccess && (
<div className="fixed top-6 left-1/2 -translate-x-1/2 bg-green-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300">
Sikeres regisztráció! Az email ellenőrzése után be tudsz lépni.
</div>
)}
{showErrorPopup && error && (
<div className="fixed top-6 left-1/2 -translate-x-1/2 bg-red-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<InputBox
type="email"
placeholder="Email cím"
value={email}
onChange={e => setEmail(e.target.value)}
onChange={(e) => setEmail(e.target.value)}
/>
<InputBox
type="password"
placeholder="Jelszó"
value={password}
onChange={e => setPassword(e.target.value)}
onChange={(e) => setPassword(e.target.value)}
/>
<Button text="Bejelentkezés" type="submit" />
</form>
</motion.div>
);
)
}
@@ -1,46 +1,77 @@
// src/pages/Auth/RegisterForm.jsx
// Regisztrációs űrlap
import InputBox from "../../components/Inputs/InputBox";
import Button from "../../components/Buttons/Button";
import { motion } from "framer-motion";
import { useState } from "react";
import InputBox from "../../components/Inputs/InputBox"
import Button from "../../components/Buttons/Button"
import { motion } from "framer-motion"
import { useState } from "react"
import { register } from "../../api/userApi"
import { useNavigate } from "react-router-dom"
export default function RegisterForm() {
const [fullName, setFullName] = useState("");
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState("");
const [lastname, setLastname] = useState("")
const [firstname, setFirstname] = useState("")
const [username, setUsername] = useState("")
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
const [phone, setPhone] = useState("")
const [error, setError] = useState("")
const [showErrorPopup, setShowErrorPopup] = useState(false)
const navigate = useNavigate()
function validateEmail(email) {
return /\S+@\S+\.\S+/.test(email);
return /\S+@\S+\.\S+/.test(email)
}
const handleSubmit = (e) => {
e.preventDefault();
setError("");
const handleSubmit = async (e) => {
e.preventDefault()
setError("")
setShowErrorPopup(false)
if (!fullName || !username || !email || !password || !confirmPassword) {
setError("Minden mező kitöltése kötelező.");
return;
if (!lastname || !firstname || !username || !email || !password || !confirmPassword || !phone) {
setError("Minden mező kitöltése kötelező.")
return
}
if (!validateEmail(email)) {
setError("Hibás email formátum.");
return;
setError("Hibás email formátum.")
return
}
if (password.length < 6) {
setError("A jelszónak legalább 6 karakter hosszúnak kell lennie.");
return;
setError("A jelszónak legalább 6 karakter hosszúnak kell lennie.")
return
}
if (password !== confirmPassword) {
setError("A jelszavak nem egyeznek.");
return;
setError("A jelszavak nem egyeznek.")
return
}
// Backend API
console.log("Regisztráció:", { fullName, username, email, password });
};
try {
const response = await register(username, email, password, firstname, lastname, phone)
// Check for 201 Created status
if (response && response.status === 201) {
navigate("/login", { state: { success: true } })
} else {
let msg = "Sikertelen regisztráció."
if (response && response.data && response.data.error) {
msg = response.data.error
}
setError(msg)
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
}
} catch (err) {
let msg = "Ismeretlen hiba történt."
if (err.response && err.response.data && err.response.data.error) {
msg = err.response.data.error
} else if (err.message) {
msg = err.message
}
setError(msg)
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
}
}
return (
<motion.div
@@ -51,42 +82,56 @@ export default function RegisterForm() {
transition={{ duration: 0.25 }}
>
<h2 className="text-4xl font-extrabold text-center mb-6 text-gray-800 tracking-wide">Regisztráció</h2>
{error && (
<div className="mb-4 text-red-600 text-center font-semibold">{error}</div>
{showErrorPopup && error && (
<div className="fixed top-6 left-1/2 -translate-x-1/2 bg-red-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<InputBox
type="text"
placeholder="Teljes név"
value={fullName}
onChange={e => setFullName(e.target.value)}
placeholder="Vezetéknév"
value={lastname}
onChange={(e) => setLastname(e.target.value)}
/>
<InputBox
type="text"
placeholder="Keresztnév"
value={firstname}
onChange={(e) => setFirstname(e.target.value)}
/>
<InputBox
type="text"
placeholder="Felhasználónév"
value={username}
onChange={e => setUsername(e.target.value)}
onChange={(e) => setUsername(e.target.value)}
/>
<InputBox
type="email"
placeholder="Email cím"
value={email}
onChange={e => setEmail(e.target.value)}
onChange={(e) => setEmail(e.target.value)}
/>
<InputBox
type="phone"
placeholder="Telefonszám"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
<InputBox
type="password"
placeholder="Jelszó"
value={password}
onChange={e => setPassword(e.target.value)}
onChange={(e) => setPassword(e.target.value)}
/>
<InputBox
type="password"
placeholder="Jelszó megerősítése"
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
<Button text="Regisztráció" type="submit" />
</form>
</motion.div>
);
)
}
@@ -7,6 +7,7 @@ import Navbar from "../../components/Navbar/Navbar.jsx"
import DeckHeader from "../../components/DeckCreator/DeckHeader.jsx"
import CardsList from "../../components/DeckCreator/CardsList.jsx"
import CardEditor from "../../components/DeckCreator/CardEditor.jsx"
import { createDeck } from '../../api/deckApi'
export default function DeckCreator() {
const { deckId } = useParams() // URL-ből deck ID (új deck esetén undefined)
@@ -86,12 +87,23 @@ export default function DeckCreator() {
const handleSaveDeck = async () => {
try {
// TODO: API mentés
console.log("Deck mentése:", deck)
alert("✅ Deck sikeresen mentve!")
const payload = {
name: deck.name,
type: (deck.type || 'Question').toString().toUpperCase(),
ctype: deck.privacy === 'public' ? 'PUBLIC' : 'PRIVATE',
description: deck.description || '',
cards: deck.cards.map(c => ({ ...c, text: c.question || c.statement || c.text || '' }))
}
const saved = await createDeck(payload)
setDeck(prev => ({ ...prev, id: saved.id ?? prev.id, creationdate: saved.creationdate ?? prev.creationdate, updatedate: saved.updatedate ?? prev.updatedate }))
console.log('Deck saved (backend):', saved)
alert('✅ Deck sikeresen mentve!')
} catch (error) {
console.error("Mentési hiba:", error)
alert("❌ Hiba történt a mentés során!")
console.error('Mentési hiba:', error)
alert('❌ Hiba történt a mentés során: ' + (error?.response?.data?.error || error.message || String(error)))
}
}
+13 -2
View File
@@ -1,13 +1,24 @@
// src/pages/Home/Home.jsx
// Régi PlayMenu-s oldal, "Home" néven
import { useState } from "react"
import { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom"
import Navbar from "../../components/Navbar/Navbar"
import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx"
import PlayMenu from "../../components/Landingpage/PlayMenu.jsx"
export default function Home() {
const navigate = useNavigate()
useEffect(() => {
const username = localStorage.getItem("username")
const authLevel = localStorage.getItem("authLevel")
if (!username || !authLevel) {
navigate("/login")
}
}, [navigate])
// Dummy callbackok és user példa
const handleJoinGame = (code) => {
alert(`Csatlakozás játékhoz: ${code}`)
@@ -15,7 +26,7 @@ export default function Home() {
const handleCreateGame = () => {
alert("Új játék létrehozása")
}
const user = { name: "Teszt Elek" }
const user = { name: localStorage.getItem("username") }
return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
@@ -1,23 +1,23 @@
// src/pages/Landing/Landingpage.jsx
// Főoldal - Landing Page
import { useState } from "react"
import { useNavigate } from "react-router-dom"
import Navbar from "../../components/Navbar/Navbar"
import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx"
import LandingPage from "../../components/Landingpage/LandingPage.jsx"
export default function LandingPageMain() {
// Navigációs callbackok
const navigate = useNavigate();
const handleNavigateToPlay = () => {
// Itt lehet navigálni a játék oldalra
alert("Navigáció a játék oldalra")
}
navigate("/login");
};
const handleNavigateToAuth = () => {
// Itt lehet navigálni a bejelentkezés oldalra
alert("Navigáció a bejelentkezés oldalra")
}
navigate("/register");
};
return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
@@ -32,5 +32,5 @@ export default function LandingPageMain() {
</main>
<Footer />
</div>
)
);
}
@@ -3,6 +3,7 @@ import { useState } from "react"
import Navbar from "../../components/Navbar/Navbar.jsx"
import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx"
import { getUserStats } from "../../api/userApi.js"
export default function Reports() {
return (