1 Commits

Author SHA1 Message Date
Walke d23328763b footer fix meg landingon 2025-10-15 15:24:06 +02:00
59 changed files with 1027 additions and 2668 deletions
-62
View File
@@ -1,62 +0,0 @@
Javitás
Deckeck:
- Következmény csak szerencse kártyánál
- Egy fajta következmény (/lap, automatikusan kerül végrehajtásra)
- Hibás kártya pakli mentésekor is törlödjön
- extra kör, kimarad bármennyi 1-től 5-ig
- megnyitás, szerkesztés, adatok betöltése
- Mentési ADATOK Csekkolása | ZSOLA
- Closer option
navbar:
- tegnapiak
TEGNAPI HIBÁK JAVÍTÁSA:
- kapcs fel routing
- navbar széthúz
- footer kapcsolat
- navabar gomboksorrend
- vagy kontat vagy kapcsolat
- navbar bejelent
- navbar layout finomít
- palki info get
GET /ap/decks/page/:from/:to (0-49) 50db (50-99) 50db ... (0-29) 30db => (30-59) 30db
- from: (oldalsz-1)*dbsz (pl: (1-1)*30=0; (2-1)*30=30)
- to: (oldalsz*dbsz) - 1 (pl: (1*30)-1=29; (2*30)-1 =59)
email verifikáció:
- verify-email/:code => Email címe hitelesítés alatt: stb
- ha sikeres => login => toastify => email címe hitelesítve
- ha sikertelen => home/register => toastify/pushup => sikertelen vegye fel velünk a kapcsolatot
- POST api/users/verify-email/:code <= BACKEND URI
HOLNAP ESTE 19:00 => Jó lenne, ha ezek megvannak
HOLNAPTÓL => JÁTÉK => SOCKET IO működése
Mobil nézet:
- landing page
- navbar
- footer
- pakli fő nézet => bar
- pakli összerakás és szerkesztés
- bejelentkezés
- regisztráció
User felület:
- Saját adatok lekérése
- Saját adatok módosítása:
- email-cím
- telefonszám
- jelszó
- felhasználó név
- Saját profil törlése
- Elfelelejtett jelszó
- Kérése => email-cím alapján => POST /api/users/forgot-password
- password-reset/:token => POST /api/users/reset-password
-7
View File
@@ -32,10 +32,3 @@ 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
+1 -1
View File
@@ -1,5 +1,5 @@
/* 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 */
/* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.475\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '20ac9ab8d4418641bf7b8dUlMXUUwNXgNRAl1VDAkAVlMGDl1X', mode: 'build'}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.475/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '20ac9ab8d4418641bf7b8dUlMXUUwNXgNRAl1VDAkAVlMGDl1X', mode: 'build'})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */
/*!
* /**
+1 -1
View File
@@ -1,5 +1,5 @@
#!/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 */
/* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.475\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '20ac9ab8d4418641bf7b8dUlMXUUwNXgNRAl1VDAkAVlMGDl1X', mode: 'build'}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.475/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '20ac9ab8d4418641bf7b8dUlMXUUwNXgNRAl1VDAkAVlMGDl1X', mode: 'build'})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */
/**
@@ -199,13 +199,12 @@ deckRouter.patch('/:id', authRequired, async (req, res) => {
try {
const deckId = req.params.id;
const userId = (req as any).user.userId;
const authLevel = (req as any).user.authLevel;
logRequest('Update deck endpoint accessed', req, res, { deckId, userId, updateFields: Object.keys(req.body) });
// Convert string enum values to integers
const updateData = convertEnumValues(req.body);
const result = await container.updateDeckCommandHandler.execute({ userid: userId, authLevel: authLevel, id: deckId, ...updateData });
const result = await container.updateDeckCommandHandler.execute({ id: deckId, ...updateData });
logRequest('Deck updated successfully', req, res, { deckId, userId });
res.json(result);
@@ -245,10 +244,9 @@ deckRouter.delete('/:id', authRequired, async (req, res) => {
try {
const deckId = req.params.id;
const userId = (req as any).user.userId;
const authLevel = (req as any).user.authLevel;
logRequest('Soft delete deck endpoint accessed', req, res, { deckId, userId });
const result = await container.deleteDeckCommandHandler.execute({ userid: userId, authLevel: authLevel, id: deckId, soft: true });
const result = await container.deleteDeckCommandHandler.execute({ id: deckId, soft: true });
logRequest('Deck soft delete successful', req, res, { deckId, userId, success: result });
res.json({ success: result });
@@ -0,0 +1,66 @@
import e, { Router } from 'express';
import { container, DIContainer } from '../../Application/Services/DIContainer';
import { ErrorResponseService } from '../../Application/Services/ErrorResponseService';
import { logRequest, logError, logAuth, logWarning, logOther } from '../../Application/Services/Logger';
import { GenerateBoardCommand } from '../../Application/Game/commands/GenerateBoardCommand';
const router = Router();
//function to test the search service
async function triggerAsyncBoardGeneration(gameId: string): Promise<boolean> {
try {
// Calculate default field counts based on game configuration
// For now, use reasonable defaults - this should be configurable by host in the future
const maxSpecialFieldsPercentage = parseInt(process.env.MAX_SPECIAL_FIELDS_PERCENTAGE || '67');
const maxSpecialFields = Math.floor((100 * maxSpecialFieldsPercentage) / 100);
// Default distribution: 60% positive, 25% negative, 15% luck
const positiveFieldCount = Math.floor(maxSpecialFields * 0.6);
const negativeFieldCount = Math.floor(maxSpecialFields * 0.25);
const luckFieldCount = Math.floor(maxSpecialFields * 0.15);
const command: GenerateBoardCommand = {
gameId,
positiveFieldCount,
negativeFieldCount,
luckFieldCount
};
logOther(`Triggering async board generation for game ${gameId}`, {
positiveFieldCount,
negativeFieldCount,
luckFieldCount,
totalSpecialFields: positiveFieldCount + negativeFieldCount + luckFieldCount
});
// Execute board generation in background
await DIContainer.getInstance().generateBoardCommandHandler.execute(command);
return true;
} catch (error) {
logError(`Async board generation failed for game ${gameId}`, error as Error);
// Don't propagate error - board generation failure shouldn't affect game creation
return false;
}
}
// Game board generation endpoint
router.post('/gameBoardGeneration', async (req, res) => {
try {
logRequest('Game board generation endpoint accessed', req, res);
const result = await triggerAsyncBoardGeneration("######-#####-#####-######");
if (result) {
logOther('Game board generation triggered successfully', result);
return res.json({ message: 'Game board generation triggered successfully' });
} else {
throw new Error('Game board generation failed to trigger');
}
} catch (error : any) {
logError('Error in game board generation endpoint', error);
return ErrorResponseService.sendInternalServerError(res);
}
});
export default router;
@@ -225,7 +225,7 @@ userRouter.post('/refresh-token', async (req, res) => {
});
// Email verification endpoint
userRouter.post('/verify-email/:token', async (req, res) => {
userRouter.get('/verify-email/:token', async (req, res) => {
try {
const { token } = req.params;
@@ -15,9 +15,6 @@ export interface ShortDeckDto {
type: number;
playedNumber: number;
ctype: number;
cardsCount: number;
creator: string;
creationdate: Date;
}
export interface DetailDeckDto {
@@ -1,5 +1,4 @@
import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate';
import { UserAggregate } from '../../../Domain/User/UserAggregate';
import { CreateDeckDto, UpdateDeckDto, ShortDeckDto, DetailDeckDto } from '../DeckDto';
export class DeckMapper {
@@ -10,9 +9,6 @@ export class DeckMapper {
type: deck.type,
playedNumber: deck.playedNumber,
ctype: deck.ctype,
cardsCount: deck.cards.length,
creator: deck.user?.username || 'Unknown',
creationdate: deck.creationdate
};
}
@@ -30,15 +26,6 @@ export class DeckMapper {
}
static toShortDtoList(decks: DeckAggregate[]): ShortDeckDto[] {
return decks.map(deck => ({
id: deck.id,
name: deck.name,
type: deck.type,
playedNumber: deck.playedNumber,
ctype: deck.ctype,
cardsCount: deck.cards.length,
creator: deck.user?.username || 'Unknown',
creationdate: deck.creationdate
}));
return decks.map(this.toShortDto);
}
}
@@ -1,6 +1,4 @@
export interface DeleteDeckCommand {
userid: string;
authLevel: number;
id: string;
soft?: boolean;
}
@@ -1,24 +1,10 @@
import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository';
import { logAuth, logError } from '../../Services/Logger';
import { DeleteDeckCommand } from './DeleteDeckCommand';
export class DeleteDeckCommandHandler {
constructor(private readonly deckRepo: IDeckRepository) {}
async execute(cmd: DeleteDeckCommand): Promise<boolean> {
//get decks userid
const deck = await this.deckRepo.findById(cmd.id);
if (!deck) {
logError(`Deck not found with ID: ${cmd.id}`);
throw new Error('Deck not found');
}
if(cmd.authLevel !==1 && deck.userid !== cmd.userid) {
logAuth(`Unauthorized access attempt to deck with ID: ${cmd.id}, UserID: ${cmd.userid}`);
throw new Error('Unauthorized');
}
if (cmd.soft) {
await this.deckRepo.softDelete(cmd.id);
} else {
@@ -1,10 +1,11 @@
import { n } from "framer-motion/dist/types.d-D0HXPxHm";
export interface UpdateDeckCommand {
userid: string;
authLevel: number;
id: string;
userstate?: number;
name?: string;
type?: number;
userid?: string;
cards?: any[];
ctype?: number;
state?: number;
@@ -3,7 +3,7 @@ import { UpdateDeckCommand } from './UpdateDeckCommand';
import { ShortDeckDto } from '../../DTOs/DeckDto';
import { DeckMapper } from '../../DTOs/Mappers/DeckMapper';
import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate';
import { logAuth, logError } from '../../Services/Logger';
import { logError } from '../../Services/Logger';
export class UpdateDeckCommandHandler {
constructor(private readonly deckRepo: IDeckRepository) {}
@@ -24,11 +24,6 @@ export class UpdateDeckCommandHandler {
throw new Error('Deck not found');
}
if(cmd.authLevel !==1 && existingDeck.userid !== cmd.userid) {
logAuth(`Unauthorized access attempt to deck with ID: ${cmd.id}, UserID: ${cmd.userid}`);
throw new Error('Unauthorized');
}
const for_update: Partial<DeckAggregate> = {};
if(cmd.name !== undefined) for_update.name = cmd.name;
if(cmd.type !== undefined) for_update.type = cmd.type;
@@ -23,9 +23,9 @@ async function isTokenBlacklisted(token: string): Promise<boolean> {
/**
* Extract token from request (cookie or Authorization header)
*/
function extractToken(req: Request, type: 'auth' | 'refresh'): string | null {
function extractToken(req: Request): string | null {
// First try to get token from cookie
const cookieToken = req.cookies[`${type}_token`];
const cookieToken = req.cookies['auth_token'];
if (cookieToken) {
return cookieToken;
}
@@ -42,9 +42,8 @@ function extractToken(req: Request, type: 'auth' | 'refresh'): string | null {
export async function authRequired(req: Request, res: Response, next: NextFunction) {
try {
// Extract token from request
const token = extractToken(req, "auth");
const refreshToken = extractToken(req, "refresh");
if (!token || !refreshToken) {
const token = extractToken(req);
if (!token) {
logAuth('Authentication failed - No token provided', undefined, {
ip: req.ip,
userAgent: req.get ? req.get('User-Agent') : 'unknown',
@@ -96,9 +95,8 @@ 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, "auth");
const refreshToken = extractToken(req, "refresh");
if (!token || !refreshToken) {
const token = extractToken(req);
if (!token) {
logWarning('Admin access denied - No token provided', {
ip: req.ip,
path: req.path
@@ -281,7 +281,9 @@ export class JWTService {
} else {
// For cookie auth, create token pair and set cookies
const newTokenPair = this.create(freshPayload, res);
this.setTokenCookies(res, newTokenPair);
res.setHeader('X-New-Access-Token', newTokenPair.accessToken);
res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken);
res.setHeader('X-Token-Refreshed', 'true');
}
return true;
@@ -1,6 +1,5 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { OrganizationAggregate } from '../Organization/OrganizationAggregate';
import { UserAggregate } from '../User/UserAggregate';
export enum Type {
LUCK = 0,
@@ -82,8 +81,4 @@ export class DeckAggregate {
@ManyToOne(() => OrganizationAggregate, { nullable: true })
@JoinColumn({ name: 'organization_id' })
organization!: OrganizationAggregate | null;
@ManyToOne(() => UserAggregate, { eager: false })
@JoinColumn({ name: 'user_id' })
user!: UserAggregate | null;
}
@@ -255,7 +255,7 @@ export class DeckRepository implements IDeckRepository {
const [decks, totalCount] = await this.repo.findAndCount({
where: { state: Not(State.SOFT_DELETE) },
relations: ['organization', 'user'],
relations: ['organization'],
order: { creationdate: 'DESC' },
skip,
take
@@ -270,7 +270,6 @@ export class DeckRepository implements IDeckRepository {
// Regular user complex filtering
const queryBuilder = this.repo.createQueryBuilder('deck')
.leftJoinAndSelect('deck.organization', 'org')
.leftJoinAndSelect('deck.user', 'user')
.where('deck.state != :deletedState', { deletedState: State.SOFT_DELETE });
queryBuilder.andWhere('(' +
+1 -1
View File
@@ -42,7 +42,7 @@ EMAIL_PORT=465
EMAIL_SECURE=true
EMAIL_USER=noreply@serpentrace.hu
EMAIL_PASS=ZUx720ece&Cin&F{
EMAIL_FROM=noreply@serpentrace.hu
EMAIL_FROM=noreply@serpentrace.com
# CHAT SYSTEM CONFIGURATION
CHAT_INACTIVITY_TIMEOUT_MINUTES=30
@@ -1,206 +0,0 @@
services:
# Backend service with hot reload
backend:
build:
context: ../SerpentRace_Backend
dockerfile: ../SerpentRace_Docker/Dockerfile_backend.dev
container_name: serpentrace-backend-dev
restart: unless-stopped
env_file:
- .env.dev
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- PORT=3000
- FRONTEND_URL=http://localhost:5173
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=serpentrace
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
- REDIS_URL=redis://redis:6379
- REDIS_HOST=redis
- REDIS_PORT=6379
- MINIO_ENDPOINT=minio
- MINIO_PORT=9000
- MINIO_ACCESS_KEY=serpentrace
- MINIO_SECRET_KEY=serpentrace123!
- MINIO_USE_SSL=false
volumes: [ ../SerpentRace_Backend/logs:/app/logs ]
develop:
watch:
- action: sync
path: ../SerpentRace_Backend/src
target: /app/src
ignore:
- node_modules/
- dist/
- "*.log"
- action: sync
path: ../SerpentRace_Backend/package.json
target: /app/package.json
- action: rebuild
path: ../SerpentRace_Backend/package-lock.json
- action: rebuild
path: ../SerpentRace_Backend/tsconfig.json
- action: rebuild
path: ./Dockerfile_backend.dev
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_healthy
network_mode: host
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Frontend service with hot reload
frontend:
build:
context: ../SerpentRace_Frontend
dockerfile: ../SerpentRace_Docker/Dockerfile_frontend.dev
container_name: serpentrace-frontend-dev
restart: unless-stopped
ports:
- "5173:5173"
environment:
- NODE_ENV=development
- VITE_API_URL=http://localhost:3000
volumes: []
develop:
watch:
- action: sync
path: ../SerpentRace_Frontend/src
target: /app/src
ignore:
- node_modules/
- dist/
- "*.log"
- action: sync
path: ../SerpentRace_Frontend/public
target: /app/public
- action: sync
path: ../SerpentRace_Frontend/package.json
target: /app/package.json
- action: rebuild
path: ../SerpentRace_Frontend/package-lock.json
- action: rebuild
path: ../SerpentRace_Frontend/vite.config.js
- action: rebuild
path: ./Dockerfile_frontend.dev
depends_on:
- backend
network_mode: host
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: serpentrace-postgres-dev
restart: unless-stopped
ports:
- "5432:5432"
environment:
POSTGRES_DB: serpentrace
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
volumes:
- postgres_dev_data:/var/lib/postgresql/data
- ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql:ro
network_mode: host
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
# Redis Cache
redis:
image: redis:7-alpine
container_name: serpentrace-redis-dev
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_dev_data:/data
command: redis-server --appendonly yes
network_mode: host
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# MinIO Object Storage
minio:
image: minio/minio:latest
container_name: serpentrace-minio-dev
restart: unless-stopped
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: serpentrace
MINIO_ROOT_PASSWORD: serpentrace123!
volumes:
- minio_dev_data:/data
command: server /data --console-address ":9001"
network_mode: host
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 5
# Redis Commander for development debugging
redis-commander:
image: rediscommander/redis-commander:latest
container_name: serpentrace-redis-commander-dev
restart: unless-stopped
ports:
- "8081:8081"
environment:
- REDIS_HOSTS=local:redis:6379
depends_on:
redis:
condition: service_healthy
network_mode: host
# Database administration tool
pgadmin:
image: dpage/pgadmin4:latest
container_name: serpentrace-pgadmin-dev
restart: unless-stopped
ports:
- "8080:80"
environment:
PGADMIN_DEFAULT_EMAIL: admin@serpentrace.dev
PGADMIN_DEFAULT_PASSWORD: admin
PGADMIN_CONFIG_SERVER_MODE: 'False'
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
PGADMIN_CONFIG_WTF_CSRF_ENABLED: 'False'
volumes:
- pgadmin_dev_data:/var/lib/pgadmin
- ./pgadmin_servers.json:/pgadmin4/servers.json:ro
depends_on:
postgres:
condition: service_healthy
network_mode: host
volumes:
postgres_dev_data:
driver: local
redis_dev_data:
driver: local
minio_dev_data:
driver: local
pgadmin_dev_data:
driver: local
+1 -2
View File
@@ -75,8 +75,7 @@ services:
environment:
- NODE_ENV=development
- VITE_API_URL=http://localhost:3000
volumes:
[]
volumes: []
develop:
watch:
- action: sync
+3 -3
View File
@@ -1,10 +1,10 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/pictures/Logo.png" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SerpentRace</title>
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
-23
View File
@@ -15,7 +15,6 @@
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.6.0",
"react-toastify": "^11.0.5",
"tailwindcss": "^4.1.7"
},
"devDependencies": {
@@ -1817,15 +1816,6 @@
"node": ">=18"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3373,19 +3363,6 @@
"node": ">=18"
}
},
"node_modules/react-toastify": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
"integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
"license": "MIT",
"dependencies": {
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
-1
View File
@@ -17,7 +17,6 @@
"react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.6.0",
"react-toastify": "^11.0.5",
"tailwindcss": "^4.1.7"
},
"devDependencies": {
+4 -12
View File
@@ -10,15 +10,11 @@ import Landingpage from "./pages/Landing/Landingpage"
import Home from "./pages/Landing/Home"
import DeckManagerPage from "./pages/Decks/DeckManagerPage"
import DeckCreator from "./pages/DeckCreator/DeckCreator"
import CompanyHub from "./pages/Contacts/Contacts"
import CompanyHub from "./pages/Companies/Companies"
import About from "./pages/About/About"
import ScrollToTop from "./components/ScrollToTop"
import GameScreen from "./pages/Game/GameScreen"
import Reports from "./pages/Report/Reports"
import Lobby from "./pages/Lobby/Lobby"
import { ToastConfig } from "./components/Toastify/toastifyServices" // ✅ fontos: named import, nem default!
function App() {
const [isMobile, setIsMobile] = useState(false)
@@ -47,11 +43,9 @@ function App() {
// }
return (
<>
<Router>
<Routes>
<Route path="/about" element={<About />} />
<Route path="/lobby" element={<Lobby />} />
<Route path="/register" element={<AuthRegister />} />
<Route path="/login" element={<AuthLogin />} />
<Route path="/verify-email" element={<EmailVerification />} />
@@ -64,14 +58,12 @@ function App() {
<Route path="/deck-creator" element={<DeckCreator />} />
<Route path="/deck-creator/:deckId" element={<DeckCreator />} />
<Route path="/game" element={<GameScreen />} />
<Route path="/contacts" element={<CompanyHub />} />
<Route path="/companies" element={<CompanyHub />} />
<Route path="/report" element={<Reports />} />
{/* Add more routes as needed */}
</Routes>
</Router>
{/* ✅ Toastify Container */}
<ToastConfig />
</>
)
}
-25
View File
@@ -1,25 +0,0 @@
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
}
+35 -7
View File
@@ -1,13 +1,13 @@
import axios from "axios"
export const API_CONFIG = {
baseURL: (import.meta.env.VITE_API_URL ? import.meta.env.VITE_API_URL : "") + "/api",
baseURL: import.meta.env.VITE_API_URL + "/api",
wsURL: "http://localhost:3000",
timeout: 10000,
retryAttempts: 3,
}
export const apiClient = axios.create({
const apiClient = axios.create({
baseURL: API_CONFIG.baseURL,
timeout: API_CONFIG.timeout,
withCredentials: true, // Important for cookie-based auth
@@ -16,11 +16,39 @@ export const apiClient = axios.create({
},
})
// Add request interceptor for debugging
apiClient.interceptors.request.use(
(config) => {
console.log("Request URL:", config.url)
console.log("Request headers:", config.headers)
console.log("Current cookies:", document.cookie)
return config
},
(error) => {
return Promise.reject(error)
}
)
// Add response interceptor for debugging cookies
apiClient.interceptors.response.use(
(response) => {
console.log("Response status:", response.status)
console.log("Response headers:", response.headers)
console.log("Set-Cookie headers:", response.headers["set-cookie"])
console.log("Cookies after response:", document.cookie)
return response
},
(error) => {
console.error("API Error:", error.response?.data || error.message)
return Promise.reject(error)
}
)
//login
export const login = async (username, password) => {
try {
const response = await apiClient.post("/users/login", { username, password })
return response
return response.data
} catch (error) {
throw error
}
@@ -30,16 +58,16 @@ export const login = async (username, password) => {
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
return { ...response.data, status: response.status }
} catch (error) {
throw error
}
}
// Get current user's game statistics
export const getUserStats = async () => {
//verify email
export const verifyEmail = async (token) => {
try {
const response = await apiClient.get("/users/me/stats")
const response = await apiClient.get(`/users/verify-email/${token}`)
return response.data
} catch (error) {
throw error
@@ -7,8 +7,6 @@ import TaskCardEditor from "./TaskCardEditor.jsx"
import JokerCardEditor from "./JokerCardEditor.jsx"
import LuckCardEditor from "./LuckCardEditor.jsx"
import CardPreview from "./CardPreview.jsx"
import { notifySuccess, notifyError,notifyWarning } from "../../components/Toastify/toastifyServices"
export default function CardEditor({ card, isCreating, cardType, onSave, onCancel }) {
const [cardData, setCardData] = useState(null)
@@ -20,45 +18,29 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
id: null,
type: type,
points: 10,
timeLimit: 30,
consequence: { type: 0, value: 1 }
timeLimit: 30
}
switch (type) {
case 'QUESTION':
case 'task':
return {
...baseData,
subType: 'quiz',
question: '',
options: ['', '', '', ''],
correctAnswer: 0,
explanation: '',
acceptedAnswers: [''],
wrongConsequence: { type: 1, value: 1 }
explanation: ''
}
case 'PAIRING':
case 'MATCHING':
return {
...baseData,
type: 'QUESTION',
subType: 'matching',
taskDescription: '',
leftItems: ['', ''],
rightItems: ['', ''],
correctPairs: { 0: 0, 1: 1 },
wrongConsequence: { type: 1, value: 1 }
}
case 'JOKER':
case 'joker':
return {
...baseData,
title: '',
description: '',
effect: '',
actionType: 'skip',
usage: 'once',
wrongConsequence: { type: 1, value: 1 }
usage: 'once'
}
case 'LUCK':
case 'luck':
return {
...baseData,
event: '',
@@ -74,119 +56,53 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
// Kártya adatok inicializálása
useEffect(() => {
try {
if (isCreating && cardType) {
const defaultData = getDefaultCardData(cardType)
setCardData(defaultData)
setCardData(getDefaultCardData(cardType))
} else if (card) {
setCardData({ ...card })
} else {
setCardData(null)
}
} catch (error) {
console.error('Kártya inicializálási hiba:', error)
setCardData(null)
}
}, [card, isCreating, cardType])
const validateCard = (data) => {
try {
if (!data || !data.type) {
notifyError("Érvénytelen kártya adatok!")
return false
const handleSave = () => {
if (!cardData) return
// Validáció
if (!validateCard(cardData)) return
onSave(cardData)
}
if (data.type === 'QUESTION') {
// Quiz típus validálás
if (data.subType === 'quiz') {
if (!data.question || !data.question.trim()) {
notifyError("Kérdés megadása kötelező!")
return false
}
if (data.options && data.options.some(opt => !opt.trim())) {
notifyError("Minden válaszlehetőséget ki kell tölteni!")
return false
}
}
// Igaz/Hamis típus validálás
else if (data.subType === 'truefalse') {
if (!data.statement || !data.statement.trim()) {
notifyError("Állítás megadása kötelező!")
return false
}
if (data.isTrue === undefined || data.isTrue === null) {
notifyError("Válaszd ki, hogy az állítás igaz vagy hamis!")
return false
}
}
// Párosítás típus validálás
else if (data.subType === 'matching') {
if (!data.taskDescription || !data.taskDescription.trim()) {
notifyError("Feladat leírása kötelező!")
return false
}
if (!data.leftItems || data.leftItems.length === 0) {
notifyError("Legalább egy párosítást meg kell adni!")
return false
}
if (data.leftItems.some(item => !item.trim()) || data.rightItems.some(item => !item.trim())) {
notifyError("Minden párosítási elemet ki kell tölteni!")
return false
}
}
// Szöveges válasz típus validálás
else if (data.subType === 'text') {
if (!data.question || !data.question.trim()) {
notifyError("Kérdés megadása kötelező!")
return false
}
if (!data.acceptedAnswers || data.acceptedAnswers.length === 0 || data.acceptedAnswers.every(ans => !ans.trim())) {
notifyError("Legalább egy elfogadott választ meg kell adni!")
return false
}
}
// Általános validálás (ha nincs subType megadva)
else {
const validateCard = (data) => {
if (data.type === 'task') {
if (!data.question && !data.statement) {
notifyError("Kérdés vagy állítás megadása kötelező!")
alert("Kérdés vagy állítás megadása kötelező!")
return false
}
}
} else if (data.type === 'JOKER') {
if (!data.text || !data.text.trim()) {
notifyError("Joker kártya szövege nem lehet üres!")
if (data.subType === 'quiz' && data.options.some(opt => !opt.trim())) {
alert("❌ Minden válaszlehetőséget ki kell tölteni!")
return false
}
} else if (data.type === 'LUCK') {
} else if (data.type === 'joker') {
if (!data.text || !data.text.trim()) {
notifyError("Szerencse kártya szövege nem lehet üres!")
alert("❌ Joker kártya szövege nem lehet üres!")
return false
}
} else if (data.type === 'luck') {
if (!data.text || !data.text.trim()) {
alert("❌ Szerencse kártya szövege nem lehet üres!")
return false
}
}
return true
} catch (error) {
console.error('Validálási hiba:', error)
notifyError("Hiba történt a kártya ellenőrzése során")
return false
}
}
const updateCardData = (updates) => {
setCardData(prev => prev ? { ...prev, ...updates } : null)
}
const handleSave = () => {
if (!cardData) {
notifyError("Nincs mentendő kártya adat!")
return
}
if (!validateCard(cardData)) return
onSave(cardData)
}
// Ha nincs kiválasztott kártya vagy új kártya létrehozás
if (!cardData) {
return (
@@ -207,47 +123,24 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
return (
<div className="flex-1 flex flex-col">
{/* Type Mismatch Warning */}
{cardData?.type && cardType && cardData.type !== cardType && !isCreating && (
<div className="bg-[color:var(--color-error)]/10 border-l-4 border-[color:var(--color-error)] px-6 py-4">
<div className="flex items-center gap-3">
<div className="text-[color:var(--color-error)] text-xl"></div>
<div>
<div className="text-[color:var(--color-error)] font-semibold">
Figyelmeztetés: Nem megfelelő kártya típus
</div>
<div className="text-[color:var(--color-error)]/80 text-sm">
{`Ez egy ${
cardData.type === 'QUESTION' ? 'Feladat' :
cardData.type === 'JOKER' ? 'Joker' : 'Szerencse'
} kártya, de a pakli típusa ${
cardType === 'QUESTION' ? 'Feladat' :
cardType === 'JOKER' ? 'Joker' : 'Szerencse'
}.`}
</div>
</div>
</div>
</div>
)}
{/* Header */}
<div className="bg-[color:var(--color-surface)] border-b border-[color:var(--color-surface-selected)] px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="text-2xl">
{cardData.type === 'QUESTION' && '📋'}
{cardData.type === 'JOKER' && '🃏'}
{cardData.type === 'LUCK' && '🎲'}
{cardData.type === 'task' && '📋'}
{cardData.type === 'joker' && '🃏'}
{cardData.type === 'luck' && '🎲'}
</div>
<div>
<h2 className="text-xl font-bold text-[color:var(--color-text)]">
{isCreating ? 'Új' : 'Szerkesztés'} {' '}
{(isCreating ? cardType : cardData.type) === 'QUESTION' && 'Feladat kártya'}
{(isCreating ? cardType : cardData.type) === 'JOKER' && 'Joker kártya'}
{(isCreating ? cardType : cardData.type) === 'LUCK' && 'Szerencse kártya'}
{cardData.type === 'task' && 'Feladat kártya'}
{cardData.type === 'joker' && 'Joker kártya'}
{cardData.type === 'luck' && 'Szerencse kártya'}
</h2>
<div className="text-[color:var(--color-text-muted)] text-sm">
{cardData.type === 'QUESTION' && cardData.subType && (
{cardData.type === 'task' && cardData.subType && (
<>
{cardData.subType === 'quiz' && 'Quiz (A/B/C/D)'}
{cardData.subType === 'truefalse' && 'Igaz/Hamis'}
@@ -275,10 +168,7 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
</button>
<button
onClick={() => {
notifyWarning('Kártya készítés megszakítva')
onCancel()
}}
onClick={onCancel}
className="flex items-center gap-2 px-4 py-2 rounded-xl bg-[color:var(--color-background)] hover:bg-[color:var(--color-surface-selected)] text-[color:var(--color-text)] transition-all duration-200"
>
<FaTimes />
@@ -299,26 +189,28 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
{/* Content */}
<div className="flex-1 overflow-hidden">
{showPreview ? (
/* Preview Mode */
<div className="h-full bg-[color:var(--color-background)] flex items-center justify-center p-6">
<CardPreview card={cardData} />
</div>
) : (
/* Edit Mode */
<div className="h-full overflow-y-auto p-6">
{cardData.type === 'QUESTION' && (
{cardData.type === 'task' && (
<TaskCardEditor
card={cardData}
onChange={updateCardData}
/>
)}
{cardData.type === 'JOKER' && (
{cardData.type === 'joker' && (
<JokerCardEditor
card={cardData}
onChange={updateCardData}
/>
)}
{cardData.type === 'LUCK' && (
{cardData.type === 'luck' && (
<LuckCardEditor
card={cardData}
onChange={updateCardData}
@@ -1,23 +1,13 @@
// src/components/DeckCreator/CardsList.jsx
// Bal oldali kártyák listája és új kártya létrehozás
import React, { useState } from "react"
import {
FaPlus,
FaEdit,
FaTrash,
FaQuestionCircle,
FaCheck,
FaTimes,
FaDice,
FaTheaterMasks
} from "react-icons/fa"
import { notifySuccess, notifyError } from "../../components/Toastify/toastifyServices"
import React from "react"
import { FaPlus, FaEdit, FaTrash, FaQuestionCircle, FaCheck, FaTimes, FaDice, FaTheaterMasks } from "react-icons/fa"
const cardTypeIcons = {
QUESTION: { icon: FaQuestionCircle, color: "var(--color-question)" },
JOKER: { icon: FaTheaterMasks, color: "var(--color-fun)" },
LUCK: { icon: FaDice, color: "var(--color-luck)" }
task: { icon: FaQuestionCircle, color: "var(--color-question)" },
joker: { icon: FaTheaterMasks, color: "var(--color-fun)" },
luck: { icon: FaDice, color: "var(--color-luck)" }
}
const cardSubTypeLabels = {
@@ -30,80 +20,84 @@ const cardSubTypeLabels = {
export default function CardsList({
cards,
selectedCard,
deckType,
onSelectCard,
onCreateCard,
onDeleteCard,
isCreatingCard,
newCardType
}) {
const [confirmingDelete, setConfirmingDelete] = useState(null)
const getCardPreview = (card) => {
if (card.type === 'QUESTION') {
if (card.type === 'task') {
return card.question || card.statement || 'Új feladat kártya'
}
if (card.type === 'JOKER') {
if (card.type === 'joker') {
return card.text || 'Új joker kártya'
}
if (card.type === 'LUCK') {
if (card.type === 'luck') {
return card.text || 'Új szerencse kártya'
}
return "Ismeretlen kártya"
return 'Ismeretlen kártya'
}
const getCardTypeLabel = (card) => {
if (card.type === 'QUESTION') {
if (card.type === 'task') {
if (card.subType) {
return cardSubTypeLabels[card.subType] || "Feladat"
return cardSubTypeLabels[card.subType] || 'Feladat'
}
return "Feladat"
return 'Feladat'
}
if (card.type === 'JOKER') {
if (card.type === 'joker') {
return 'Joker'
}
if (card.type === 'LUCK') {
if (card.type === 'luck') {
return 'Szerencse'
}
return "Ismeretlen"
}
const handleConfirmDelete = () => {
if (confirmingDelete) {
onDeleteCard(confirmingDelete)
notifySuccess("Kártya sikeresen törölve a pakliból!")
setConfirmingDelete(null)
}
}
const handleCancelDelete = () => {
setConfirmingDelete(null)
return 'Ismeretlen'
}
return (
<div className="flex flex-col h-full relative">
<div className="flex flex-col h-full">
{/* Header */}
<div className="p-4 border-b border-[color:var(--color-surface-selected)]">
<h2 className="text-lg font-bold text-[color:var(--color-text)] mb-4 flex items-center gap-2">
🃏 Kártyák
</h2>
{/* New Card Button */}
<button
onClick={() => onCreateCard(deckType)}
className={`w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-[color:var(--color-text-inverse)] font-semibold transition-all duration-200 hover:scale-105 shadow-lg ${
deckType === 'QUESTION' ? 'bg-[color:var(--color-question)] hover:bg-[color:var(--color-question)]/80' :
deckType === 'LUCK' ? 'bg-[color:var(--color-luck)] hover:bg-[color:var(--color-luck)]/80' :
'bg-[color:var(--color-fun)] hover:bg-[color:var(--color-fun)]/80'
}`}
>
{/* New Card Dropdown */}
<div className="relative group">
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl bg-[color:var(--color-success)] hover:bg-[color:var(--color-success)]/80 text-[color:var(--color-text-inverse)] font-semibold transition-all duration-200 hover:scale-105 shadow-lg">
<FaPlus />
<span>
{deckType === 'QUESTION' && '📋 Új feladat kártya'}
{deckType === 'JOKER' && '🃏 Új joker kártya'}
{deckType === 'LUCK' && '🎲 Új szerencse kártya'}
</span>
Új kártya
</button>
{/* Dropdown Menu */}
<div className="absolute top-full left-0 right-0 mt-2 bg-[color:var(--color-card)] rounded-xl shadow-lg border border-[color:var(--color-surface-selected)] opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-10">
<button
onClick={() => onCreateCard('task')}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-[color:var(--color-surface-selected)] text-[color:var(--color-text)] transition-colors duration-200 rounded-t-xl"
>
<FaQuestionCircle className="text-[color:var(--color-question)]" />
📋 Feladat kártya
</button>
<button
onClick={() => onCreateCard('joker')}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-[color:var(--color-surface-selected)] text-[color:var(--color-text)] transition-colors duration-200"
>
<FaTheaterMasks className="text-[color:var(--color-fun)]" />
🃏 Joker kártya
</button>
<button
onClick={() => onCreateCard('luck')}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-[color:var(--color-surface-selected)] text-[color:var(--color-text)] transition-colors duration-200 rounded-b-xl"
>
<FaDice className="text-[color:var(--color-luck)]" />
🎲 Szerencse kártya
</button>
</div>
</div>
</div>
{/* Cards List */}
@@ -121,7 +115,7 @@ export default function CardsList({
)}
<div>
<div className="text-[color:var(--color-text)] font-medium">
Új {newCardType === "QUESTION" ? "feladat" : newCardType === "JOKER" ? "joker" : "szerencse"} kártya
Új {newCardType === 'task' ? 'feladat' : newCardType === 'joker' ? 'joker' : 'szerencse'} kártya
</div>
<div className="text-[color:var(--color-text-muted)] text-sm">
Szerkesztés folyamatban...
@@ -141,31 +135,17 @@ export default function CardsList({
key={card.id}
onClick={() => onSelectCard(card)}
className={`
p-4 rounded-xl border cursor-pointer transition-all duration-200 hover:scale-105 group relative
${
isSelected
? "bg-[color:var(--color-success)]/10 border-[color:var(--color-success)] shadow-lg"
: "bg-[color:var(--color-background)]/50 border-[color:var(--color-surface-selected)] hover:bg-[color:var(--color-background)]/80"
p-4 rounded-xl border cursor-pointer transition-all duration-200 hover:scale-105 group
${isSelected
? 'bg-[color:var(--color-success)]/10 border-[color:var(--color-success)] shadow-lg'
: 'bg-[color:var(--color-background)]/50 border-[color:var(--color-surface-selected)] hover:bg-[color:var(--color-background)]/80'
}
${card.type !== deckType ? "opacity-70" : ""}
`}
>
{card.type !== deckType && (
<div className="absolute inset-0 flex items-center justify-center">
<div className="absolute inset-0 bg-[color:var(--color-error)]/5 backdrop-blur-[1px] rounded-xl"></div>
<div className="absolute rotate-[-15deg] border-t-2 border-[color:var(--color-error)] w-full"></div>
<span className="absolute top-1 right-1 text-xs text-[color:var(--color-error)] bg-[color:var(--color-error)]/10 px-2 py-1 rounded-lg">
Nem megfelelő típus
</span>
</div>
)}
{/* Card Header */}
<div className="flex items-start justify-between gap-2 mb-3">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div
className="flex items-center justify-center w-10 h-10 rounded-full border-2"
style={{ borderColor: cardIcon.color }}
>
<div className="flex items-center justify-center w-10 h-10 rounded-full border-2" style={{ borderColor: cardIcon.color }}>
{React.createElement(cardIcon.icon, {
style: { color: cardIcon.color },
className: "text-lg"
@@ -189,7 +169,7 @@ export default function CardsList({
<button
onClick={(e) => {
e.stopPropagation()
setConfirmingDelete(card.id)
onDeleteCard(card.id)
}}
className="p-1.5 rounded-lg bg-[color:var(--color-error)]/10 hover:bg-[color:var(--color-error)]/20 text-[color:var(--color-error)] transition-colors duration-200"
>
@@ -203,10 +183,10 @@ export default function CardsList({
<div
className="text-[color:var(--color-text)] text-sm leading-relaxed"
style={{
display: "-webkit-box",
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden"
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}
>
{getCardPreview(card)}
@@ -229,40 +209,20 @@ export default function CardsList({
)}
</div>
{/* Confirm Delete Popup */}
{confirmingDelete && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl p-6 w-80 text-center animate-fadeIn">
<h3 className="text-lg font-semibold mb-4 text-gray-800">
Biztosan törölni szeretnéd?
</h3>
<p className="text-sm text-gray-600 mb-6">
Ez a művelet nem visszavonható.
</p>
<div className="flex justify-center gap-4">
<button
onClick={handleConfirmDelete}
className="bg-[color:var(--color-error)] text-white px-4 py-2 rounded-lg hover:bg-red-600 transition"
>
Igen
</button>
<button
onClick={handleCancelDelete}
className="bg-gray-200 px-4 py-2 rounded-lg hover:bg-gray-300 transition"
>
Mégse
</button>
</div>
</div>
</div>
)}
{/* Footer Stats */}
<div className="p-4 border-t border-[color:var(--color-surface-selected)] bg-[color:var(--color-background)]/30">
<div className="text-center">
<div className="text-[color:var(--color-text)] font-semibold">
📊 Összesen: {cards.length} kártya
</div>
{cards.length > 0 && (
<div className="flex justify-center gap-4 mt-2 text-xs text-[color:var(--color-text-muted)]">
<span>📋 {cards.filter(c => c.type === 'task').length}</span>
<span>🃏 {cards.filter(c => c.type === 'joker').length}</span>
<span>🎲 {cards.filter(c => c.type === 'luck').length}</span>
</div>
)}
</div>
</div>
</div>
@@ -1,13 +1,13 @@
// src/components/DeckCreator/DeckHeader.jsx
// Deck alapadatok szerkesztése és mentés
import React, { useState, useRef, useEffect } from "react"
import React from "react"
import { FaSave, FaArrowLeft, FaGlobe, FaLock, FaQuestionCircle, FaDice, FaLaughBeam } from "react-icons/fa"
const deckTypes = [
{ value: "QUESTION", label: "Kérdés", icon: FaQuestionCircle, color: "var(--color-question)" },
{ value: "LUCK", label: "Szerencse", icon: FaDice, color: "var(--color-luck)" },
{ value: "JOKER", label: "Joker", icon: FaLaughBeam, color: "var(--color-fun)" }
{ value: "Question", label: "Kérdés", icon: FaQuestionCircle, color: "var(--color-question)" },
{ value: "Luck", label: "Szerencse", icon: FaDice, color: "var(--color-luck)" },
{ value: "Fun", label: "Szórakozás", icon: FaLaughBeam, color: "var(--color-fun)" }
]
const privacyOptions = [
@@ -16,35 +16,17 @@ const privacyOptions = [
]
export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {
const [isTypeDropdownOpen, setIsTypeDropdownOpen] = useState(false);
const [isPrivacyDropdownOpen, setIsPrivacyDropdownOpen] = useState(false);
const typeDropdownRef = useRef(null);
const privacyDropdownRef = useRef(null);
const currentDeckType = deckTypes.find(type => type.value === deck.type) || deckTypes[0]
const currentPrivacy = privacyOptions.find(option => option.value === deck.privacy) || privacyOptions[0]
useEffect(() => {
function handleClickOutside(event) {
if (typeDropdownRef.current && !typeDropdownRef.current.contains(event.target)) {
setIsTypeDropdownOpen(false);
}
if (privacyDropdownRef.current && !privacyDropdownRef.current.contains(event.target)) {
setIsPrivacyDropdownOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleInputChange = (field, value) => {
onUpdate({ [field]: value })
}
// Remove unused card count variables
const cardsCount = deck.cards?.length || 0
const taskCards = deck.cards?.filter(card => card.type === 'task')?.length || 0
const jokerCards = deck.cards?.filter(card => card.type === 'joker')?.length || 0
const luckCards = deck.cards?.filter(card => card.type === 'luck')?.length || 0
return (
<div className="bg-[color:var(--color-surface)] border-b border-[color:var(--color-surface-selected)] px-6 py-4">
@@ -60,7 +42,7 @@ export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {
</button>
<h1 className="text-2xl font-bold text-[color:var(--color-text)]">
📝 Pakli Szerkesztés
📝 Deck Szerkesztés
</h1>
</div>
@@ -74,79 +56,41 @@ export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {
</div>
{/* Main Content Row */}
<div className="space-y-4">
{/* Two Column Layout */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Deck Name - Takes up 2 columns */}
<div className="md:col-span-2">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-end">
{/* Deck Basic Info */}
<div className="lg:col-span-2 space-y-4">
{/* Deck Name */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
📦 Pakli neve
📦 Deck neve
</label>
<input
type="text"
value={deck.name}
onChange={(e) => handleInputChange('name', e.target.value)}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200"
placeholder="Add meg a pakli nevét..."
placeholder="Add meg a deck nevét..."
/>
</div>
{/* Empty space for visual balance */}
<div className="hidden md:block"></div>
</div>
{/* Type, Privacy and Description Row */}
{/* Type and Privacy Row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Deck Type */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
🎯 Típus
</label>
<div className="relative">
<button
type="button"
onClick={() => setIsTypeDropdownOpen(!isTypeDropdownOpen)}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200 flex items-center"
style={{
paddingLeft: "2.5rem"
}}
>
<div className="absolute left-4 top-1/2 -translate-y-1/2">
{React.createElement(currentDeckType.icon, {
style: { color: currentDeckType.color }
})}
</div>
{currentDeckType.label}
<div className="absolute right-4 top-1/2 -translate-y-1/2">
<svg className={`w-4 h-4 text-[color:var(--color-text-muted)] transform transition-transform ${isTypeDropdownOpen ? 'rotate-180' : ''}`} viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</div>
</button>
{isTypeDropdownOpen && (
<div
className="absolute z-10 w-full mt-1 bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] rounded-xl shadow-lg overflow-hidden"
ref={typeDropdownRef}
<select
value={deck.type}
onChange={(e) => handleInputChange('type', 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"
>
{deckTypes.map(type => (
<button
key={type.value}
onClick={() => {
handleInputChange('type', type.value);
setIsTypeDropdownOpen(false);
}}
className={`w-full px-4 py-2 flex items-center gap-3 hover:bg-[color:var(--color-surface-selected)] transition-colors text-[color:var(--color-text)] ${deck.type === type.value ? 'bg-[color:var(--color-surface-selected)]' : ''}`}
>
{React.createElement(type.icon, {
style: { color: type.color }
})}
<span>{type.label}</span>
</button>
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</div>
)}
</div>
</select>
</div>
{/* Privacy */}
@@ -154,51 +98,17 @@ export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
👁 Láthatóság
</label>
<div className="relative">
<button
type="button"
onClick={() => setIsPrivacyDropdownOpen(!isPrivacyDropdownOpen)}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200 flex items-center"
style={{
paddingLeft: "2.5rem"
}}
>
<div className="absolute left-4 top-1/2 -translate-y-1/2">
{React.createElement(currentPrivacy.icon, {
className: "text-[color:var(--color-text)]"
})}
</div>
{currentPrivacy.label}
<div className="absolute right-4 top-1/2 -translate-y-1/2">
<svg className={`w-4 h-4 text-[color:var(--color-text-muted)] transform transition-transform ${isPrivacyDropdownOpen ? 'rotate-180' : ''}`} viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</div>
</button>
{isPrivacyDropdownOpen && (
<div
className="absolute z-10 w-full mt-1 bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] rounded-xl shadow-lg overflow-hidden"
ref={privacyDropdownRef}
<select
value={deck.privacy}
onChange={(e) => handleInputChange('privacy', 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"
>
{privacyOptions.map(option => (
<button
key={option.value}
onClick={() => {
handleInputChange('privacy', option.value);
setIsPrivacyDropdownOpen(false);
}}
className={`w-full px-4 py-2 flex items-center gap-3 hover:bg-[color:var(--color-surface-selected)] transition-colors text-[color:var(--color-text)] ${deck.privacy === option.value ? 'bg-[color:var(--color-surface-selected)]' : ''}`}
>
{React.createElement(option.icon, {
className: "text-[color:var(--color-text)]"
})}
<span>{option.label}</span>
</button>
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</div>
)}
</div>
</select>
</div>
{/* Description */}
@@ -216,6 +126,36 @@ export default function DeckHeader({ deck, onUpdate, onSave, onBack }) {
</div>
</div>
</div>
{/* Stats Panel */}
<div className="bg-[color:var(--color-background)]/50 rounded-xl p-4 border border-[color:var(--color-surface-selected)]">
<h3 className="text-[color:var(--color-text)] font-semibold mb-3 flex items-center gap-2">
📊 Statisztikák
</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between items-center">
<span className="text-[color:var(--color-text-muted)]">Összes kártya:</span>
<span className="text-[color:var(--color-text)] font-semibold">{cardsCount}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[color:var(--color-text-muted)]">📋 Feladat:</span>
<span className="text-[color:var(--color-question)] font-semibold">{taskCards}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[color:var(--color-text-muted)]">🃏 Joker:</span>
<span className="text-[color:var(--color-success)] font-semibold">{jokerCards}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-[color:var(--color-text-muted)]">🎲 Szerencse:</span>
<span className="text-[color:var(--color-luck)] font-semibold">{luckCards}</span>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -4,29 +4,17 @@
import React, { useState, useEffect } from 'react'
import { FaTheaterMasks, FaInfoCircle, FaUsers } from 'react-icons/fa'
const consequenceTypes = [
{ value: 0, label: '⬆️ Előre lépés', description: 'A játékos előre lép X mezőt' },
{ value: 1, label: '⬇️ Hátra lépés', description: 'A játékos hátra lép X mezőt' },
{ value: 2, label: '⏸️ Kör kihagyás', description: 'A játékos kihagy egy kört' },
{ value: 3, label: '⏩ Extra kör', description: 'A játékos kap egy extra kört' },
{ value: 5, label: '🏁 Vissza a starthoz', description: 'A játékos visszakerül a starthoz' }
]
export default function JokerCardEditor({ card, onChange }) {
const [cardData, setCardData] = useState({
type: 'JOKER',
text: '',
consequence: { type: 0, value: 1 },
wrongConsequence: { type: 1, value: 1 }
type: 'joker',
text: ''
})
useEffect(() => {
if (card) {
setCardData({
type: 'JOKER',
text: card.text || '',
consequence: card.consequence || { type: 0, value: 1 },
wrongConsequence: card.wrongConsequence || { type: 1, value: 1 }
type: 'joker',
text: card.text || ''
})
}
}, [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
const exampleCards = [
"Felelsz vagy mersz? (Az előző játékos kérdez)",
@@ -99,10 +57,18 @@ export default function JokerCardEditor({ card, onChange }) {
}
return (
<div className="space-y-6">
{/* Info box */}
<div className="max-w-4xl mx-auto">
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<div className="bg-[color:var(--color-fun)]/10 border border-[color:var(--color-fun)]/30 rounded-lg p-4">
{/* Header */}
<div className="flex items-center gap-3 mb-6">
<FaTheaterMasks className="text-2xl text-[color:var(--color-fun)]" />
<h3 className="text-xl font-bold text-[color:var(--color-text)]">
Joker Kártya Szerkesztő
</h3>
</div>
{/* Info box */}
<div className="bg-[color:var(--color-fun)]/10 border border-[color:var(--color-fun)]/30 rounded-lg p-4 mb-6">
<div className="flex items-start gap-3">
<FaInfoCircle className="text-[color:var(--color-fun)] mt-1 flex-shrink-0" />
<div>
@@ -120,24 +86,18 @@ export default function JokerCardEditor({ card, onChange }) {
</div>
</div>
</div>
</div>
{/* Kártya szövege */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4 flex items-center gap-2">
<FaTheaterMasks className="text-[color:var(--color-fun)]" />
Kártya szövege
</h3>
{/* Card text input */}
<div className="space-y-4">
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
<label className="block text-[color:var(--color-text)] font-medium mb-2">
Joker kártya feladat *
</label>
<textarea
value={cardData.text}
onChange={handleTextChange}
placeholder="Pl: Felelsz vagy mersz? (Az előző játékos kérdez)"
className="w-full h-32 px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] placeholder-[color:var(--color-text-muted)] resize-none focus:ring-2 focus:ring-[color:var(--color-fun)] focus:border-transparent outline-none transition-all duration-200"
className="w-full h-32 p-4 bg-[color:var(--color-surface)] border border-[color:var(--color-border)] rounded-lg text-[color:var(--color-text)] placeholder-[color:var(--color-text-muted)] resize-none focus:outline-none focus:border-[color:var(--color-fun)] transition-colors"
maxLength={150}
/>
<div className="flex justify-between items-center mt-2">
@@ -149,6 +109,7 @@ export default function JokerCardEditor({ card, onChange }) {
</span>
</div>
</div>
</div>
{/* Example cards */}
<div className="mt-6">
@@ -186,100 +147,6 @@ export default function JokerCardEditor({ card, onChange }) {
</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>
)
}
@@ -4,27 +4,17 @@
import React, { useState, useEffect } from 'react'
import { FaDice, FaInfoCircle } from 'react-icons/fa'
const consequenceTypes = [
{ value: 0, label: '⬆️ Előre lépés', description: 'A játékos előre lép X mezőt' },
{ value: 1, label: '⬇️ Hátra lépés', description: 'A játékos hátra lép X mezőt' },
{ value: 2, label: '⏸️ Kör kihagyás', description: 'A játékos kihagy egy kört' },
{ value: 3, label: '⏩ Extra kör', description: 'A játékos kap egy extra kört' },
{ value: 5, label: '🏁 Vissza a starthoz', description: 'A játékos visszakerül a starthoz' }
]
export default function LuckCardEditor({ card, onChange }) {
const [cardData, setCardData] = useState({
type: 'LUCK',
text: '',
consequence: { type: 0, value: 1 }
type: 'luck',
text: ''
})
useEffect(() => {
if (card) {
setCardData({
type: 'LUCK',
text: card.text || '',
consequence: card.consequence || { type: 0, value: 1 }
type: 'luck',
text: card.text || ''
})
}
}, [card])
@@ -41,26 +31,19 @@ export default function LuckCardEditor({ card, onChange }) {
}
}
const updateConsequence = (field, value) => {
const newCardData = {
...cardData,
consequence: {
...cardData.consequence,
[field]: value
}
}
setCardData(newCardData)
if (onChange) {
onChange(newCardData)
}
}
return (
<div className="space-y-6">
{/* Info box */}
<div className="max-w-4xl mx-auto">
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<div className="bg-[color:var(--color-luck)]/10 border border-[color:var(--color-luck)]/30 rounded-lg p-4">
{/* Header */}
<div className="flex items-center gap-3 mb-6">
<FaDice className="text-2xl text-[color:var(--color-luck)]" />
<h3 className="text-xl font-bold text-[color:var(--color-text)]">
Szerencse Kártya Szerkesztő
</h3>
</div>
{/* Info box */}
<div className="bg-[color:var(--color-luck)]/10 border border-[color:var(--color-luck)]/30 rounded-lg p-4 mb-6">
<div className="flex items-start gap-3">
<FaInfoCircle className="text-[color:var(--color-luck)] mt-1 flex-shrink-0" />
<div>
@@ -77,23 +60,18 @@ export default function LuckCardEditor({ card, onChange }) {
</div>
</div>
</div>
</div>
{/* Kártya szövege */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4 flex items-center gap-2">
<FaDice className="text-[color:var(--color-luck)]" />
Kártya szövege
</h3>
{/* Card text input */}
<div className="space-y-4">
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Szerencse esemény leírása *
<label className="block text-[color:var(--color-text)] font-medium mb-2">
Kártya szövege *
</label>
<textarea
value={cardData.text}
onChange={handleTextChange}
placeholder="Pl: Órai projektekkel kiváltottál több vizsgát is! Lépj előre 4 mezőt"
className="w-full h-32 px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] placeholder-[color:var(--color-text-muted)] resize-none focus:ring-2 focus:ring-[color:var(--color-luck)] focus:border-transparent outline-none transition-all duration-200"
className="w-full h-32 p-4 bg-[color:var(--color-surface)] border border-[color:var(--color-border)] rounded-lg text-[color:var(--color-text)] placeholder-[color:var(--color-text-muted)] resize-none focus:outline-none focus:border-[color:var(--color-luck)] transition-colors"
maxLength={200}
/>
<div className="flex justify-between items-center mt-2">
@@ -105,6 +83,7 @@ export default function LuckCardEditor({ card, onChange }) {
</span>
</div>
</div>
</div>
{/* Preview */}
{cardData.text.trim() && (
@@ -121,53 +100,6 @@ export default function LuckCardEditor({ card, onChange }) {
</div>
)}
</div>
{/* Következmények */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
🎯 Következmények
</h3>
<div className="space-y-4">
{/* Consequence Type */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Hatás típusa
</label>
<select
value={cardData.consequence?.type ?? 0}
onChange={(e) => updateConsequence('type', parseInt(e.target.value))}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-luck)] focus:border-transparent outline-none transition-all duration-200"
>
{consequenceTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
<div className="text-xs text-[color:var(--color-text-muted)] mt-1">
{consequenceTypes.find(t => t.value === (cardData.consequence?.type ?? 0))?.description}
</div>
</div>
{/* Consequence Value - csak ha előre/hátra lépés */}
{(cardData.consequence?.type === 0 || cardData.consequence?.type === 1) && (
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Mezők száma
</label>
<input
type="number"
min="1"
max="10"
value={cardData.consequence?.value ?? 1}
onChange={(e) => updateConsequence('value', parseInt(e.target.value) || 1)}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-luck)] focus:border-transparent outline-none transition-all duration-200"
/>
</div>
)}
</div>
</div>
</div>
)
}
@@ -20,14 +20,6 @@ const timeLimits = [
{ 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 }) {
const updateField = (field, value) => {
@@ -79,39 +71,16 @@ export default function TaskCardEditor({ card, onChange }) {
}
const updateAcceptedAnswer = (index, value) => {
const currentAnswers = card.acceptedAnswers || ['']
const newAnswers = [...currentAnswers]
const newAnswers = [...card.acceptedAnswers]
newAnswers[index] = value
onChange({ acceptedAnswers: newAnswers })
}
const removeAcceptedAnswer = (index) => {
const currentAnswers = card.acceptedAnswers || ['']
if (currentAnswers.length <= 1) return // Legalább egy válasz maradjon
const newAnswers = currentAnswers.filter((_, i) => i !== index)
const newAnswers = card.acceptedAnswers.filter((_, i) => i !== index)
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 (
<div className="max-w-4xl mx-auto space-y-6">
{/* Feladat típus választó */}
@@ -378,7 +347,7 @@ export default function TaskCardEditor({ card, onChange }) {
</div>
<div className="space-y-2">
{(card.acceptedAnswers || ['']).map((answer, index) => (
{(card.acceptedAnswers || ['', '', '']).map((answer, index) => (
<div key={index} className="flex gap-2">
<input
type="text"
@@ -388,7 +357,7 @@ export default function TaskCardEditor({ card, onChange }) {
placeholder={`Elfogadott válasz ${index + 1}...`}
/>
{(card.acceptedAnswers?.length || 1) > 1 && (
{(card.acceptedAnswers?.length || 0) > 1 && (
<button
onClick={() => removeAcceptedAnswer(index)}
className="p-2 rounded-lg bg-[color:var(--color-error)]/10 text-[color:var(--color-error)] hover:bg-[color:var(--color-error)]/20 transition-all duration-200"
@@ -405,14 +374,13 @@ export default function TaskCardEditor({ card, onChange }) {
</div>
</div>
{/* Beállítások - Később elérhető */}
<div className="relative">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 opacity-50 pointer-events-none blur-[1px]">
{/* Beállítások */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={false}
disabled
checked={card.caseSensitive || false}
onChange={(e) => updateField('caseSensitive', e.target.checked)}
className="w-4 h-4 text-[color:var(--color-success)] border-[color:var(--color-surface-selected)] rounded focus:ring-[color:var(--color-success)]"
/>
<span className="text-[color:var(--color-text)]">Kis/nagy betű érzékeny</span>
@@ -421,8 +389,8 @@ export default function TaskCardEditor({ card, onChange }) {
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={false}
disabled
checked={card.exactMatch || false}
onChange={(e) => updateField('exactMatch', e.target.checked)}
className="w-4 h-4 text-[color:var(--color-success)] border-[color:var(--color-surface-selected)] rounded focus:ring-[color:var(--color-success)]"
/>
<span className="text-[color:var(--color-text)]">Pontos egyezés</span>
@@ -431,24 +399,14 @@ export default function TaskCardEditor({ card, onChange }) {
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={false}
disabled
checked={card.partialAccepted || true}
onChange={(e) => updateField('partialAccepted', e.target.checked)}
className="w-4 h-4 text-[color:var(--color-success)] border-[color:var(--color-surface-selected)] rounded focus:ring-[color:var(--color-success)]"
/>
<span className="text-[color:var(--color-text)]">Részleges elfogadás</span>
</label>
</div>
{/* Hamarosan elérhető felület */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="bg-[color:var(--color-warning)]/20 backdrop-blur-sm border-2 border-[color:var(--color-warning)] rounded-lg px-4 py-2">
<span className="text-[color:var(--color-warning)] font-semibold text-sm flex items-center gap-2">
🚧 Hamarosan elérhető
</span>
</div>
</div>
</div>
{/* Tipp */}
<div className="mt-4">
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
@@ -465,14 +423,12 @@ export default function TaskCardEditor({ card, onChange }) {
</div>
)}
{/* Közös beállítások - Később elérhető */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6 relative">
{/* Közös beállítások */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
Beállítások
</h3>
<div className="relative">
<div className="opacity-50 pointer-events-none blur-[1px]">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Pontszám */}
<div>
@@ -481,8 +437,8 @@ export default function TaskCardEditor({ card, onChange }) {
</label>
<input
type="number"
value={10}
disabled
value={card.points || 10}
onChange={(e) => updateField('points', parseInt(e.target.value) || 0)}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200"
min="0"
max="1000"
@@ -495,10 +451,15 @@ export default function TaskCardEditor({ card, onChange }) {
Időlimit
</label>
<select
disabled
value={card.timeLimit || 30}
onChange={(e) => updateField('timeLimit', parseInt(e.target.value))}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200"
>
<option>30 másodperc</option>
{timeLimits.map(time => (
<option key={time.value} value={time.value}>
{time.label}
</option>
))}
</select>
</div>
@@ -510,8 +471,8 @@ export default function TaskCardEditor({ card, onChange }) {
</label>
<input
type="number"
value={100}
disabled
value={card.characterLimit || 100}
onChange={(e) => updateField('characterLimit', parseInt(e.target.value) || 0)}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200"
min="10"
max="500"
@@ -526,118 +487,14 @@ export default function TaskCardEditor({ card, onChange }) {
💡 Magyarázat (opcionális)
</label>
<textarea
disabled
value={card.explanation || ''}
onChange={(e) => updateField('explanation', e.target.value)}
className="w-full px-4 py-3 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200 resize-none"
rows="3"
placeholder="Magyarázat a helyes válaszhoz..."
/>
</div>
</div>
{/* Hamarosan elérhető felület */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="bg-[color:var(--color-warning)]/20 backdrop-blur-sm border-2 border-[color:var(--color-warning)] rounded-lg px-4 py-2">
<span className="text-[color:var(--color-warning)] font-semibold text-sm flex items-center gap-2">
🚧 Hamarosan elérhető
</span>
</div>
</div>
</div>
</div>
{/* Következmények (helyes válasz esetén) */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
🎯 Következmények (helyes válasz esetén)
</h3>
<div className="space-y-4">
{/* Consequence Type */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Hatás típusa
</label>
<select
value={card.consequence?.type ?? 0}
onChange={(e) => updateConsequence('type', parseInt(e.target.value))}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200"
>
{consequenceTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
<div className="text-xs text-[color:var(--color-text-muted)] mt-1">
{consequenceTypes.find(t => t.value === (card.consequence?.type ?? 0))?.description}
</div>
</div>
{/* Consequence Value - csak ha előre/hátra lépés */}
{(card.consequence?.type === 0 || card.consequence?.type === 1) && (
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Mezők száma
</label>
<input
type="number"
value={card.consequence?.value ?? 1}
onChange={(e) => updateConsequence('value', parseInt(e.target.value) || 1)}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200"
min="1"
max="10"
/>
</div>
)}
</div>
</div>
{/* Következmények (rossz válasz esetén) */}
<div className="bg-[color:var(--color-surface)] rounded-xl p-6">
<h3 className="text-lg font-semibold text-[color:var(--color-text)] mb-4">
Következmények (rossz válasz esetén)
</h3>
<div className="space-y-4">
{/* Wrong Consequence Type */}
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Hatás típusa
</label>
<select
value={card.wrongConsequence?.type ?? 1}
onChange={(e) => updateWrongConsequence('type', parseInt(e.target.value))}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-danger)] focus:border-transparent outline-none transition-all duration-200"
>
{consequenceTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
<div className="text-xs text-[color:var(--color-text-muted)] mt-1">
{consequenceTypes.find(t => t.value === (card.wrongConsequence?.type ?? 1))?.description}
</div>
</div>
{/* Wrong Consequence Value - csak ha előre/hátra lépés */}
{(card.wrongConsequence?.type === 0 || card.wrongConsequence?.type === 1) && (
<div>
<label className="block text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
Mezők száma
</label>
<input
type="number"
value={card.wrongConsequence?.value ?? 1}
onChange={(e) => updateWrongConsequence('value', parseInt(e.target.value) || 1)}
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-danger)] focus:border-transparent outline-none transition-all duration-200"
min="1"
max="10"
/>
</div>
)}
</div>
</div>
</div>
)
}
@@ -1,77 +1,62 @@
import React, { useEffect, useRef, useState } from "react"
import React from "react"
import { Link } from "react-router-dom"
import Logo from "../../assets/pictures/Logo"
const ArrowUpIcon = () => <span style={{ fontSize: "1.25rem" }}></span>
const Footer = () => {
const [isVisible, setIsVisible] = useState(false)
const footerRef = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
setIsVisible(entry.isIntersecting)
},
{ threshold: 0.3 }
)
if (footerRef.current) {
observer.observe(footerRef.current)
}
return () => {
if (footerRef.current) {
observer.unobserve(footerRef.current)
}
}
}, [])
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" })
}
return (
<footer
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-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">
<a href="/" className="hover:scale-105 hover:brightness-110">
<div className="flex flex-col items-center footer-animate">
<a href="/" className="transition-transform duration-500 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">
<span className="text-lg font-semibold text-green-600 underline underline-offset-4 mb-2 drop-shadow-sm">
<div className="flex flex-col gap-1 footer-animate">
<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-500">
<a href="/" className="hover:underline hover:text-green-400 transition">
Főoldal
</a>
<a href="/about" className="hover:underline hover:text-green-500">
<a href="/about" className="hover:underline hover:text-green-400 transition">
Rólunk
</a>
<a href="/contacts" className="hover:underline hover:text-green-500">
<a href="/contact" className="hover:underline hover:text-green-400 transition">
Kapcsolat
</a>
</div>
{/* Közösség */}
<div className="flex flex-col gap-1">
<span className="text-lg font-semibold text-green-600 underline underline-offset-4 mb-2 drop-shadow-sm">
<div className="flex flex-col gap-1 footer-animate">
<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-500"
className="hover:underline hover:text-green-400 transition"
>
Discord
</a>
@@ -79,36 +64,34 @@ const Footer = () => {
href="https://github.com/"
target="_blank"
rel="noopener noreferrer"
className="hover:underline hover:text-green-500"
className="hover:underline hover:text-green-400 transition"
>
GitHub
</a>
</div>
{/* Elérhetőség */}
<div className="flex flex-col gap-1">
<span className="text-lg font-semibold text-green-600 underline underline-offset-4 mb-2 drop-shadow-sm">
<div className="flex flex-col gap-1 footer-animate">
<span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm">
Elérhetőség
</span>
<span className="opacity-85">Email: info@serpentrace.hu</span>
<span className="opacity-85">Telefon: +36 30 123 4567</span>
<span className="opacity-80">Email: info@serpentrace.hu</span>
<span className="opacity-80">Telefon: +36 30 123 4567</span>
</div>
</div>
<div className="text-center mt-8 text-sm opacity-70">
<div className="text-center mt-8 text-sm opacity-70 footer-animate">
© {new Date().getFullYear()} SerpentRace. Minden jog fenntartva.
</div>
{/* Scroll to top */}
{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 hover:scale-110"
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"
aria-label="Ugrás az oldal tetejére"
>
<ArrowUpIcon />
</button>
)}
</footer>
)
}
@@ -7,7 +7,7 @@ export default function InputBox({ type, placeholder, value, onChange, width })
return (
<input
type={type}
className={`${widthClass} py-3 px-4 border border-battleship-gray rounded-lg focus:border-mint-lighter focus:outline-none text-text placeholder-text-muted bg-background font-semibold text-lg`}
className={`${widthClass} py-3 px-4 border border-battleship-gray rounded-lg focus:border-mint focus:outline-none text-text placeholder-text-muted bg-background font-semibold text-lg`}
placeholder={placeholder}
value={value}
onChange={onChange}
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"
import React, { useState } from "react"
import { useNavigate } from "react-router-dom"
import {
FaPlus,
@@ -9,8 +9,6 @@ import {
FaSortAlphaDown,
FaSortAlphaUp,
FaQuestionCircle,
FaChevronLeft,
FaChevronRight,
} from "react-icons/fa"
import SearchBox from "../Search/SearchBox"
import PopUp from "../PopUp/PopUp"
@@ -19,10 +17,22 @@ import DeckInfoPopUp from "../PopUp/DeckInfoPopUp"
const deckTypes = [
{ label: "Luck", color: "var(--color-luck)" },
{ label: "Question", color: "var(--color-question)" },
{ label: "Joker", color: "var(--color-fun)" },
{ label: "Fun", color: "var(--color-fun)" },
]
// initial state will be fetched from backend
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" },
]
const origins = ["Mind", "Vállalati", "Saját"]
@@ -72,47 +82,9 @@ const DeckManager = () => {
const [search, setSearch] = useState("")
const [showSortHelp, setShowSortHelp] = useState(false)
const [selectedDeck, setSelectedDeck] = useState(null)
const [allDecks, setAllDecks] = useState([]) // Összes pakli
const [loading, setLoading] = useState(false)
const [itemsPerPage, setItemsPerPage] = useState(20)
const [currentPage, setCurrentPage] = useState(1)
// Load all decks once
useEffect(() => {
let mounted = true
const load = async () => {
setLoading(true)
try {
// Load all decks (0-99 is the max limit = 100 decks)
const result = await import('../../api/deckApi').then(m => m.getDecksPage(0, 99))
if (!mounted) return
console.log('Loaded decks:', result) // Debug
// 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
}))
console.log('Mapped decks:', mapped) // Debug
setAllDecks(mapped)
} catch (err) {
console.error('Failed to load decks', err)
} finally {
setLoading(false)
}
}
load()
return () => { mounted = false }
}, [])
// Filter logic
let filteredDecks = allDecks.filter((deck) => {
// Filter logic (mock)
let filteredDecks = mockDecks.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())
@@ -133,23 +105,11 @@ const DeckManager = () => {
return 0
})
// Pagination logic - frontend only
const totalDecks = filteredDecks.length
const totalPages = Math.ceil(totalDecks / itemsPerPage)
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const paginatedDecks = filteredDecks.slice(startIndex, endIndex)
// Reset to page 1 when filters or items per page change
useEffect(() => {
setCurrentPage(1)
}, [selectedType, selectedOrigin, search, sortBy, itemsPerPage])
return (
<div className="w-full flex flex-col bg-[color:var(--color-background)]">
<div className="w-full max-w-[1200px] mx-auto px-4 py-10">
<div className="w-full max-w-6xl mx-auto px-4 py-10">
{/* Filters */}
<div className="flex flex-col md:flex-row gap-3 justify-between items-center mb-10 bg-[color:var(--color-surface)]/80 backdrop-blur-lg rounded-2xl px-6 py-4 shadow-lg">
<div className="flex flex-col md:flex-row gap-4 justify-between items-center mb-10 bg-[color:var(--color-surface)]/80 backdrop-blur-lg rounded-2xl px-6 py-4 shadow-lg">
<div className="flex gap-2 items-center w-full md:w-auto">
<SearchBox
value={search}
@@ -185,8 +145,8 @@ const DeckManager = () => {
? "Szerencse"
: type.label === "Question"
? "Kérdés"
: type.label === "Joker"
? "Joker"
: type.label === "Fun"
? "Szórakozás"
: type.label}
</button>
))}
@@ -285,54 +245,18 @@ const DeckManager = () => {
)}
</div>
</div>
{/* Items per page selector and pagination info */}
<div className="flex flex-col md:flex-row gap-4 justify-between items-center mb-6 bg-[color:var(--color-surface)]/60 backdrop-blur-lg rounded-xl px-6 py-3 shadow">
<div className="flex items-center gap-3">
<span className="text-[color:var(--color-text-muted)] text-sm font-medium">
Elemek oldalanként:
</span>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="px-3 py-1.5 rounded-lg bg-[color:var(--color-background)] text-[color:var(--color-text)] border border-[color:var(--color-surface-selected)] focus:ring-2 focus:ring-[color:var(--color-success)] outline-none transition-all duration-200"
>
<option value={20}>20</option>
<option value={30}>30</option>
<option value={40}>40</option>
<option value={50}>50</option>
</select>
</div>
<div className="text-[color:var(--color-text-muted)] text-sm">
{totalDecks > 0 ? (
<>
{startIndex + 1}-{Math.min(endIndex, totalDecks)} / {totalDecks} pakli
</>
) : (
<>0 pakli</>
)}
</div>
</div>
{/* Decks Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-8 mt-8">
{/* Create New Deck (Mockup) */}
<div
onClick={() => navigate("/deck-creator")}
onClick={() => navigate('/deck-creator')}
className="flex flex-col items-center justify-center h-48 bg-[color:var(--color-card)] border-2 border-dashed border-[color:var(--color-success)] rounded-2xl cursor-pointer hover:bg-[color:var(--color-success)]/20 transition-all duration-200 shadow-lg"
>
<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 (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 && paginatedDecks.map((deck) => {
{/* Existing Decks (Mockup) */}
{filteredDecks.map((deck) => {
const deckType = deckTypes.find((t) => t.label === deck.type)
const borderColor = deckType ? deckType.color : "var(--color-success)"
return (
@@ -355,7 +279,7 @@ const DeckManager = () => {
: deck.type === "Question"
? "Kérdés"
: deck.type === "Fun"
? "Joker"
? "Szórakozás"
: deck.type}
</span>
<h2 className="text-xl font-bold text-[color:var(--color-text)] mb-1 truncate">
@@ -369,77 +293,15 @@ const DeckManager = () => {
)
})}
</div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="flex justify-center items-center gap-2 mt-8">
<button
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className={`px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 ${
currentPage === 1
? 'bg-[color:var(--color-surface)] text-[color:var(--color-text-muted)] cursor-not-allowed'
: 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] hover:bg-[color:var(--color-success)]/80 hover:scale-105'
}`}
>
<FaChevronLeft />
Előző
</button>
<div className="flex items-center gap-2">
{[...Array(totalPages)].map((_, index) => {
const pageNum = index + 1
// Show first page, last page, current page and neighbors
if (
pageNum === 1 ||
pageNum === totalPages ||
(pageNum >= currentPage - 1 && pageNum <= currentPage + 1)
) {
return (
<button
key={pageNum}
onClick={() => setCurrentPage(pageNum)}
className={`w-10 h-10 rounded-lg font-medium transition-all duration-200 ${
currentPage === pageNum
? 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] scale-110 shadow-lg'
: 'bg-[color:var(--color-surface)] text-[color:var(--color-text)] hover:bg-[color:var(--color-surface-selected)]'
}`}
>
{pageNum}
</button>
)
} else if (
pageNum === currentPage - 2 ||
pageNum === currentPage + 2
) {
return (
<span key={pageNum} className="text-[color:var(--color-text-muted)]">
...
</span>
)
}
return null
})}
</div>
<button
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className={`px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2 ${
currentPage === totalPages
? 'bg-[color:var(--color-surface)] text-[color:var(--color-text-muted)] cursor-not-allowed'
: 'bg-[color:var(--color-success)] text-[color:var(--color-text-inverse)] hover:bg-[color:var(--color-success)]/80 hover:scale-105'
}`}
>
Következő
<FaChevronRight />
</button>
</div>
)}
</div>
{/* Deck Info Popup */}
{selectedDeck && <DeckInfoPopUp deck={selectedDeck} onClose={() => setSelectedDeck(null)} />}
{selectedDeck && (
<DeckInfoPopUp
deck={selectedDeck}
onClose={() => setSelectedDeck(null)}
/>
)}
</div>
)
}
@@ -5,13 +5,14 @@ import logoImg from "../../assets/pictures/Logo.png"
import ButtonGreen from "../Buttons/ButtonGreen.jsx"
import { FaUsers, FaPaintBrush, FaHeadset } from "react-icons/fa"
import { motion } from "framer-motion"
import { isAuthenticated } from "../../hooks/useRequireAuth" // <-- added import
import { useNavigate } from "react-router-dom" // <-- NEW
const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onNavigateToContacts }) => {
const auth = isAuthenticated() // <-- check without redirect
const navigate = useNavigate() // <-- NEW
import { useNavigate } from "react-router-dom"
const LandingPage = ({ onNavigateToPlay }) => {
const navigate = useNavigate()
const handleNavigateToAuth = () => {
navigate("/register")
}
return (
<div className="w-full">
{/* Hero Section */}
@@ -60,16 +61,8 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 1 }}
>
{/* If not authenticated show Login/Register; if authenticated show Home button */}
{!auth ? (
<>
<ButtonGreen text="Bejelentkezés" onClick={onNavigateToPlay} width="w-60" />
<ButtonGreen text="Regisztráció" onClick={onNavigateToAuth} width="w-60" />
<ButtonGreen text="Játék" onClick={onNavigateToGame} width="w-60" />
</>
) : (
<ButtonGreen text="Játék" onClick={onNavigateToGame} width="w-60" />
)}
<ButtonGreen text="Játék" onClick={onNavigateToPlay} width="w-60" />
<ButtonGreen text="Regisztráció" onClick={handleNavigateToAuth} width="w-60" />
</motion.div>
</div>
</motion.section>
@@ -178,7 +171,7 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth, onNavigateToGame, onN
<ButtonGreen
text="Kapcsolatfelvétel"
onClick={onNavigateToContacts}
onClick={handleNavigateToAuth}
className="px-12 py-4 text-xl font-bold"
/>
</motion.div>
@@ -4,14 +4,9 @@ import logoImg from "../../assets/pictures/Logo.png" // <-- EZT ADD HOZZÁ
import ButtonDark from "../Buttons/ButtonDark.jsx"
import InputBoxDark from "../Inputs/InputBoxDark.jsx"
const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
const PlayMenu = ({ onJoinGame, onCreateGame, user }) => {
const [joinCode, setJoinCode] = useState("")
const [error, setError] = useState("")
const [guestName, setGuestName] = useState("")
const [guestError, setGuestError] = useState("")
// gyors username kiolvasás (ha a parent objektum user={ { name: ... } } küldi)
const username = user?.name ?? null
const handleJoin = () => {
if (!joinCode.trim()) {
@@ -26,19 +21,9 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
onCreateGame()
}
// egyszerű segéd a kezdobetűk kinyerésére
const initials = username
? username
.split(" ")
.map((s) => s[0])
.join("")
.slice(0, 2)
.toUpperCase()
: ""
return (
<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-16 flex flex-col md:flex-row items-center justify-center rounded-3xl shadow-2xl min-h-[60vh] overflow-hidden"
style={{
background: "linear-gradient(90deg, var(--color-surface) 30%, var(--color-mint) 100%)",
}}
@@ -47,10 +32,10 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
<div className="flex-1 flex items-center justify-center w-full h-full py-10 md:py-0 md:pl-10">
<LogoCard
imageSrc={logoImg}
containerHeight="420px"
containerWidth="420px"
imageHeight="420px"
imageWidth="420px"
containerHeight="450px"
containerWidth="450px"
imageHeight="450px"
imageWidth="450px"
rotateAmplitude={7}
scaleOnHover={1.03}
showMobileWarning={false}
@@ -58,49 +43,12 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
displayOverlayContent={false}
/>
</div>
{/* Jobb oldali panel */}
<div className="flex-1 w-full flex items-center justify-center px-6 md:px-12 py-8">
<div
className="w-full max-w-md rounded-2xl p-6 md:p-8 flex flex-col gap-6"
style={{ background: "rgba(0,0,0,0.15)", backdropFilter: "blur(6px)" }}
>
<div className="flex items-center justify-between">
{username ? (
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold"
style={{ background: "rgba(34,197,94,0.12)", color: "var(--color-mint)" }}
>
{initials}
</div>
<div className="text-[32px]" style={{ color: "var(--color-muted, #cbd5e1)" }}>
<span className="font-medium" style={{ color: "var(--color-text, #fff)" }}>
{username}
</span>
</div>
</div>
) : (
<div className="w-full">
<div className="font-semibold mb-3 text-text">Nincs bejelentkezve játssz vendégként:</div>
<InputBoxDark
type="text"
placeholder="Nickname..."
value={guestName}
onChange={(e) => {
setGuestName(e.target.value)
setGuestError("")
}}
width="w-full"
/>
{guestError && <div className="text-xs mt-2 text-error">{guestError}</div>}
</div>
)}
</div>
<div className="flex-1 w-full flex flex-col items-center justify-center px-4 md:px-12 py-10">
<div className="w-full max-w-md rounded-2xl p-8 flex flex-col gap-8">
<div>
<h2 className="font-semibold mb-3 text-text">Csatlakozás játékhoz</h2>
<div className={`${error ? "border border-error rounded-lg p-2" : ""}`}>
<h2 className="text-lg font-semibold mb-2 text-text">Csatlakozás játékhoz</h2>
<div className={`${error ? "border border-error rounded-lg" : ""}`}>
<InputBoxDark
type="text"
placeholder="Játék kódja"
@@ -109,22 +57,18 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
width="w-full"
/>
</div>
{error && <div className="text-xs mt-2 text-error">{error}</div>}
{error && <div className="text-xs mt-1 text-error">{error}</div>}
<div className="mt-4">
<ButtonDark text="Csatlakozás" type="button" onClick={handleJoin} width="w-full" />
</div>
</div>
{username ? (
<div className="border-t border-white/10 pt-4">
{username && (
{user && (
<div>
<h3 className="font-semibold mb-3 text-text">Új játék létrehozása</h3>
<h2 className="text-lg font-semibold mb-2 text-text">Új játék létrehozása</h2>
<ButtonDark text="Játék létrehozása" type="button" onClick={handleCreate} width="w-full" />
</div>
)}
</div>
) : null}
</div>
</div>
</section>
)
@@ -1,98 +1,42 @@
import React, { useState } from "react"
import Logo from "../../assets/pictures/Logo"
import { Link, useNavigate } from "react-router-dom"
import About from "../../pages/About/About"
import Home from "../../pages/Landing/Home"
const navLinkClass = "px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10"
const navLinkClassPlay =
"px-4 py-2 rounded-lg text-white bg-white/12 hover:bg-white/20 transition-all duration-200"
const Navbar = () => {
const [menuOpen, setMenuOpen] = useState(false)
const navigate = useNavigate()
const isLoggedIn = Boolean(localStorage.getItem("authLevel") && localStorage.getItem("username"))
const handleLogout = () => {
localStorage.removeItem("authLevel")
localStorage.removeItem("username")
navigate("/")
}
return (
<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">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16 items-center">
{/* Logo + Brand */}
{/* Logo */}
<div className="flex-shrink-0 flex items-center gap-2">
<Link to="/" className="flex items-center mt-1 h-9">
<a href="/" className="flex items-center mt-1 h-9">
<Logo size={36} />
</Link>
<Link to="/" className="flex items-center h-9 text-white font-bold text-2xl tracking-tight">
</a>
<a href="/" className="flex items-center h-9 text-white font-bold text-2xl tracking-tight">
SerpentRace
</Link>
</a>
</div>
{/* Desktop Menu */}
<div className="hidden md:flex items-center space-x-6">
{/* Bal oldali linkek */}
<Link to="/" className={navLinkClass}>Főoldal</Link>
<Link to="/about" className={navLinkClass}>Rólunk</Link>
<Link to="/contacts" className={navLinkClass}>Kapcsolat</Link>
{/* Csak bejelentkezve */}
{isLoggedIn && (
<>
<Link to="/decks" className={navLinkClass}>Paklik</Link>
<Link to="/report" className={navLinkClass}>Statisztikák</Link>
</>
)}
{/* Játék gomb */}
<Link to="/home" className={navLinkClassPlay}>Játék</Link>
{/* Jobb oldali akciók */}
{!isLoggedIn ? (
<div className="flex items-center space-x-4">
<Link
to="/login"
className="px-4 py-2 rounded-lg hover:bg-white/20 text-white font-semibold transition-all"
>
Bejelentkezés
</Link>
{/* Elválasztó vonal */}
<div className="w-px h-10 bg-white/100"></div>
<Link
to="/register"
className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white font-semibold transition-all"
>
Regisztráció
</Link>
<div className="hidden md:flex space-x-8">
<a href="/home" className={navLinkClass}>
Home
</a>
<a href="/report" className={navLinkClass}>
Stats
</a>
<a href="/about" className={navLinkClass}>
About
</a>
<a href="/companies" className={navLinkClass}>
Contact
</a>
</div>
) : (
<button
onClick={handleLogout}
className="ml-2 p-2 rounded-full bg-[#166534] hover:bg-[#1f7a45] text-white shadow-lg hover:shadow-green-400/40 transition-all transform hover:scale-105 cursor-pointer"
title="Kijelentkezés"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 16l4-4m0 0l-4-4m4 4H3m13 4v1a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2v1"
/>
</svg>
</button>
)}
</div>
{/* Mobile Hamburger */}
<div className="md:hidden flex items-center">
<button
@@ -108,7 +52,12 @@ const Navbar = () => {
xmlns="http://www.w3.org/2000/svg"
>
{menuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8h16M4 16h16" />
)}
@@ -117,69 +66,21 @@ const Navbar = () => {
</div>
</div>
</div>
{/* Mobile Menu */}
{menuOpen && (
<div className="md:hidden bg-emerald-600 px-2 pt-2 pb-3 space-y-1">
<Link to="/" onClick={() => setMenuOpen(false)} className={navLinkClass}>Főoldal</Link>
<Link to="/about" onClick={() => setMenuOpen(false)} className={navLinkClass}>Rólunk</Link>
<Link to="/contacts" onClick={() => setMenuOpen(false)} className={navLinkClass}>Kapcsolat</Link>
{isLoggedIn && (
<>
<Link to="/decks" onClick={() => setMenuOpen(false)} className={navLinkClass}>Paklik</Link>
<Link to="/report" onClick={() => setMenuOpen(false)} className={navLinkClass}>Statisztikák</Link>
</>
)}
<Link to="/home" onClick={() => setMenuOpen(false)} className={navLinkClassPlay}>Játék</Link>
{!isLoggedIn ? (
<div className="flex flex-col space-y-2">
<Link
to="/login"
onClick={() => setMenuOpen(false)}
className="block px-3 py-2 rounded-lg hover:bg-white/20 text-white font-semibold transition-all"
>
Bejelentkezés
</Link>
{/* Elválasztó vonal mobilon */}
<div className="w-full h-px bg-white/30"></div>
<Link
to="/register"
onClick={() => setMenuOpen(false)}
className="block px-3 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white font-semibold transition-all"
>
Regisztráció
</Link>
</div>
) : (
<div className="flex justify-end px-2 pb-2">
<button
onClick={() => {
handleLogout()
setMenuOpen(false)
}}
className="p-2 rounded-full bg-[#166534] hover:bg-[#1f7a45] text-white shadow-lg hover:shadow-green-400/40 transition-all transform hover:scale-105 cursor-pointer"
title="Kijelentkezés"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 16l4-4m0 0l-4-4m4 4H3m13 4v1a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2v1"
/>
</svg>
</button>
</div>
)}
<a href="#" className={navLinkClass}>
Home
</a>
<a href="#" className={navLinkClass}>
Leaderboard
</a>
<a href="#" className={navLinkClass}>
About
</a>
<a href="#" className={navLinkClass}>
Contact
</a>
</div>
)}
</nav>
@@ -14,10 +14,6 @@ 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
@@ -32,30 +28,18 @@ export default function DeckInfoPopUp({ deck, onClose }) {
const deckTypes = {
"Luck": { label: "Szerencse", color: "var(--color-luck)" },
"Question": { label: "Kérdés", color: "var(--color-question)" },
"Fun": { label: "Joker", color: "var(--color-fun)" }
"Fun": { label: "Szórakozás", color: "var(--color-fun)" }
}
const currentDeckType = deckTypes[deck.type] || { label: deck.type, color: "var(--color-success)" }
// 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 })
// Mock data - ezeket majd a backend adatokra cseréljük
const mockData = {
...deck,
creator,
privacy,
questionsCount,
answersCount
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
}
const formatDate = (dateString) => {
@@ -1,165 +0,0 @@
import { toast, ToastContainer, Bounce } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
/**
* 🔧 Ezt csak egyszer kell betenni az App.jsx-be!
* <ToastConfig /> = az a komponens, ami kirendereli a toastokat
*/
export const ToastConfig = () => (
<ToastContainer
position="bottom-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick={false}
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="light"
transition={Bounce}
/>
);
/**
* 🦄 Alapértelmezett toast üzenet (semleges)
* notify("Üzenet szövege")
*/
export const notify = (message) => {
toast(message, {
position: "bottom-right",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: false,
pauseOnHover: true,
draggable: true,
theme: "light",
transition: Bounce,
});
};
/**
* ✅ Sikeres művelethez
* notifySuccess("Sikeres mentés!")
*/
export const notifySuccess = (message) => {
toast.success(message, {
position: "bottom-right",
autoClose: 4000,
theme: "light",
transition: Bounce,
});
};
/**
* ❌ Hibás művelethez
* notifyError("Hiba történt a mentés során!")
*/
export const notifyError = (message) => {
toast.error(message, {
position: "bottom-right",
autoClose: 5000,
theme: "light",
transition: Bounce,
});
};
/**
* ️ Információs üzenethez
* notifyInfo("Friss adatok betöltve!")
*/
export const notifyInfo = (message) => {
toast.info(message, {
position: "bottom-right",
autoClose: 4000,
theme: "light",
transition: Bounce,
});
};
/**
* ⚠️ Figyelmeztetéshez
* notifyWarning("Figyelem! Nem mentett módosítások vannak!")
*/
export const notifyWarning = (message) => {
toast.warn(message, {
position: "bottom-right",
autoClose: 4000,
theme: "light",
transition: Bounce,
});
};
// import React, { useState } from "react";
// import { notifyWarning } from "../../components/Toastify/toastifyServices";
// function AuthLogin() {
// const [email, setEmail] = useState("");
// const [password, setPassword] = useState("");
// const handleLogin = async (e) => {
// e.preventDefault();
// // Példa jelszó ellenőrzés logikára:
// if (password !== "titkosjelszo123") {
// notifyWarning("⚠️ Hibás jelszó! Kérlek próbáld újra.");
// return;
// }
// // Ha jó a jelszó:
// notifySuccess("✅ Sikeres bejelentkezés!");
// };
// return (
// <form onSubmit={handleLogin} className="login-form">
// <input
// type="email"
// placeholder="Email cím"
// value={email}
// onChange={(e) => setEmail(e.target.value)}
// />
// <input
// type="password"
// placeholder="Jelszó"
// value={password}
// onChange={(e) => setPassword(e.target.value)}
// />
// <button type="submit">Bejelentkezés</button>
// </form>
// );
// }
// export default AuthLogin;
// meghivas
// import { toast } from "react-toastify";
// // 🔔 Alap toast
// export const notify = (msg) => toast(msg);
// // ✅ Sikeres üzenet
// export const notifySuccess = (msg) => toast.success(msg);
// // ⚠️ Figyelmeztetés
// export const notifyWarning = (msg) => toast.warning(msg);
// // ❌ Hiba
// export const notifyError = (msg) => toast.error(msg);
// // ️ Információ
// export const notifyInfo = (msg) => toast.info(msg);
// hasznalat
// import { notifyWarning } from "../../components/Toastify/toastifyServices";
// if (password !== "titkos") {
// notifyWarning("⚠️ Hibás jelszó!");
// }
@@ -1,52 +0,0 @@
import { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom"
export function requireAuthSync({ key = "username", redirectTo = "/login", replace = true } = {}) {
const value = localStorage.getItem(key)
if (!value) {
if (replace) window.location.replace(redirectTo)
else window.location.assign(redirectTo)
return false
}
return true
}
// New: non-redirecting check for auth status
export function isAuthenticated(key = "username") {
try {
return !!localStorage.getItem(key)
} catch {
return false
}
}
// Default hook: ad vissza egy [value, setValue] párt, szinkronizálja localStorage-t és opcionálisan átirányít, ha nincs érték
export default function useRequireAuth({ key = "username", redirectTo = "/login", redirect = true } = {}) {
const navigate = useNavigate()
const [value, setValue] = useState(() => {
try {
return localStorage.getItem(key)
} catch {
return null
}
})
// Ha nincs érték és redirect engedélyezve van, átirányítjuk (komponens mount-oláskor)
useEffect(() => {
if (!value && redirect) {
navigate(redirectTo)
}
}, [navigate, value, redirectTo, redirect])
// Szinkronizáljuk a localStorage-t amikor a state változik
useEffect(() => {
try {
if (value == null) localStorage.removeItem(key)
else localStorage.setItem(key, value)
} catch {
// fail silently
}
}, [key, value])
return [value, setValue]
}
+1 -5
View File
@@ -8,13 +8,9 @@
--color-eerie-black: #181d23;
--color-mint: #15803d;
--color-mint-dark: #136636;
--color-mint-darker: #11522b;
--color-mint-light: #16a34a;
--color-mint-lighter: #22c55e;
/* Gombok */
--color-button-primary: #16a34a;
--color-button-primary-hover: #15803d;
@@ -31,7 +27,7 @@
/* Deck típus színek */
--color-luck: #5fa985; /* zöld, mint a success */
--color-question: #4f7be6; /* új kék, illik az oldalhoz */
--color-fun: #FFD700; /* citromsárga a joker kártyákhoz */
--color-fun: #e15b64; /* piros, mint az error */
/* Háttérszínek */
--color-background: #181d23;
@@ -1,20 +1,19 @@
// 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, useEffect } from "react"
import { useLocation, useNavigate } from "react-router-dom"
import { useLocation } from "react-router-dom"
import { login } from "../../api/userApi"
import { FaArrowLeft } from "react-icons/fa"
export default function LoginForm() {
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) {
@@ -30,40 +29,22 @@ export default function LoginForm() {
const handleSubmit = (e) => {
e.preventDefault()
setError("")
setShowErrorPopup(false)
if (!email || !password) {
setError("Minden mező kitöltése kötelező.")
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
return
}
if (!validateEmail(email)) {
setError("Hibás email formátum.")
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
return
}
// Backend API
login(email, password)
.then((response) => {
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)
}
.then((data) => {
console.log(data)
console.log("Bejelentkezés:", { email, password })
})
.catch(() => {
.catch((error) => {
setError("Hibás bejelentkezési adatok.")
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
})
}
@@ -74,35 +55,14 @@ export default function LoginForm() {
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
className="relative flex flex-col items-center"
>
{/* 🔙 Vissza nyíl gomb — most pontosan a fehér box bal felső sarkában */}
<div
className="absolute -top-6 -left-6 flex items-center group cursor-pointer select-none"
onClick={() => navigate("/")}
>
<FaArrowLeft className="text-gray-700 text-xl transition-transform duration-300 group-hover:-translate-x-1" />
<span className="ml-2 text-gray-700 font-medium opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300">
Vissza a főoldalra
</span>
</div>
<h2 className="text-4xl font-extrabold text-center mb-6 text-gray-800 tracking-wide">
Bejelentkezés
</h2>
<h2 className="text-4xl font-extrabold text-center mb-6 text-gray-800 tracking-wide">Bejelentkezés</h2>
{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>
)}
{error && <div className="mb-4 text-red-600 text-center font-semibold">{error}</div>}
<form onSubmit={handleSubmit} className="space-y-6">
<InputBox
type="email"
@@ -1,12 +1,12 @@
// 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 { register } from "../../api/userApi"
import { useNavigate, useLocation } from "react-router-dom"
import { ToastConfig } from "../../components/Toastify/toastifyServices"
import { FaArrowLeft } from "react-icons/fa"
import { useNavigate } from "react-router-dom"
export default function RegisterForm() {
const [lastname, setLastname] = useState("")
@@ -19,7 +19,6 @@ export default function RegisterForm() {
const [error, setError] = useState("")
const [showErrorPopup, setShowErrorPopup] = useState(false)
const navigate = useNavigate()
const location = useLocation()
function validateEmail(email) {
return /\S+@\S+\.\S+/.test(email)
@@ -46,26 +45,30 @@ export default function RegisterForm() {
setError("A jelszavak nem egyeznek.")
return
}
// Backend API
try {
const response = await register(username, email, password, firstname, lastname, phone)
// Check for 201 Created status
if (response && response.status === 201) {
ToastConfig("✅ Sikeres regisztráció!")
if (location.pathname === "/login") {
navigate("/login", { state: { success: true } })
window.location.reload()
} else {
navigate("/login", { state: { success: true } })
}
console.log(response)
console.log("Regisztráció:", { username, email, password, firstname, lastname, phone })
} else {
let msg = "Sikertelen regisztráció."
if (response?.data?.error) msg = response.data.error
if (response && response.error) {
msg = response.error
}
setError(msg)
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
}
} catch (err) {
let msg = err?.response?.data?.error || err.message || "Ismeretlen hiba történt."
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)
@@ -79,37 +82,56 @@ export default function RegisterForm() {
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
className="relative flex flex-col items-center"
>
{/* 🔙 Vissza nyíl gomb ugyanott, mint a login oldalon */}
<div
className="absolute -top-2 -left-1 flex items-center group cursor-pointer select-none"
onClick={() => navigate("/")}
>
<FaArrowLeft className="text-gray-700 text-xl transition-transform duration-300 group-hover:-translate-x-1" />
<span className="ml-2 text-gray-700 font-medium opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300">
Vissza a főoldalra
</span>
</div>
<h2 className="text-4xl font-extrabold text-center mb-6 text-gray-800 tracking-wide">
Regisztráció
</h2>
<h2 className="text-4xl font-extrabold text-center mb-6 text-gray-800 tracking-wide">Regisztráció</h2>
{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="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)} />
<InputBox type="email" placeholder="Email cím" value={email} onChange={(e) => setEmail(e.target.value)} />
<InputBox type="phone" placeholder="Telefonszám" value={phone} onChange={(e) => setPhone(e.target.value)} />
<InputBox type="password" placeholder="Jelszó" value={password} onChange={(e) => setPassword(e.target.value)} />
<InputBox type="password" placeholder="Jelszó megerősítése" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} />
<InputBox
type="text"
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)}
/>
<InputBox
type="email"
placeholder="Email cím"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<InputBox
type="phone"
placeholder="Telefonszám"
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
<InputBox
type="password"
placeholder="Jelszó"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<InputBox
type="password"
placeholder="Jelszó megerősítése"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
<Button text="Regisztráció" type="submit" />
</form>
</motion.div>
@@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from "react"
import React from "react"
import Navbar from "../../components/Navbar/Navbar.jsx"
import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx"
import Background from "../../assets/backgrounds/Background"
import {
FaBuilding,
FaEnvelope,
@@ -56,20 +56,6 @@ const SectionContainer = ({ id, title, children }) => {
}
const CompanyHub = () => {
const [visible, setVisible] = useState(false)
const sectionRef = useRef(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setVisible(true)
},
{ threshold: 0.3 }
)
if (sectionRef.current) observer.observe(sectionRef.current)
return () => observer.disconnect()
}, [])
return (
<div className="relative min-h-screen text-white">
{/* Background fixed behind everything */}
@@ -81,12 +67,6 @@ const CompanyHub = () => {
<Navbar />
<main className="flex-grow relative px-4 py-8 md:px-12 md:py-16 overflow-y-auto scroll-smooth">
<section
ref={sectionRef}
className={`max-w-5xl mx-auto transition-all duration-1000 ease-out ${
visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
}`}
>
<div className="flex justify-center gap-6 mt-8 flex-wrap">
<Card
icon={<FaBuilding />}
@@ -235,7 +215,6 @@ const CompanyHub = () => {
</ul>
</div>
</section>
</section>
</main>
<Footer />
@@ -7,19 +7,17 @@ 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'
import { notifySuccess, notifyError, notifyWarning } from "../../components/Toastify/toastifyServices"
export default function DeckCreator() {
const { deckId } = useParams()
const { deckId } = useParams() // URL-ből deck ID (új deck esetén undefined)
const navigate = useNavigate()
// Deck alapadatok
const [deck, setDeck] = useState({
id: null,
name: "Új Pakli",
type: "QUESTION",
privacy: "private",
name: "Új Deck",
type: "Question", // Question, Luck, Fun
privacy: "private", // private, public
description: "",
cards: []
})
@@ -27,17 +25,19 @@ export default function DeckCreator() {
// UI állapotok
const [selectedCard, setSelectedCard] = useState(null)
const [isCreatingCard, setIsCreatingCard] = useState(false)
const [newCardType, setNewCardType] = useState(null)
const [newCardType, setNewCardType] = useState(null) // task, joker, luck
// Betöltés API-ból
// Betöltés (később API-ból)
useEffect(() => {
if (deckId) {
// TODO: Betöltés API-ból
loadDeck(deckId)
} else {
// Új deck
setDeck({
id: null,
name: "Új Pakli",
type: "QUESTION",
name: "Új Deck",
type: "Question",
privacy: "private",
description: "",
cards: []
@@ -46,14 +46,36 @@ export default function DeckCreator() {
}, [deckId])
const loadDeck = async (id) => {
// Mock adatok később backendből jön majd
// Mock deck betöltés
const mockDeck = {
id,
name: "Demo Deck",
type: "QUESTION",
id: id,
name: "Quiz Night",
type: "Question",
privacy: "public",
description: "Ez egy teszt deck",
cards: []
description: "Szórakoztató kvíz este",
cards: [
{
id: 1,
type: "task",
subType: "quiz",
question: "Mi Magyarország fővárosa?",
options: ["Budapest", "Debrecen", "Szeged", "Pécs"],
correctAnswer: 0,
points: 10,
timeLimit: 30,
explanation: "Budapest 1873 óta Magyarország fővárosa."
},
{
id: 2,
type: "task",
subType: "truefalse",
statement: "A Duna Magyarország leghosszabb folyója.",
isTrue: false,
points: 5,
timeLimit: 15,
explanation: "A Tisza a leghosszabb folyó Magyarországon."
}
]
}
setDeck(mockDeck)
}
@@ -64,40 +86,20 @@ export default function DeckCreator() {
const handleSaveDeck = async () => {
try {
// Típus konverzió backendhez
const typeMapping = {
'LUCK': 0,
'JOKER': 1,
'QUESTION': 2
}
const payload = {
name: deck.name?.trim() || "Névtelen pakli",
type: typeMapping[deck.type] ?? 2,
ctype: deck.privacy === 'public' ? 'PUBLIC' : 'PRIVATE',
description: deck.description || "",
cards: deck.cards || []
}
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)
notifySuccess('Pakli sikeresen elmentve!')
// TODO: API mentés
console.log("Deck mentése:", deck)
alert("✅ Deck sikeresen mentve!")
} catch (error) {
console.error('Mentési hiba:', error)
notifyError('Hiba történt a mentés során: ' + (error?.response?.data?.error || error.message))
console.error("Mentési hiba:", error)
alert("❌ Hiba történt a mentés során!")
}
}
const handleBack = () => {
if (confirm("Biztosan visszamész? A nem mentett változtatások elvesznek.")) {
navigate("/decks")
}
}
const handleCreateCard = (cardType) => {
setNewCardType(cardType)
@@ -111,72 +113,34 @@ export default function DeckCreator() {
setNewCardType(null)
}
// 💡 Demo verzió: beállítások szekció kihagyva
const handleSaveCard = (cardData) => {
try {
if (cardData.section === "settings") {
console.log("Beállítások szekció kihagyva (demo verzió)")
return
}
const defaultConsequence = { type: 0, value: 1 }
const defaultWrongConsequence = { type: 1, value: 1 }
const updatedCard = {
if (isCreatingCard) {
// Új kártya hozzáadása
const newCard = {
...cardData,
id: isCreatingCard ? Date.now() : cardData.id,
consequence: cardData.consequence || defaultConsequence,
...(cardData.type === 'QUESTION' || cardData.type === 'JOKER' || cardData.type === 'PAIRING'
? { wrongConsequence: cardData.wrongConsequence || defaultWrongConsequence }
: {}
)
id: Date.now(), // Temporary ID
}
let wasInvalidCardDeleted = false
setDeck(prev => {
const invalidCards = prev.cards.filter(card => card.type !== prev.type)
if (isCreatingCard && cardData.type === prev.type && invalidCards.length > 0) {
wasInvalidCardDeleted = true
return {
setDeck(prev => ({
...prev,
cards: [
...prev.cards.filter(card => card.type === prev.type),
updatedCard
]
}
}
return {
...prev,
cards: isCreatingCard
? [...prev.cards, updatedCard]
: prev.cards.map(card => card.id === updatedCard.id ? updatedCard : card)
}
})
setSelectedCard(updatedCard)
cards: [...prev.cards, newCard]
}))
setIsCreatingCard(false)
setNewCardType(null)
if (wasInvalidCardDeleted) {
const invalidCount = deck.cards.filter(card => card.type !== deck.type).length
notifyWarning(`Kártya mentve! ${invalidCount} db nem megfelelő típusú kártya törlésre került.`)
setSelectedCard(newCard)
} else {
notifySuccess('Kártya sikeresen mentve!')
}
} catch (error) {
console.error('Kártya mentési hiba:', error)
notifyError('Hiba történt a kártya mentése során')
// Meglévő kártya frissítése
setDeck(prev => ({
...prev,
cards: prev.cards.map(card =>
card.id === cardData.id ? cardData : card
)
}))
setSelectedCard(cardData)
}
}
// 💬 Felugró ablak törlés előtt
const handleDeleteCard = (cardId) => {
const confirmDelete = window.confirm("Biztosan törölni szeretnéd ezt a kártyát?")
if (!confirmDelete) return
if (confirm("Biztosan törlöd ezt a kártyát?")) {
setDeck(prev => ({
...prev,
cards: prev.cards.filter(card => card.id !== cardId)
@@ -185,8 +149,7 @@ export default function DeckCreator() {
if (selectedCard?.id === cardId) {
setSelectedCard(null)
}
notifySuccess("Kártya törölve a pakliból!")
}
}
return (
@@ -209,7 +172,6 @@ export default function DeckCreator() {
<CardsList
cards={deck.cards}
selectedCard={selectedCard}
deckType={deck.type}
onSelectCard={handleSelectCard}
onCreateCard={handleCreateCard}
onDeleteCard={handleDeleteCard}
@@ -223,7 +185,7 @@ export default function DeckCreator() {
<CardEditor
card={selectedCard}
isCreating={isCreatingCard}
cardType={isCreatingCard ? newCardType : deck.type}
cardType={newCardType}
onSave={handleSaveCard}
onCancel={() => {
setIsCreatingCard(false)
@@ -1,7 +1,7 @@
// src/pages/Decks/DeckManagerPage.jsx
// Deck Management Page (with Navbar, no Footer)
import DeckManager from "../../components/DeckCreator/DeckManager.jsx"
import DeckManager from "../../components/Landingpage/DeckManager.jsx"
import Navbar from "../../components/Navbar/Navbar.jsx"
export default function DeckManagerPage() {
@@ -66,17 +66,12 @@ const GameScreen = () => {
{ id: 3, name: "Fürtös", position: 68, score: 14, color: "bg-yellow-600", emoji: "😂" },
])
// New: selected dice value from dropdown (null = none)
const [selectedDice, setSelectedDice] = useState(null)
// Sort players by position in descending order
const sortedPlayers = [...players].sort((a, b) => b.position - a.position)
// Handle dice roll completion
// Handle dice roll
const handleDiceRoll = (value) => {
console.log("Rolled:", value)
// reset dropdown selection after roll
setSelectedDice(null)
// You can add logic here to move the current player based on the dice value
}
@@ -123,6 +118,9 @@ const GameScreen = () => {
{/* Háttér */}
<div className="absolute w-full h-full opacity-10 pointer-events-none overflow-hidden">
{[...Array(35)].map((_, i) => (
// Sajat pulse effect! => node_modules/tailwindcss/index.css:
// --animate-pulse8: pulse 6s cubic-bezier(0.4, 0.2, 0.6, 1) infinite;
<div
key={i}
className="absolute rounded-full bg-teal-600 animate-pulse8"
@@ -189,7 +187,7 @@ const GameScreen = () => {
{sortedPlayers.map((player, index) => (
<div
key={player.id}
className="flex items-center mb-3 p-2 bg-gray-900 rounded-lg hover:bg-gray-700 transition-colors"
className="flex items-center mb-3 p-2 bg-gray-900 rounded-lg hover:bg-gray-800 transition-colors"
>
<div
className={`w-8 h-8 ${player.color} rounded-full mr-3 flex items-center justify-center text-white text-sm font-bold shadow-md`}
@@ -224,31 +222,8 @@ const GameScreen = () => {
{/* Dice Container */}
<div className="bg-gray-800 rounded-xl p-4 shadow-lg border border-teal-700 text-center">
<h2 className="text-xl font-semibold mb-3 text-teal-300">Dobókocka</h2>
<p className="text-gray-300 text-sm mb-4">
Kattints a kockára dobáshoz vagy válassz egy számot az alábbiból!
</p>
{/* Dropdown to select number 1-6 (triggers animated roll to that number) */}
<div className="mb-3">
<select
value={selectedDice ?? ""}
onChange={(e) => {
const v = e.target.value ? Number(e.target.value) : null
setSelectedDice(v)
}}
className="bg-gray-900 text-gray-200 rounded-md p-2 border border-gray-700"
>
<option value="">Válassz számot...</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
</div>
<Dice onRoll={handleDiceRoll} selectedValue={selectedDice} />
<p className="text-gray-300 text-sm mb-4">Kattints a kockára dobáshoz!</p>
<Dice onRoll={handleDiceRoll} />
</div>
</div>
</div>
@@ -0,0 +1,35 @@
// src/pages/Home/Home.jsx
// Régi PlayMenu-s oldal, "Home" néven
import { useState } from "react"
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() {
// Dummy callbackok és user példa
const handleJoinGame = (code) => {
alert(`Csatlakozás játékhoz: ${code}`)
}
const handleCreateGame = () => {
alert("Új játék létrehozása")
}
const user = { name: "Teszt Elek" }
return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
<div className="fixed inset-0 -z-10 pointer-events-none">
<Background />
</div>
<div className="fixed top-0 left-0 right-0 z-30">
<Navbar />
</div>
<main className="flex-1 flex flex-col items-center justify-start py-15 min-h-0 mt-[640px]">
<PlayMenu onJoinGame={handleJoinGame} onCreateGame={handleCreateGame} user={user} />
{/* Ide jöhetnek további szekciók, ha szeretnél még tartalmat */}
</main>
<Footer />
</div>
)
}
@@ -1,17 +1,13 @@
// src/pages/Home/Home.jsx
// Régi PlayMenu-s oldal, "Home" néven
import { useEffect } from "react"
import useRequireAuth from "../../hooks/useRequireAuth"
import Navbar from "../../components/Navbar/Navbar"
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 PlayMenu from "../../components/Landingpage/PlayMenu.jsx"
export default function Home() {
// a hook inicializálja a user-t a localStorage-ból és visszaadja a state-et + settert
const [user, setUser] = useRequireAuth({ redirect: false }) // no redirect on unauthenticated visitors
// Dummy callbackok és user példa
const handleJoinGame = (code) => {
alert(`Csatlakozás játékhoz: ${code}`)
@@ -19,9 +15,7 @@ export default function Home() {
const handleCreateGame = () => {
alert("Új játék létrehozása")
}
const userObj = { name: user }
// ha szükséges a user módosítása máshol: setUser("újnév") automatikusan menti localStorage-be
const user = { name: "Teszt Elek" }
return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
@@ -31,13 +25,8 @@ export default function Home() {
<div className="fixed top-0 left-0 right-0 z-30">
<Navbar />
</div>
<main className="flex-1 min-h-[calc(100vh-64px)] flex mt-[64px] flex-col items-center justify-center">
<PlayMenu
onJoinGame={handleJoinGame}
onCreateGame={handleCreateGame}
user={userObj}
setUser={setUser}
/>
<main className="flex-1 flex flex-col items-center justify-start py-15 min-h-0 mt-[64px]">
<PlayMenu onJoinGame={handleJoinGame} onCreateGame={handleCreateGame} user={user} />
{/* Ide jöhetnek további szekciók, ha szeretnél még tartalmat */}
</main>
<Footer />
@@ -1,34 +1,23 @@
// src/pages/Landing/Landingpage.jsx
// Főoldal - Landing Page
import { data, useNavigate } from "react-router-dom"
import { useState } from "react"
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() {
const navigate = useNavigate();
// Navigációs callbackok
const handleNavigateToPlay = () => {
navigate("/login", { preventScrollReset: false });
window.scrollTo(0, 0);
};
// Itt lehet navigálni a játék oldalra
alert("Navigáció a játék oldalra")
}
const handleNavigateToAuth = () => {
navigate("/companies", { preventScrollReset: false });
window.scrollTo(0, 0);
};
const handleNavigateToGame = () => {
navigate("/home", { preventScrollReset: false });
window.scrollTo(0, 0);
};
const handleNavigateToContacts = () => {
navigate("/contacts");
};
// Itt lehet navigálni a bejelentkezés oldalra
alert("Navigáció a bejelentkezés oldalra")
}
return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
@@ -39,9 +28,9 @@ export default function LandingPageMain() {
<Navbar />
</div>
<main className="flex-1 flex flex-col items-center justify-start py-15 min-h-0 mt-[64px]">
<LandingPage onNavigateToContacts={handleNavigateToContacts} onNavigateToPlay={handleNavigateToPlay} onNavigateToAuth={handleNavigateToAuth} onNavigateToGame={handleNavigateToGame} />
<LandingPage onNavigateToPlay={handleNavigateToPlay} onNavigateToAuth={handleNavigateToAuth} />
</main>
<Footer />
</div>
);
)
}
@@ -1,96 +0,0 @@
import React, { useEffect, useRef, useState } from "react"
import { useNavigate, useLocation } from "react-router-dom"
import Navbar from "../../components/Navbar/Navbar"
import Background from "../../assets/backgrounds/Background.jsx"
import useRequireAuth from "../../hooks/useRequireAuth"
const Lobby = () => {
const [visible, setVisible] = useState(false)
const sectionRef = useRef(null)
const navigate = useNavigate()
const location = useLocation()
const [user, setUser] = useRequireAuth()
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setVisible(true)
},
{ threshold: 0.3 }
)
if (sectionRef.current) observer.observe(sectionRef.current)
return () => observer.disconnect()
}, [])
const handleExit = () => {
if (window.confirm("Biztosan ki szeretnél lépni a lobbyból?")) {
navigate("/home")
}
}
const getInitials = (name) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.slice(0, 2)
.toUpperCase()
}
return (
<div className="flex flex-col min-h-screen overflow-y-auto relative">
<div className="fixed top-0 left-0 w-full h-full -z-10">
<Background />
</div>
<div className="fixed top-0 left-0 right-0 z-30">
<Navbar />
</div>
<main className="flex-grow text-white px-6 pt-16 mt-0 mb-20 flex items-center justify-center">
<section
ref={sectionRef}
className={`w-full max-w-3xl rounded-2xl p-8 md:p-10 transition-all duration-1000 ease-out backdrop-blur-md shadow-2xl ${
visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10"
}`}
style={{ background: "rgba(0,0,0,0.25)" }}
>
<h1 className="text-4xl md:text-5xl font-extrabold text-green-300 mb-4 text-center tracking-wide drop-shadow-lg">
{user} Lobby-ja
</h1>
<p className="text-lg text-zinc-300 mb-8 text-center">
Játékosok, akik csatlakoztak ehhez a szobához:
</p>
<div className="bg-zinc-800/90 rounded-xl shadow-lg p-6 mb-8">
<ul className="flex flex-col gap-4">
<li className="bg-zinc-700 py-3 px-4 rounded-xl text-green-400 font-semibold flex items-center gap-4 shadow hover:shadow-green-500/20 transition">
<div
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold"
style={{ background: "rgba(34,197,94,0.12)", color: "var(--color-mint)" }}
>
{getInitials(user)}
</div>
<span className="text-white text-lg">{user}</span>
</li>
</ul>
</div>
<div className="flex justify-center">
<button
onClick={handleExit}
className="bg-gradient-to-r from-green-700 to-green-500 hover:from-green-600 hover:to-green-400 text-white px-8 py-3 rounded-xl font-semibold shadow-lg hover:shadow-green-400/30 transition-transform transform hover:scale-105"
>
Kilépés
</button>
</div>
</section>
</main>
</div>
)
}
export default Lobby
@@ -3,12 +3,8 @@ 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"
import useRequireAuth from "../../hooks/useRequireAuth.jsx"
export default function Reports() {
const [username] = useRequireAuth({ key: "username", redirectTo: "/login" })
return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
{/* Háttér */}
@@ -27,8 +23,9 @@ export default function Reports() {
{/* Fejléc */}
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-white">Játék Riportok</h2>
<p className="text-gray-300 mt-2">Áttekintés a legutóbbi játékokról és statisztikákról</p>
{username && <p className="text-sm text-gray-400 mt-1">Bejelentkezett: {username}</p>}
<p className="text-gray-300 mt-2">
Áttekintés a legutóbbi játékokról és statisztikákról
</p>
</div>
{/* Statisztikai kártyák */}
@@ -6,7 +6,7 @@ import Logo from "../../assets/pictures/Logo.jsx"
import Navbar from "../../components/Navbar/Navbar"
import Footer from "../../components/Footer/Footer.jsx"
import UserProfile from "../../components/Userdetails/Userdetails.jsx"
import CompanyHub from "../Contacts/Contacts.jsx"
import CompanyHub from "../Companies/Companies.jsx"
import RatingSet from "../../components/PopUp/RatingSet" // <- statisztikai komponens
+65 -97
View File
@@ -1,103 +1,74 @@
import React, { useState, useEffect, useRef } from "react"
import { dotPositions } from "./diceDotPositions"
import React, { useState, useEffect, useRef } from 'react';
const Dice = ({ onRoll, selectedValue }) => {
const [diceValue, setDiceValue] = useState(1)
const [isRolling, setIsRolling] = useState(false)
const animationRef = useRef(null)
const rollTimeoutRef = useRef(null)
const Dice = ({ onRoll }) => {
const [diceValue, setDiceValue] = useState(1);
const [isRolling, setIsRolling] = useState(false);
const animationRef = useRef(null);
const rollTimeoutRef = useRef(null);
const diceFaces = [
[<div key="center" className="dice-dot" style={dotPositions.center}></div>],
[
<div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>,
<div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
],
[
<div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>,
<div key="center" className="dice-dot" style={dotPositions.center}></div>,
<div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
],
[
<div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>,
<div key="top-right" className="dice-dot" style={dotPositions.topRight}></div>,
<div key="bottom-left" className="dice-dot" style={dotPositions.bottomLeft}></div>,
<div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
],
[
<div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>,
<div key="top-right" className="dice-dot" style={dotPositions.topRight}></div>,
<div key="center" className="dice-dot" style={dotPositions.center}></div>,
<div key="bottom-left" className="dice-dot" style={dotPositions.bottomLeft}></div>,
<div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
],
[
<div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>,
<div key="top-right" className="dice-dot" style={dotPositions.topRight}></div>,
<div key="middle-left" className="dice-dot" style={dotPositions.middleLeft}></div>,
<div key="middle-right" className="dice-dot" style={dotPositions.middleRight}></div>,
<div key="bottom-left" className="dice-dot" style={dotPositions.bottomLeft}></div>,
<div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
],
]
[<div key="center" className="dice-dot"></div>],
[<div key="top-left" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot"></div>],
[<div key="top-left" className="dice-dot"></div>, <div key="center" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot"></div>],
[<div key="top-left" className="dice-dot"></div>, <div key="top-right" className="dice-dot"></div>,
<div key="bottom-left" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot"></div>],
[<div key="top-left" className="dice-dot"></div>, <div key="top-right" className="dice-dot"></div>,
<div key="center" className="dice-dot"></div>,
<div key="bottom-left" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot"></div>],
[<div key="top-left" className="dice-dot"></div>, <div key="top-right" className="dice-dot"></div>,
<div key="middle-left" className="dice-dot"></div>, <div key="middle-right" className="dice-dot"></div>,
<div key="bottom-left" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot"></div>]
];
useEffect(() => {
return () => {
if (animationRef.current) cancelAnimationFrame(animationRef.current)
if (rollTimeoutRef.current) clearTimeout(rollTimeoutRef.current)
}
}, [])
if (animationRef.current) cancelAnimationFrame(animationRef.current);
if (rollTimeoutRef.current) clearTimeout(rollTimeoutRef.current);
};
}, []);
// Helper that starts the rolling animation and finishes with targetValue if provided
const startRoll = (targetValue = null) => {
if (isRolling) return
const rollDice = () => {
if (isRolling) return;
setIsRolling(true)
setIsRolling(true);
let duration = 0
const rollInterval = 80 // ms between dice face changes
const maxDuration = 1500 // total animation time
let duration = 0;
const rollInterval = 80; // ms between dice face changes
const maxDuration = 1500; // total animation time
const rollAnimation = () => {
const randomValue = Math.floor(Math.random() * 6) + 1
setDiceValue(randomValue)
const randomValue = Math.floor(Math.random() * 6) + 1;
setDiceValue(randomValue);
duration += rollInterval
duration += rollInterval;
if (duration < maxDuration) {
// Speed effect: slow down towards the end
const nextInterval = rollInterval * (1 + (duration / maxDuration) * 2)
const nextInterval = rollInterval * (1 + (duration / maxDuration) * 2);
rollTimeoutRef.current = setTimeout(() => {
animationRef.current = requestAnimationFrame(rollAnimation)
}, nextInterval)
animationRef.current = requestAnimationFrame(rollAnimation);
}, nextInterval);
} else {
// Final roll (use targetValue if provided)
const finalValue = targetValue != null ? Number(targetValue) : Math.floor(Math.random() * 6) + 1
setDiceValue(finalValue)
setIsRolling(false)
// Final roll
const finalValue = Math.floor(Math.random() * 6) + 1;
setDiceValue(finalValue);
setIsRolling(false);
if (onRoll) onRoll(finalValue)
}
if (onRoll) onRoll(finalValue);
}
};
animationRef.current = requestAnimationFrame(rollAnimation)
}
// Click to roll randomly
const rollDice = () => {
startRoll(null)
}
// If parent provides a selectedValue, animate to that value
useEffect(() => {
if (selectedValue != null) {
startRoll(Number(selectedValue))
}
}, [selectedValue])
animationRef.current = requestAnimationFrame(rollAnimation);
};
return (
<div className={`dice-container ${isRolling ? "rolling" : ""}`} onClick={rollDice}>
<div className="dice">{diceFaces[diceValue - 1]}</div>
<div
className={`dice-container ${isRolling ? 'rolling' : ''}`}
onClick={rollDice}
>
<div className="dice">
{diceFaces[diceValue - 1]}
</div>
<style jsx>{`
.dice-container {
width: 80px;
@@ -136,21 +107,11 @@ const Dice = ({ onRoll, selectedValue }) => {
}
@keyframes roll {
0% {
transform: rotateX(0deg) rotateY(0deg);
}
25% {
transform: rotateX(90deg) rotateY(45deg);
}
50% {
transform: rotateX(180deg) rotateY(90deg);
}
75% {
transform: rotateX(270deg) rotateY(135deg);
}
100% {
transform: rotateX(360deg) rotateY(180deg);
}
0% { transform: rotateX(0deg) rotateY(0deg); }
25% { transform: rotateX(90deg) rotateY(45deg); }
50% { transform: rotateX(180deg) rotateY(90deg); }
75% { transform: rotateX(270deg) rotateY(135deg); }
100% { transform: rotateX(360deg) rotateY(180deg); }
}
.dice-dot {
@@ -162,10 +123,17 @@ const Dice = ({ onRoll, selectedValue }) => {
box-shadow: inset 0 0 3px rgba(0,0,0,0.3);
}
/* removed :nth-child positioning — positions are provided inline from diceDotPositions.js */
/* Positioning dots */
.dice-dot:nth-child(1) { top: 50%; left: 50%; transform: translate(-50%, -50%); } /* Center */
.dice-dot:nth-child(2) { top: 20%; left: 20%; } /* Top-left */
.dice-dot:nth-child(3) { bottom: 20%; right: 20%; } /* Bottom-right */
.dice-dot:nth-child(4) { top: 20%; right: 20%; } /* Top-right */
.dice-dot:nth-child(5) { bottom: 20%; left: 20%; } /* Bottom-left */
.dice-dot:nth-child(6) { top: 50%; left: 20%; transform: translateY(-50%); } /* Middle-left */
.dice-dot:nth-child(7) { top: 50%; right: 20%; transform: translateY(-50%); } /* Middle-right */
`}</style>
</div>
)
}
);
};
export default Dice
export default Dice;
@@ -1,9 +0,0 @@
export const dotPositions = {
center: { top: "50%", left: "50%", transform: "translate(-50%, -50%)" },
topLeft: { top: "20%", left: "20%" },
bottomRight: { bottom: "20%", right: "20%" },
topRight: { top: "20%", right: "20%" },
bottomLeft: { bottom: "20%", left: "20%" },
middleLeft: { top: "50%", left: "20%", transform: "translateY(-50%)" },
middleRight: { top: "50%", right: "20%", transform: "translateY(-50%)" },
}
-12
View File
@@ -18,7 +18,6 @@ if %errorlevel% neq 0 (
if "%1"=="dev:start" goto dev_start
if "%1"=="dev:watch" goto dev_watch
if "%1"=="dev:watch_nat" goto dev_watch_nat
if "%1"=="dev:stop" goto dev_stop
if "%1"=="prod:start" goto prod_start
if "%1"=="prod:stop" goto prod_stop
@@ -50,16 +49,6 @@ docker-compose -f docker-compose.watch.yml --env-file .env.dev up --build --watc
cd ..
goto end
:dev_watch_nat
echo [INFO] Starting SerpentRace development environment with file watchers without nat...
echo [INFO] This will automatically sync file changes and rebuild containers as needed
cd SerpentRace_Docker
echo [INFO] This will use system network to avoid nat
docker-compose -f docker-compose.watch.nat.yml --env-file .env.dev up --build --watch
cd ..
goto end
:dev_stop
echo [INFO] Stopping SerpentRace development environment...
cd SerpentRace_Docker
@@ -120,7 +109,6 @@ echo.
echo Commands:
echo dev:start Start development environment with hot reload
echo dev:watch Start development environment with file watchers (auto-rebuild)
echo dev:watch_nat Start development environment with file watchers (auto-rebuild); Without NAT
echo dev:stop Stop development environment
echo prod:start Start production environment
echo prod:stop Stop production environment
Executable → Regular
View File
-11
View File
@@ -1,11 +0,0 @@
kapcs fel routing
navbar széthúz
footer kapcsolat
navabar gomboksorrend
vagy kontat vagy kapcsolat
navbar bejelent
navbar layout finomít
deck lista kialakít köv oldal max stb
palki info get
deck hibák javítása