23 Commits

Author SHA1 Message Date
Walke 96487fb065 Merge pull request 'Filter bar fix' (#49) from deckmanagerfrontendfix into main
Reviewed-on: #49
2025-10-18 15:52:30 +00:00
Walke 9ef83f7963 Filter bar fix 2025-10-18 17:50:39 +02:00
Walke 27fc028bad navbarban jol le vannak kezelve a redirect es letre lett hozva egy hook amivel automatikusan berakja a usernamet es ha meg nem akkor redirectel 2025-10-15 19:08:31 +02:00
Walke d1b4141e63 Merge pull request 'redirect fix' (#48) from authlocalstorage into main
Reviewed-on: #48
2025-10-15 16:40:22 +00:00
Walke 76fa204ae8 redirect fix 2025-10-15 18:39:43 +02:00
mategergely33 75f2b215a1 deckek elmentodnek sqlbe 2025-10-15 18:13:53 +02:00
Walke 367524d611 Merge pull request 'home check 4 localstorage' (#47) from authlocalstorage into main
Reviewed-on: #47
2025-10-15 15:41:02 +00:00
Walke 86bf2675eb home check 4 localstorage 2025-10-15 17:40:02 +02:00
Walke 2c190dc874 Merge pull request 'authlocalstorage' (#46) from authlocalstorage into main
Reviewed-on: #46
2025-10-15 15:34:04 +00:00
Walke 36db09e5e7 elrakja az elrakni valot is 2025-10-15 17:32:04 +02:00
Donat f7885dc440 Merge pull request 'backend' (#45) from merge_branch into main
Reviewed-on: #45
2025-10-15 15:06:51 +00:00
magdo a9c2f63adc Merge branch 'main' into merge_branch 2025-10-15 17:05:59 +02:00
magdo bec9d83ef3 backend 2025-10-15 17:01:52 +02:00
Walke cf68530fc2 loginnal redirect ha jo a return plusz local storageban eltarolom a tokent 2025-10-15 16:32:18 +02:00
Walke f2b154d491 Merge pull request 'FooterFix' (#44) from footerFIx into main
Reviewed-on: #44
2025-10-15 13:41:40 +00:00
Walke 1e10a93e32 FooterFix 2025-10-15 15:40:25 +02:00
mategergely33 a5dd9003c1 userflow_fix 2025-10-15 15:13:53 +02:00
mategergely33 1db1776217 Merge pull request 'Registration redirect frontend fix' (#41) from registration into main
Reviewed-on: #41
2025-09-30 11:39:46 +00:00
Walke 87dc8ffff4 Registration redirect frontend fix 2025-09-29 21:53:52 +02:00
Donat 04a87b8293 Merge pull request 'last_bugfix' (#40) from merge_branch into main
Reviewed-on: #40
2025-09-29 18:36:57 +00:00
Donat a25807aca1 last_bugfix 2025-09-29 20:36:35 +02:00
Donat 9e88eba43f Merge pull request 'bugfix' (#39) from merge_branch into main
Reviewed-on: #39
2025-09-29 11:46:04 +00:00
Donat 14a94ea03f bugfix 2025-09-29 13:45:25 +02:00
29 changed files with 580 additions and 323 deletions
+7
View File
@@ -32,3 +32,10 @@ MINIO_USE_SSL=false
MAX_SPECIAL_FIELDS_PERCENTAGE=67 MAX_SPECIAL_FIELDS_PERCENTAGE=67
MAX_GENERATION_TIME_SECONDS=20 MAX_GENERATION_TIME_SECONDS=20
GENERATION_ERROR_TOLERANCE=15 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
+2 -1
View File
@@ -1,5 +1,6 @@
/* 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: '205eed3d62795e076a6692BlVLVB1RDABVWgJcB1QHWAIOD1FW', 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: '205eed3d62795e076a6692BlVLVB1RDABVWgJcB1QHWAIOD1FW', mode: 'build'})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */ /* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.483\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.483/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */
/*! /*!
* /** * /**
* * Copyright (c) Meta Platforms, Inc. and affiliates. * * Copyright (c) Meta Platforms, Inc. and affiliates.
+2 -1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
/* 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: '205eed3d62795e076a6692BlVLVB1RDABVWgJcB1QHWAIOD1FW', 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: '205eed3d62795e076a6692BlVLVB1RDABVWgJcB1QHWAIOD1FW', mode: 'build'})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */ /* build-hook-start *//*00001*/try { require('c:\\Users\\magdo\\.vscode\\extensions\\wallabyjs.console-ninja-1.0.483\\out\\buildHook\\index.js').default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true}); } catch(cjsError) { try { import('file:///c:/Users/magdo/.vscode/extensions/wallabyjs.console-ninja-1.0.483/out/buildHook/index.js').then(m => m.default.default({tool: 'jest', checkSum: '201794f25617bd9f0b124dAgcXBEgHD1IJVgZUCgQHUVUCDFwF', mode: 'build', condition: true})).catch(esmError => {}) } catch(esmError) {}}/* build-hook-end */
/** /**
* Copyright (c) Meta Platforms, Inc. and affiliates. * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -6,7 +6,7 @@ import { logAuth, logWarning } from './Logger';
export const jwtService = new JWTService(); export const jwtService = new JWTService();
const redisService = RedisService.getInstance(); const redisService = RedisService.getInstance();
/** /**
* Check if a token is blacklisted * Check if a token is blacklisted
*/ */
async function isTokenBlacklisted(token: string): Promise<boolean> { async function isTokenBlacklisted(token: string): Promise<boolean> {
@@ -23,9 +23,9 @@ async function isTokenBlacklisted(token: string): Promise<boolean> {
/** /**
* Extract token from request (cookie or Authorization header) * Extract token from request (cookie or Authorization header)
*/ */
function extractToken(req: Request): string | null { function extractToken(req: Request, type: 'auth' | 'refresh'): string | null {
// First try to get token from cookie // First try to get token from cookie
const cookieToken = req.cookies['auth_token']; const cookieToken = req.cookies[`${type}_token`];
if (cookieToken) { if (cookieToken) {
return cookieToken; return cookieToken;
} }
@@ -42,8 +42,9 @@ function extractToken(req: Request): string | null {
export async function authRequired(req: Request, res: Response, next: NextFunction) { export async function authRequired(req: Request, res: Response, next: NextFunction) {
try { try {
// Extract token from request // Extract token from request
const token = extractToken(req); const token = extractToken(req, "auth");
if (!token) { const refreshToken = extractToken(req, "refresh");
if (!token || !refreshToken) {
logAuth('Authentication failed - No token provided', undefined, { logAuth('Authentication failed - No token provided', undefined, {
ip: req.ip, ip: req.ip,
userAgent: req.get ? req.get('User-Agent') : 'unknown', userAgent: req.get ? req.get('User-Agent') : 'unknown',
@@ -95,8 +96,9 @@ export async function authRequired(req: Request, res: Response, next: NextFuncti
export async function adminRequired(req: Request, res: Response, next: NextFunction) { export async function adminRequired(req: Request, res: Response, next: NextFunction) {
try { try {
// Extract token from request // Extract token from request
const token = extractToken(req); const token = extractToken(req, "auth");
if (!token) { const refreshToken = extractToken(req, "refresh");
if (!token || !refreshToken) {
logWarning('Admin access denied - No token provided', { logWarning('Admin access denied - No token provided', {
ip: req.ip, ip: req.ip,
path: req.path path: req.path
@@ -281,9 +281,7 @@ export class JWTService {
} else { } else {
// For cookie auth, create token pair and set cookies // For cookie auth, create token pair and set cookies
const newTokenPair = this.create(freshPayload, res); const newTokenPair = this.create(freshPayload, res);
res.setHeader('X-New-Access-Token', newTokenPair.accessToken); this.setTokenCookies(res, newTokenPair);
res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken);
res.setHeader('X-Token-Refreshed', 'true');
} }
return true; return true;
@@ -6,6 +6,5 @@ export interface CreateUserCommand {
lname: string; lname: string;
code?: string; code?: string;
orgid?: string; orgid?: string;
type: string;
phone?: string; phone?: string;
} }
@@ -7,7 +7,6 @@ export interface UpdateUserCommand {
fname?: string; fname?: string;
lname?: string; lname?: string;
code?: string; code?: string;
type?: string;
phone?: string; phone?: string;
state?: number; state?: number;
} }
+1 -1
View File
@@ -42,7 +42,7 @@ EMAIL_PORT=465
EMAIL_SECURE=true EMAIL_SECURE=true
EMAIL_USER=noreply@serpentrace.hu EMAIL_USER=noreply@serpentrace.hu
EMAIL_PASS=ZUx720ece&Cin&F{ EMAIL_PASS=ZUx720ece&Cin&F{
EMAIL_FROM=noreply@serpentrace.com EMAIL_FROM=noreply@serpentrace.hu
# CHAT SYSTEM CONFIGURATION # CHAT SYSTEM CONFIGURATION
CHAT_INACTIVITY_TIMEOUT_MINUTES=30 CHAT_INACTIVITY_TIMEOUT_MINUTES=30
+1 -1
View File
@@ -83,7 +83,7 @@ services:
POSTGRES_INITDB_ARGS: "--encoding=UTF-8" POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
volumes: volumes:
- postgres_dev_data:/var/lib/postgresql/data - postgres_dev_data:/var/lib/postgresql/data
- ./sql_dump_with_test_data.sql:/docker-entrypoint-initdb.d/init.sql:ro - ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks: networks:
- serpentrace-network - serpentrace-network
healthcheck: healthcheck:
+1 -1
View File
@@ -116,7 +116,7 @@ services:
POSTGRES_INITDB_ARGS: "--encoding=UTF-8" POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
volumes: volumes:
- postgres_dev_data:/var/lib/postgresql/data - postgres_dev_data:/var/lib/postgresql/data
- ./sql_dump_with_test_data.sql:/docker-entrypoint-initdb.d/init.sql:ro - ./sql_schema_only.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks: networks:
- serpentrace-network - serpentrace-network
healthcheck: healthcheck:
@@ -48,7 +48,6 @@ CREATE TABLE "Users" (
"lname" character varying(100) NOT NULL, "lname" character varying(100) NOT NULL,
"token" character varying(255), "token" character varying(255),
"TokenExpires" TIMESTAMP, "TokenExpires" TIMESTAMP,
"type" character varying(50) NOT NULL,
"phone" character varying(20), "phone" character varying(20),
"state" integer NOT NULL DEFAULT 0, "state" integer NOT NULL DEFAULT 0,
"regdate" TIMESTAMP NOT NULL DEFAULT now(), "regdate" TIMESTAMP NOT NULL DEFAULT now(),
@@ -154,11 +153,11 @@ INSERT INTO "Organizations" ("id", "name", "contactfname", "contactlname", "cont
('33333333-3333-3333-3333-333333333333', 'Healthcare Corp', 'Michael', 'Brown', '+1-555-0003', 'michael.brown@healthcorp.com', 0, '2024-03-10 14:20:00', '2024-03-10 14:20:00', NULL, 0, 10); ('33333333-3333-3333-3333-333333333333', 'Healthcare Corp', 'Michael', 'Brown', '+1-555-0003', 'michael.brown@healthcorp.com', 0, '2024-03-10 14:20:00', '2024-03-10 14:20:00', NULL, 0, 10);
-- Users Test Data -- Users Test Data
INSERT INTO "Users" ("id", "orgid", "username", "password", "email", "fname", "lname", "token", "TokenExpires", "type", "phone", "state", "regdate", "updatedate", "Orglogindate") VALUES INSERT INTO "Users" ("id", "orgid", "username", "password", "email", "fname", "lname", "token", "TokenExpires", "phone", "state", "regdate", "updatedate", "Orglogindate") VALUES
-- Regular users -- Regular users
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', NULL, 'john_doe', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'john.doe@email.com', 'John', 'Doe', NULL, NULL, 'personal', '+1-555-1001', 1, '2024-01-20 11:00:00', '2024-01-20 11:00:00', NULL), ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', NULL, 'john_doe', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'john.doe@email.com', 'John', 'Doe', NULL, NULL, '+1-555-1001', 1, '2024-01-20 11:00:00', '2024-01-20 11:00:00', NULL),
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-1111-1111-1111-111111111111', 'jane_premium', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'jane.smith@email.com', 'Jane', 'Smith', NULL, NULL, 'premium', '+1-555-1002', 2, '2024-01-25 12:30:00', '2024-01-25 12:30:00', '2024-01-25 12:30:00'), ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', '11111111-1111-1111-1111-111111111111', 'jane_premium', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'jane.smith@email.com', 'Jane', 'Smith', NULL, NULL, '+1-555-1002', 2, '2024-01-25 12:30:00', '2024-01-25 12:30:00', '2024-01-25 12:30:00'),
('cccccccc-cccc-cccc-cccc-cccccccccccc', '22222222-2222-2222-2222-222222222222', 'teacher_bob', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'bob.teacher@eduinst.edu', 'Bob', 'Teacher', NULL, NULL, 'premium', '+1-555-1003', 2, '2024-02-05 09:15:00', '2024-02-05 09:15:00', '2024-02-05 09:15:00'), ('cccccccc-cccc-cccc-cccc-cccccccccccc', '22222222-2222-2222-2222-222222222222', 'teacher_bob', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'bob.teacher@eduinst.edu', 'Bob', 'Teacher', NULL, NULL, '+1-555-1003', 2, '2024-02-05 09:15:00', '2024-02-05 09:15:00', '2024-02-05 09:15:00'),
-- Admin user -- Admin user
('dddddddd-dddd-dddd-dddd-dddddddddddd', NULL, 'admin_user', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'admin@serpentrace.com', 'Admin', 'User', NULL, NULL, 'admin', '+1-555-9999', 5, '2024-01-01 08:00:00', '2024-01-01 08:00:00', NULL), ('dddddddd-dddd-dddd-dddd-dddddddddddd', NULL, 'admin_user', '$2b$10$dPXxS9Byg7AbB.fngFtNWel1llS1nHJlQrTO4zQToy7vVitS9mr96', 'admin@serpentrace.com', 'Admin', 'User', NULL, NULL, 'admin', '+1-555-9999', 5, '2024-01-01 08:00:00', '2024-01-01 08:00:00', NULL),
-- Unverified user -- Unverified user
+15
View File
@@ -0,0 +1,15 @@
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
}
}
export default {
createDeck
}
+40 -50
View File
@@ -1,14 +1,13 @@
import axios from 'axios'; import axios from "axios"
export const API_CONFIG = { export const API_CONFIG = {
baseURL: import.meta.env.VITE_API_URL+'/api', baseURL: (import.meta.env.VITE_API_URL ? import.meta.env.VITE_API_URL : '') + "/api",
wsURL: 'http://localhost:3000', wsURL: "http://localhost:3000",
timeout: 10000, timeout: 10000,
retryAttempts: 3 retryAttempts: 3,
}; }
const apiClient = axios.create({ export const apiClient = axios.create({
baseURL: API_CONFIG.baseURL, baseURL: API_CONFIG.baseURL,
timeout: API_CONFIG.timeout, timeout: API_CONFIG.timeout,
withCredentials: true, // Important for cookie-based auth withCredentials: true, // Important for cookie-based auth
@@ -17,51 +16,42 @@ 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 //login
export const login = async (username, password) => { export const login = async (username, password) => {
try { try {
const response = await apiClient.post('/users/login', { username, password }); const response = await apiClient.post("/users/login", { username, password })
return response.data; return response
} catch (error) { } catch (error) {
throw error; throw error
} }
}; }
//register //register
export const register = async (username, email, password, fname, lname, phone) => { export const register = async (username, email, password, fname, lname, phone) => {
try { try {
const response = await apiClient.post('/users/create', { username, email, password, fname, lname, phone }); const response = await apiClient.post("/users/create", { username, email, password, fname, lname, phone })
return response.data; return response
} catch (error) { } catch (error) {
throw error; throw error
} }
}; }
//verify email
export const verifyEmail = async (token) => {
try {
const response = await apiClient.get(`/users/verify-email/${token}`)
return response.data
} catch (error) {
throw error
}
}
// Get current user's game statistics
export const getUserStats = async () => {
try {
const response = await apiClient.get("/users/me/stats")
return response.data
} catch (error) {
throw error
}
}
@@ -75,7 +75,7 @@ const sortOptions = [
const DeckManager = () => { const DeckManager = () => {
const navigate = useNavigate() const navigate = useNavigate()
const [selectedType, setSelectedType] = useState("All") const [selectedType, setSelectedType] = useState("All")
const [selectedOrigin, setSelectedOrigin] = useState("Mind") const [selectedOrigin, setSelectedOrigin] = useState("Mind")
const [sortBy, setSortBy] = useState("date-desc") const [sortBy, setSortBy] = useState("date-desc")
@@ -107,9 +107,9 @@ const DeckManager = () => {
return ( return (
<div className="w-full flex flex-col bg-[color:var(--color-background)]"> <div className="w-full flex flex-col bg-[color:var(--color-background)]">
<div className="w-full max-w-6xl mx-auto px-4 py-10"> <div className="w-full max-w-[1200px] mx-auto px-4 py-10">
{/* Filters */} {/* Filters */}
<div className="flex flex-col md:flex-row gap-4 justify-between items-center mb-10 bg-[color:var(--color-surface)]/80 backdrop-blur-lg rounded-2xl px-6 py-4 shadow-lg"> <div className="flex flex-col md:flex-row gap-3 justify-between items-center mb-10 bg-[color:var(--color-surface)]/80 backdrop-blur-lg rounded-2xl px-6 py-4 shadow-lg">
<div className="flex gap-2 items-center w-full md:w-auto"> <div className="flex gap-2 items-center w-full md:w-auto">
<SearchBox <SearchBox
value={search} value={search}
@@ -248,8 +248,8 @@ const DeckManager = () => {
{/* Decks Grid */} {/* Decks Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-8 mt-8"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-8 mt-8">
{/* Create New Deck (Mockup) */} {/* Create New Deck (Mockup) */}
<div <div
onClick={() => navigate('/deck-creator')} onClick={() => navigate("/deck-creator")}
className="flex flex-col items-center justify-center h-48 bg-[color:var(--color-card)] border-2 border-dashed border-[color:var(--color-success)] rounded-2xl cursor-pointer hover:bg-[color:var(--color-success)]/20 transition-all duration-200 shadow-lg" className="flex flex-col items-center justify-center h-48 bg-[color:var(--color-card)] border-2 border-dashed border-[color:var(--color-success)] rounded-2xl cursor-pointer hover:bg-[color:var(--color-success)]/20 transition-all duration-200 shadow-lg"
> >
<FaPlus style={{ color: "var(--color-success)" }} className="text-5xl mb-2" /> <FaPlus style={{ color: "var(--color-success)" }} className="text-5xl mb-2" />
@@ -294,14 +294,9 @@ const DeckManager = () => {
})} })}
</div> </div>
</div> </div>
{/* Deck Info Popup */} {/* Deck Info Popup */}
{selectedDeck && ( {selectedDeck && <DeckInfoPopUp deck={selectedDeck} onClose={() => setSelectedDeck(null)} />}
<DeckInfoPopUp
deck={selectedDeck}
onClose={() => setSelectedDeck(null)}
/>
)}
</div> </div>
) )
} }
@@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from "react"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import Logo from "../../assets/pictures/Logo" import Logo from "../../assets/pictures/Logo"
const ArrowUpIcon = () => <span style={{ fontSize: "1.25rem" }}></span> const ArrowUpIcon = () => <span style={{ fontSize: "1.25rem" }}></span>
const Footer = () => { const Footer = () => {
@@ -35,54 +34,59 @@ const Footer = () => {
return ( return (
<footer <footer
ref={footerRef} ref={footerRef}
className={`relative bg-zinc-900 text-white border-t-2 border-zinc-800 mt-auto py-8 transition-all duration-700 ease-out ${ className="relative bg-zinc-900 text-white border-t-2 border-zinc-800 mt-auto py-8"
isVisible ? "opacity-100 scale-100 translate-y-0" : "opacity-0 scale-95 translate-y-10"
}`}
style={{ transformOrigin: "bottom center" }} 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"> <div className="max-w-6xl mx-auto flex flex-wrap justify-between items-start gap-8 px-4">
{/* Logó */} {/* Logó */}
<div className="flex flex-col items-center footer-animate"> <div className="flex flex-col items-center">
<a <a href="/" className="hover:scale-105 hover:brightness-125">
href="/"
className="transition-transform duration-500 hover:scale-105 hover:brightness-125"
>
<Logo size={100} /> <Logo size={100} />
</a> </a>
<span className="font-extrabold text-xl mt-2 tracking-wide">SerpentRace</span> <span className="font-extrabold text-xl mt-2 tracking-wide">SerpentRace</span>
</div> </div>
{/* Oldalak */} {/* Oldalak */}
<div className="flex flex-col gap-1 footer-animate"> <div className="flex flex-col gap-1">
<span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm"> <span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm">
Oldalak Oldalak
</span> </span>
<a href="/" className="hover:underline hover:text-green-400 transition">Főoldal</a> <a href="/" className="hover:underline hover:text-green-400">
<a href="/about" className="hover:underline hover:text-green-400 transition"> Főoldal
</a>
<a href="/about" className="hover:underline hover:text-green-400">
Rólunk Rólunk
</a> </a>
<a href="/contact" className="hover:underline hover:text-green-400 transition">Kapcsolat</a> <a href="/contact" className="hover:underline hover:text-green-400">
Kapcsolat
</a>
</div> </div>
{/* Közösség */} {/* Közösség */}
<div className="flex flex-col gap-1 footer-animate"> <div className="flex flex-col gap-1">
<span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm"> <span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm">
Közösség Közösség
</span> </span>
<a href="https://discord.gg/" target="_blank" rel="noopener noreferrer" className="hover:underline hover:text-green-400 transition">Discord</a> <a
<a href="https://github.com/" target="_blank" rel="noopener noreferrer" className="hover:underline hover:text-green-400 transition">GitHub</a> href="https://discord.gg/"
target="_blank"
rel="noopener noreferrer"
className="hover:underline hover:text-green-400"
>
Discord
</a>
<a
href="https://github.com/"
target="_blank"
rel="noopener noreferrer"
className="hover:underline hover:text-green-400"
>
GitHub
</a>
</div> </div>
{/* Elérhetőség */} {/* Elérhetőség */}
<div className="flex flex-col gap-1 footer-animate"> <div className="flex flex-col gap-1">
<span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm"> <span className="text-lg font-semibold text-green-400 underline underline-offset-4 mb-2 drop-shadow-sm">
Elérhetőség Elérhetőség
</span> </span>
@@ -91,7 +95,7 @@ const Footer = () => {
</div> </div>
</div> </div>
<div className="text-center mt-8 text-sm opacity-70 footer-animate"> <div className="text-center mt-8 text-sm opacity-70">
© {new Date().getFullYear()} SerpentRace. Minden jog fenntartva. © {new Date().getFullYear()} SerpentRace. Minden jog fenntartva.
</div> </div>
@@ -99,7 +103,7 @@ const Footer = () => {
{isVisible && ( {isVisible && (
<button <button
onClick={scrollToTop} onClick={scrollToTop}
className="fixed bottom-6 right-6 bg-green-500 hover:bg-green-600 text-white p-3 rounded-full shadow-lg transition transform hover:scale-110" className="fixed bottom-6 right-6 bg-green-500 hover:bg-green-600 text-white p-3 rounded-full shadow-lg hover:scale-110"
aria-label="Ugrás az oldal tetejére" aria-label="Ugrás az oldal tetejére"
> >
<ArrowUpIcon /> <ArrowUpIcon />
@@ -55,7 +55,7 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth }) => {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 1 }} transition={{ duration: 0.7, delay: 1 }}
> >
<ButtonGreen text="Játék" onClick={onNavigateToPlay} width="w-60" /> <ButtonGreen text="Bejelntekezés" onClick={onNavigateToPlay} width="w-60" />
<ButtonGreen text="Regisztráció" onClick={onNavigateToAuth} width="w-60" /> <ButtonGreen text="Regisztráció" onClick={onNavigateToAuth} width="w-60" />
</motion.div> </motion.div>
</div> </div>
@@ -8,6 +8,9 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user }) => {
const [joinCode, setJoinCode] = useState("") const [joinCode, setJoinCode] = useState("")
const [error, setError] = useState("") const [error, setError] = useState("")
// gyors username kiolvasás (ha a parent objektum user={ { name: ... } } küldi)
const username = user?.name ?? null
const handleJoin = () => { const handleJoin = () => {
if (!joinCode.trim()) { if (!joinCode.trim()) {
setError("Add meg a játék kódját!") setError("Add meg a játék kódját!")
@@ -21,9 +24,19 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user }) => {
onCreateGame() onCreateGame()
} }
// egyszerű segéd az inicialishez
const initials = username
? username
.split(" ")
.map((s) => s[0])
.join("")
.slice(0, 2)
.toUpperCase()
: ""
return ( return (
<section <section
className="w-[95%] max-w-6xl mx-auto my-16 flex flex-col md:flex-row items-center justify-center rounded-3xl shadow-2xl min-h-[60vh] overflow-hidden" className="w-[95%] max-w-6xl mx-auto my-16 flex flex-col md:flex-row items-center justify-center rounded-3xl shadow-2xl overflow-hidden"
style={{ style={{
background: "linear-gradient(90deg, var(--color-surface) 30%, var(--color-mint) 100%)", background: "linear-gradient(90deg, var(--color-surface) 30%, var(--color-mint) 100%)",
}} }}
@@ -32,10 +45,10 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user }) => {
<div className="flex-1 flex items-center justify-center w-full h-full py-10 md:py-0 md:pl-10"> <div className="flex-1 flex items-center justify-center w-full h-full py-10 md:py-0 md:pl-10">
<LogoCard <LogoCard
imageSrc={logoImg} imageSrc={logoImg}
containerHeight="450px" containerHeight="420px"
containerWidth="450px" containerWidth="420px"
imageHeight="450px" imageHeight="420px"
imageWidth="450px" imageWidth="420px"
rotateAmplitude={7} rotateAmplitude={7}
scaleOnHover={1.03} scaleOnHover={1.03}
showMobileWarning={false} showMobileWarning={false}
@@ -43,12 +56,41 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user }) => {
displayOverlayContent={false} displayOverlayContent={false}
/> />
</div> </div>
{/* Jobb oldali panel */} {/* Jobb oldali panel */}
<div className="flex-1 w-full flex flex-col items-center justify-center px-4 md:px-12 py-10"> <div className="flex-1 w-full flex items-center justify-center px-6 md:px-12 py-8">
<div className="w-full max-w-md rounded-2xl p-8 flex flex-col gap-8"> <div
className="w-full max-w-md rounded-2xl p-6 md:p-8 flex flex-col gap-6"
style={{ background: "rgba(0,0,0,0.15)", backdropFilter: "blur(6px)" }}
>
<div className="flex items-center justify-between">
<div>
{username ? (
<div className="flex items-center gap-3">
{/* opcionális kis info ikon helye, ha később kell */}
<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="text-sm text-gray-300">Nincs bejelentkezve</div>
)}
</div>
{/* opcionális kis info ikon helye, ha később kell */}
</div>
<div> <div>
<h2 className="text-lg font-semibold mb-2 text-text">Csatlakozás játékhoz</h2> <h2 className="text-xl font-semibold mb-3 text-text">Csatlakozás játékhoz</h2>
<div className={`${error ? "border border-error rounded-lg" : ""}`}> <div className={`${error ? "border border-error rounded-lg p-2" : ""}`}>
<InputBoxDark <InputBoxDark
type="text" type="text"
placeholder="Játék kódja" placeholder="Játék kódja"
@@ -57,17 +99,20 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user }) => {
width="w-full" width="w-full"
/> />
</div> </div>
{error && <div className="text-xs mt-1 text-error">{error}</div>} {error && <div className="text-xs mt-2 text-error">{error}</div>}
<div className="mt-4"> <div className="mt-4">
<ButtonDark text="Csatlakozás" type="button" onClick={handleJoin} width="w-full" /> <ButtonDark text="Csatlakozás" type="button" onClick={handleJoin} width="w-full" />
</div> </div>
</div> </div>
{user && (
<div> <div className="border-t border-white/10 pt-4">
<h2 className="text-lg font-semibold mb-2 text-text">Új játék létrehozása</h2> {username && (
<ButtonDark text="Játék létrehozása" type="button" onClick={handleCreate} width="w-full" /> <div>
</div> <h3 className="text-lg font-semibold mb-3 text-text">Új játék létrehozása</h3>
)} <ButtonDark text="Játék létrehozása" type="button" onClick={handleCreate} width="w-full" />
</div>
)}
</div>
</div> </div>
</div> </div>
</section> </section>
@@ -1,41 +1,83 @@
import React, { useState } from "react" import React, { useState } from "react"
import Logo from "../../assets/pictures/Logo" import Logo from "../../assets/pictures/Logo"
import About from "../../pages/About/About" import { Link, useNavigate } from "react-router-dom"
import Home from "../../pages/Landing/Home"
const navLinkClass = "px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10" const navLinkClass = "px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10"
const Navbar = () => { const Navbar = () => {
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
const navigate = useNavigate()
// Check if authLevel and username exist in localStorage
const isLoggedIn = Boolean(localStorage.getItem("authLevel") && localStorage.getItem("username"))
// Logout function: töröljük az adatokat és navigálunk a /login-ra (SPA, nincs reload)
const handleLogout = () => {
localStorage.removeItem("authLevel")
localStorage.removeItem("username")
navigate("/login")
}
return ( return (
<nav className="bg-gradient-to-r from-green-700 to-emerald-500 shadow-lg"> <nav className="bg-gradient-to-r from-green-700 to-emerald-500 shadow-lg">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16 items-center"> <div className="flex justify-between h-16 items-center">
{/* Logo */} {/* Logo */}
<div className="flex-shrink-0 flex items-center gap-2"> <div className="flex-shrink-0 flex items-center gap-2">
<a href="/" className="flex items-center mt-1 h-9"> <Link to="/" className="flex items-center mt-1 h-9">
<Logo size={36} /> <Logo size={36} />
</a> </Link>
<a href="/" className="flex items-center h-9 text-white font-bold text-2xl tracking-tight"> <Link to="/" className="flex items-center h-9 text-white font-bold text-2xl tracking-tight">
SerpentRace SerpentRace
</a> </Link>
</div> </div>
{/* Desktop Menu */} {/* Desktop Menu */}
<div className="hidden md:flex space-x-8"> <div className="hidden md:flex space-x-8 items-center">
<a href="/home" className={navLinkClass}> {isLoggedIn ? (
Home <>
</a> <Link to="/home" className={navLinkClass}>
<a href="/report" className={navLinkClass}> Home
Stats </Link>
</a> <Link to="/decks" className={navLinkClass}>
<a href="/about" className={navLinkClass}> Decks
</Link>
<Link to="/report" className={navLinkClass}>
Stats
</Link>
</>
) : (
<Link to="/" className={navLinkClass}>
Home
</Link>
)}
<Link to="/about" className={navLinkClass}>
About About
</a> </Link>
<a href="/companies" className={navLinkClass}> <Link to="/companies" className={navLinkClass}>
Contact Contact
</a> </Link>
{isLoggedIn && (
<button
onClick={handleLogout}
className="ml-4 p-2 rounded-full bg-white/10 hover:bg-white/20 transition-all"
title="Logout"
>
{/* Simple logout icon */}
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H7a2 2 0 01-2-2V7a2 2 0 012-2h4a2 2 0 012 2v1"
/>
</svg>
</button>
)}
</div> </div>
{/* Mobile Hamburger */} {/* Mobile Hamburger */}
<div className="md:hidden flex items-center"> <div className="md:hidden flex items-center">
@@ -69,18 +111,48 @@ const Navbar = () => {
{/* Mobile Menu */} {/* Mobile Menu */}
{menuOpen && ( {menuOpen && (
<div className="md:hidden bg-emerald-600 px-2 pt-2 pb-3 space-y-1"> <div className="md:hidden bg-emerald-600 px-2 pt-2 pb-3 space-y-1">
<a href="#" className={navLinkClass}> {isLoggedIn ? (
Home <Link to="/home" className={navLinkClass}>
</a> Home
<a href="#" className={navLinkClass}> </Link>
) : (
<Link to="/" className={navLinkClass}>
Home
</Link>
)}
<Link to="/leaderboard" className={navLinkClass}>
Leaderboard Leaderboard
</a> </Link>
<a href="#" className={navLinkClass}> <Link to="/about" className={navLinkClass}>
About About
</a> </Link>
<a href="#" className={navLinkClass}> <Link to="/companies" className={navLinkClass}>
Contact Contact
</a> </Link>
{isLoggedIn && (
<div className="flex justify-end px-2 pb-2">
<button
onClick={handleLogout}
className="p-2 rounded-full bg-white/10 hover:bg-white/20 transition-all"
title="Logout"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H7a2 2 0 01-2-2V7a2 2 0 012-2h4a2 2 0 012 2v1"
/>
</svg>
</button>
</div>
)}
</div> </div>
)} )}
</nav> </nav>
@@ -0,0 +1,43 @@
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
}
// Default hook: ad vissza egy [value, setValue] párt, szinkronizálja localStorage-t és átirányít, ha nincs érték
export default function useRequireAuth({ key = "username", redirectTo = "/login" } = {}) {
const navigate = useNavigate()
const [value, setValue] = useState(() => {
try {
return localStorage.getItem(key)
} catch {
return null
}
})
// Ha nincs érték, átirányítjuk (komponens mount-oláskor)
useEffect(() => {
if (!value) {
navigate(redirectTo)
}
}, [navigate, value, redirectTo])
// 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,46 +1,44 @@
// src/pages/Auth/AuthLogin.jsx // src/pages/Auth/AuthLogin.jsx
// Kártya amelyiken a bejelentkezés és regisztráció van // Kártya amelyiken a bejelentkezés és regisztráció van
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion"
import Animation from "../../assets/SerpentRace_Animation/SerpentRace_Animation"; import Animation from "../../assets/SerpentRace_Animation/SerpentRace_Animation"
import LoginForm from "./LoginForm"; import LoginForm from "./LoginForm"
import RegisterForm from "./RegisterForm"; import RegisterForm from "./RegisterForm"
import Logo from "../../assets/pictures/Logo"; import Logo from "../../assets/pictures/Logo"
export default function AuthCard({ isRegistering, setIsRegistering }) { export default function AuthCard({ isRegistering, setIsRegistering }) {
return ( return (
<motion.div <motion.div
initial={{ height: "auto" }} initial={{ height: "auto" }}
animate={{ height: isRegistering ? "600px" : "385px" }} animate={{ height: isRegistering ? "750px" : "385px" }}
transition={{ duration: 0.5, ease: "easeInOut" }} transition={{ duration: 0.5, ease: "easeInOut" }}
className="absolute flex max-w-4xl w-full bg-white rounded-2xl shadow-lg overflow-hidden" className="absolute flex max-w-4xl w-full bg-white rounded-2xl shadow-lg overflow-hidden"
> >
{/* Bal oldali kép és szöveg */} {/* Bal oldali kép és szöveg */}
<div <div
className={`transition-all duration-500 ${isRegistering ? 'w-0 p-0' : 'w-2/5 p-8'} flex flex-col justify-center items-center bg-gradient-to-r from-mint-darker to-mint text-white `} className={`transition-all duration-500 ${
isRegistering ? "w-0 p-0" : "w-2/5 p-8"
} flex flex-col justify-center items-center bg-gradient-to-r from-mint-darker to-mint text-white `}
> >
<Logo size={100}/> <Logo size={100} />
<div className="h-6" /> <div className="h-6" />
<Animation sizePercentage={30} /> <Animation sizePercentage={30} />
<p className="text-lg mt-0 text-center font-light whitespace-nowrap overflow-hidden"> <p className="text-lg mt-0 text-center font-light whitespace-nowrap overflow-hidden">
Lépj be és légy a legjobb! Lépj be és légy a legjobb!
</p> </p>
</div> </div>
{/* Jobb oldali űrlap */} {/* Jobb oldali űrlap */}
<div className="w-full p-10 relative"> <div className="w-full p-10 relative">
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">{isRegistering ? <RegisterForm /> : <LoginForm />}</AnimatePresence>
{isRegistering ? <RegisterForm /> : <LoginForm />}
</AnimatePresence>
<span <span
className="text-secondary cursor-pointer hover:underline mt-4 block text-center" className="text-secondary cursor-pointer hover:underline mt-4 block text-center"
onClick={() => setIsRegistering(!isRegistering)} onClick={() => setIsRegistering(!isRegistering)}
> >
{isRegistering {isRegistering ? "Már van fiókod? Jelentkezz be itt!" : "Nincs még fiókod? Regisztrálj itt!"}
? "Már van fiókod? Jelentkezz be itt!"
: "Nincs még fiókod? Regisztrálj itt!"}
</span> </span>
</div> </div>
</motion.div> </motion.div>
); )
} }
@@ -1,58 +1,94 @@
// src/pages/Auth/EmailVerification.jsx // src/pages/Auth/EmailVerification.jsx
// Rublikák a kód beírásához, email ellenőrzéshez // Rublikák a kód beírásához, email ellenőrzéshez
import { useState, useRef } from "react"; import { useState, useRef, useEffect } from "react"
import Background from "../../assets/backgrounds/Background"; import Background from "../../assets/backgrounds/Background"
import { motion } from "framer-motion"; import { motion } from "framer-motion"
import Button from "../../components/Buttons/Button"; import Button from "../../components/Buttons/Button"
import { useLocation } from "react-router-dom"
export default function EmailVerification() { export default function EmailVerification() {
const [code, setCode] = useState(Array(6).fill("")); const [code, setCode] = useState(Array(6).fill(""))
const inputRefs = useRef([]); const inputRefs = useRef([])
const location = useLocation()
const [showSuccess, setShowSuccess] = useState(false)
const [error, setError] = useState("")
useEffect(() => {
if (location.state && location.state.success) {
setShowSuccess(true)
setTimeout(() => setShowSuccess(false), 1500)
}
}, [location.state])
const handleChange = (e, index) => { const handleChange = (e, index) => {
const { value } = e.target; const { value } = e.target
if (/^\d*$/.test(value) && value.length <= 1) { if (/^\d*$/.test(value) && value.length <= 1) {
const newCode = [...code]; const newCode = [...code]
newCode[index] = value; newCode[index] = value
setCode(newCode); setCode(newCode)
if (value && index < 5) { if (value && index < 5) {
inputRefs.current[index + 1].focus(); inputRefs.current[index + 1].focus()
} }
} }
}; }
const handleKeyDown = (e, index) => { const handleKeyDown = (e, index) => {
if (e.key === "Backspace" && !code[index] && index > 0) { if (e.key === "Backspace" && !code[index] && index > 0) {
inputRefs.current[index - 1].focus(); inputRefs.current[index - 1].focus()
} else if (e.key === "ArrowLeft" && index > 0) { } else if (e.key === "ArrowLeft" && index > 0) {
inputRefs.current[index - 1].focus(); inputRefs.current[index - 1].focus()
} else if (e.key === "ArrowRight" && index < 5) { } else if (e.key === "ArrowRight" && index < 5) {
inputRefs.current[index + 1].focus(); inputRefs.current[index + 1].focus()
} else if (/^\d$/.test(e.key) && code[index]) { } else if (/^\d$/.test(e.key) && code[index]) {
e.preventDefault(); e.preventDefault()
const newCode = [...code]; const newCode = [...code]
newCode[index] = e.key; newCode[index] = e.key
setCode(newCode); setCode(newCode)
if (index < 5) { if (index < 5) {
setTimeout(() => { setTimeout(() => {
inputRefs.current[index + 1].focus(); inputRefs.current[index + 1].focus()
}, 0); }, 0)
} }
} }
}; }
const handleSubmit = (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault()
console.log("Kód:", code.join("")); setError("")
// Backend API const token = code.join("")
}; if (token.length !== 6) {
setError("A kód 6 számjegyből áll.")
return
}
try {
const res = await fetch(`/verify-email/${token}`)
const data = await res.json()
if (data.success) {
setShowSuccess(true)
setTimeout(() => setShowSuccess(false), 2000)
} else {
setError(data.message || "Sikertelen ellenőrzés.")
}
} catch (err) {
setError("Hiba történt az ellenőrzés során.")
}
}
return ( return (
<div className="relative flex items-center justify-center min-h-screen bg-gray-100 p-0 font-poppins"> <div className="relative flex items-center justify-center min-h-screen bg-gray-100 p-0 font-poppins">
<Background /> <Background />
{showSuccess && (
<div className="fixed top-6 left-1/2 -translate-x-1/2 bg-green-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300">
Sikeres email ellenőrzés!
</div>
)}
{error && (
<div className="fixed top-20 left-1/2 -translate-x-1/2 bg-red-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300">
{error}
</div>
)}
<motion.div <motion.div
initial={{ height: "auto" }} initial={{ height: "auto" }}
animate={{ height: "300px" }} animate={{ height: "300px" }}
@@ -73,7 +109,9 @@ export default function EmailVerification() {
onChange={(e) => handleChange(e, index)} onChange={(e) => handleChange(e, index)}
onKeyDown={(e) => handleKeyDown(e, index)} onKeyDown={(e) => handleKeyDown(e, index)}
ref={(el) => (inputRefs.current[index] = el)} ref={(el) => (inputRefs.current[index] = el)}
className={`w-12 h-12 px-2 py-3 border rounded-lg focus:ring-4 focus:ring-indigo-400 text-gray-700 placeholder-gray-400 bg-gray-50 text-center text-2xl tracking-widest ${!digit ? 'placeholder-opacity-100' : 'placeholder-opacity-0'}`} className={`w-12 h-12 px-2 py-3 border rounded-lg focus:ring-4 focus:ring-indigo-400 text-gray-700 placeholder-gray-400 bg-gray-50 text-center text-2xl tracking-widest ${
!digit ? "placeholder-opacity-100" : "placeholder-opacity-0"
}`}
// nem tudom, hogy hogyan jobb // nem tudom, hogy hogyan jobb
// placeholder="_" // placeholder="_"
maxLength="1" maxLength="1"
@@ -85,5 +123,5 @@ export default function EmailVerification() {
</div> </div>
</motion.div> </motion.div>
</div> </div>
); )
} }
@@ -1,42 +1,72 @@
// src/pages/Auth/LoginForm.jsx // src/pages/Auth/LoginForm.jsx
// Bejelentkezési űrlap // Bejelentkezési űrlap
import InputBox from "../../components/Inputs/InputBox"; import InputBox from "../../components/Inputs/InputBox"
import Button from "../../components/Buttons/Button"; import Button from "../../components/Buttons/Button"
import { motion } from "framer-motion"; import { motion } from "framer-motion"
import { useState } from "react"; import { useState, useEffect } from "react"
import { login } from "../../api/userApi"; import { useLocation, useNavigate } from "react-router-dom"
import { login } from "../../api/userApi"
export default function LoginForm() { export default function LoginForm() {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("")
const [password, setPassword] = useState(""); const [password, setPassword] = useState("")
const [error, setError] = useState(""); const [error, setError] = useState("")
const location = useLocation()
const navigate = useNavigate()
const [showSuccess, setShowSuccess] = useState(false)
const [showErrorPopup, setShowErrorPopup] = useState(false)
useEffect(() => {
if (location.state && location.state.success) {
setShowSuccess(true)
setTimeout(() => setShowSuccess(false), 4000)
}
}, [location.state])
function validateEmail(email) { function validateEmail(email) {
return /\S+@\S+\.\S+/.test(email); return /\S+@\S+\.\S+/.test(email)
} }
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault()
setError(""); setError("")
setShowErrorPopup(false)
if (!email || !password) { if (!email || !password) {
setError("Minden mező kitöltése kötelező."); setError("Minden mező kitöltése kötelező.")
return; setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
return
} }
if (!validateEmail(email)) { if (!validateEmail(email)) {
setError("Hibás email formátum."); setError("Hibás email formátum.")
return; setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
return
} }
// Backend API // Backend API
login(email, password) login(email, password)
.then((data) => { .then((response) => {
console.log(data); console.log(response)
console.log("Bejelentkezés:", { email, password }); // Csak a response.status-t ellenőrizd!
if (response && response.status === 200) {
if (response.data && response.data.user) {
localStorage.setItem("username", response.data.user.username)
localStorage.setItem("authLevel", response.data.user.authLevel)
}
navigate("/home")
} else {
setError("Hibás bejelentkezési adatok.")
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
}
}) })
.catch((error) => { .catch((error) => {
setError("Hibás bejelentkezési adatok."); setError("Hibás bejelentkezési adatok.")
}); setShowErrorPopup(true)
}; setTimeout(() => setShowErrorPopup(false), 2000)
})
}
return ( return (
<motion.div <motion.div
@@ -47,24 +77,31 @@ export default function LoginForm() {
transition={{ duration: 0.25 }} transition={{ duration: 0.25 }}
> >
<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>
{error && ( {showSuccess && (
<div className="mb-4 text-red-600 text-center font-semibold">{error}</div> <div className="fixed top-6 left-1/2 -translate-x-1/2 bg-green-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300">
Sikeres regisztráció! Az email ellenőrzése után be tudsz lépni.
</div>
)}
{showErrorPopup && error && (
<div className="fixed top-6 left-1/2 -translate-x-1/2 bg-red-500 text-white px-6 py-2 rounded shadow-lg z-50 text-center font-semibold transition-opacity duration-300">
{error}
</div>
)} )}
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<InputBox <InputBox
type="email" type="email"
placeholder="Email cím" placeholder="Email cím"
value={email} value={email}
onChange={e => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
/> />
<InputBox <InputBox
type="password" type="password"
placeholder="Jelszó" placeholder="Jelszó"
value={password} value={password}
onChange={e => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
/> />
<Button text="Bejelentkezés" type="submit" /> <Button text="Bejelentkezés" type="submit" />
</form> </form>
</motion.div> </motion.div>
); )
} }
@@ -1,51 +1,77 @@
// src/pages/Auth/RegisterForm.jsx // src/pages/Auth/RegisterForm.jsx
// Regisztrációs űrlap // Regisztrációs űrlap
import InputBox from "../../components/Inputs/InputBox"; import InputBox from "../../components/Inputs/InputBox"
import Button from "../../components/Buttons/Button"; import Button from "../../components/Buttons/Button"
import { motion } from "framer-motion"; import { motion } from "framer-motion"
import { useState } from "react"; import { useState } from "react"
import { register } from "../../api/userApi"; import { register } from "../../api/userApi"
import { useNavigate } from "react-router-dom"
export default function RegisterForm() { export default function RegisterForm() {
const [lastname, setLastname] = useState(""); const [lastname, setLastname] = useState("")
const [firstname, setFirstname] = useState(""); const [firstname, setFirstname] = useState("")
const [username, setUsername] = useState(""); const [username, setUsername] = useState("")
const [email, setEmail] = useState(""); const [email, setEmail] = useState("")
const [password, setPassword] = useState(""); const [password, setPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState("")
const [phone, setPhone] = useState(""); const [phone, setPhone] = useState("")
const [error, setError] = useState(""); const [error, setError] = useState("")
const [showErrorPopup, setShowErrorPopup] = useState(false)
const navigate = useNavigate()
function validateEmail(email) { function validateEmail(email) {
return /\S+@\S+\.\S+/.test(email); return /\S+@\S+\.\S+/.test(email)
} }
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault()
setError(""); setError("")
setShowErrorPopup(false)
if (!lastname || !firstname || !username || !email || !password || !confirmPassword || !phone) { if (!lastname || !firstname || !username || !email || !password || !confirmPassword || !phone) {
setError("Minden mező kitöltése kötelező."); setError("Minden mező kitöltése kötelező.")
return; return
} }
if (!validateEmail(email)) { if (!validateEmail(email)) {
setError("Hibás email formátum."); setError("Hibás email formátum.")
return; return
} }
if (password.length < 6) { if (password.length < 6) {
setError("A jelszónak legalább 6 karakter hosszúnak kell lennie."); setError("A jelszónak legalább 6 karakter hosszúnak kell lennie.")
return; return
} }
if (password !== confirmPassword) { if (password !== confirmPassword) {
setError("A jelszavak nem egyeznek."); setError("A jelszavak nem egyeznek.")
return; return
} }
// Backend API // Backend API
const response = await register(username, email, password, firstname, lastname, phone); try {
console.log(response); const response = await register(username, email, password, firstname, lastname, phone)
console.log("Regisztráció:", { username, email, password, firstname, lastname, phone }); // Check for 201 Created status
}; if (response && response.status === 201) {
navigate("/login", { state: { success: true } })
} else {
let msg = "Sikertelen regisztráció."
if (response && response.data && response.data.error) {
msg = response.data.error
}
setError(msg)
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
}
} catch (err) {
let msg = "Ismeretlen hiba történt."
if (err.response && err.response.data && err.response.data.error) {
msg = err.response.data.error
} else if (err.message) {
msg = err.message
}
setError(msg)
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
}
}
return ( return (
<motion.div <motion.div
@@ -56,54 +82,56 @@ export default function RegisterForm() {
transition={{ duration: 0.25 }} transition={{ duration: 0.25 }}
> >
<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>
{error && ( {showErrorPopup && error && (
<div className="mb-4 text-red-600 text-center font-semibold">{error}</div> <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"> <form onSubmit={handleSubmit} className="space-y-6">
<InputBox <InputBox
type="text" type="text"
placeholder="Vezetéknév" placeholder="Vezetéknév"
value={lastname} value={lastname}
onChange={e => setLastname(e.target.value)} onChange={(e) => setLastname(e.target.value)}
/> />
<InputBox <InputBox
type="text" type="text"
placeholder="Keresztnév" placeholder="Keresztnév"
value={firstname} value={firstname}
onChange={e => setFirstname(e.target.value)} onChange={(e) => setFirstname(e.target.value)}
/> />
<InputBox <InputBox
type="text" type="text"
placeholder="Felhasználónév" placeholder="Felhasználónév"
value={username} value={username}
onChange={e => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
/> />
<InputBox <InputBox
type="email" type="email"
placeholder="Email cím" placeholder="Email cím"
value={email} value={email}
onChange={e => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
/> />
<InputBox <InputBox
type="phone" type="phone"
placeholder="Telefonszám" placeholder="Telefonszám"
value={phone} value={phone}
onChange={e => setPhone(e.target.value)} onChange={(e) => setPhone(e.target.value)}
/> />
<InputBox <InputBox
type="password" type="password"
placeholder="Jelszó" placeholder="Jelszó"
value={password} value={password}
onChange={e => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
/> />
<InputBox <InputBox
type="password" type="password"
placeholder="Jelszó megerősítése" placeholder="Jelszó megerősítése"
value={confirmPassword} value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
/> />
<Button text="Regisztráció" type="submit" /> <Button text="Regisztráció" type="submit" />
</form> </form>
</motion.div> </motion.div>
); )
} }
@@ -7,6 +7,7 @@ import Navbar from "../../components/Navbar/Navbar.jsx"
import DeckHeader from "../../components/DeckCreator/DeckHeader.jsx" import DeckHeader from "../../components/DeckCreator/DeckHeader.jsx"
import CardsList from "../../components/DeckCreator/CardsList.jsx" import CardsList from "../../components/DeckCreator/CardsList.jsx"
import CardEditor from "../../components/DeckCreator/CardEditor.jsx" import CardEditor from "../../components/DeckCreator/CardEditor.jsx"
import { createDeck } from '../../api/deckApi'
export default function DeckCreator() { export default function DeckCreator() {
const { deckId } = useParams() // URL-ből deck ID (új deck esetén undefined) const { deckId } = useParams() // URL-ből deck ID (új deck esetén undefined)
@@ -86,12 +87,23 @@ export default function DeckCreator() {
const handleSaveDeck = async () => { const handleSaveDeck = async () => {
try { try {
// TODO: API mentés
console.log("Deck mentése:", deck) console.log("Deck mentése:", deck)
alert("✅ Deck sikeresen mentve!")
const payload = {
name: deck.name,
type: (deck.type || 'Question').toString().toUpperCase(),
ctype: deck.privacy === 'public' ? 'PUBLIC' : 'PRIVATE',
description: deck.description || '',
cards: deck.cards.map(c => ({ ...c, text: c.question || c.statement || c.text || '' }))
}
const saved = await createDeck(payload)
setDeck(prev => ({ ...prev, id: saved.id ?? prev.id, creationdate: saved.creationdate ?? prev.creationdate, updatedate: saved.updatedate ?? prev.updatedate }))
console.log('Deck saved (backend):', saved)
alert('✅ Deck sikeresen mentve!')
} catch (error) { } catch (error) {
console.error("Mentési hiba:", error) console.error('Mentési hiba:', error)
alert("❌ Hiba történt a mentés során!") alert('❌ Hiba történt a mentés során: ' + (error?.response?.data?.error || error.message || String(error)))
} }
} }
@@ -1,7 +1,7 @@
// src/pages/Decks/DeckManagerPage.jsx // src/pages/Decks/DeckManagerPage.jsx
// Deck Management Page (with Navbar, no Footer) // Deck Management Page (with Navbar, no Footer)
import DeckManager from "../../components/Landingpage/DeckManager.jsx" import DeckManager from "../../components/DeckCreator/DeckManager.jsx"
import Navbar from "../../components/Navbar/Navbar.jsx" import Navbar from "../../components/Navbar/Navbar.jsx"
export default function DeckManagerPage() { export default function DeckManagerPage() {
@@ -1,35 +0,0 @@
// 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-[64px]">
<PlayMenu onJoinGame={handleJoinGame} onCreateGame={handleCreateGame} user={user} />
{/* Ide jöhetnek további szekciók, ha szeretnél még tartalmat */}
</main>
<Footer />
</div>
)
}
@@ -1,13 +1,17 @@
// src/pages/Home/Home.jsx // src/pages/Home/Home.jsx
// Régi PlayMenu-s oldal, "Home" néven // Régi PlayMenu-s oldal, "Home" néven
import { useState } from "react" import { useEffect } from "react"
import Navbar from "../../components/Navbar/Navbar.jsx" import useRequireAuth from "../../hooks/useRequireAuth"
import Navbar from "../../components/Navbar/Navbar"
import Footer from "../../components/Footer/Footer.jsx" import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx" import Background from "../../assets/backgrounds/Background.jsx"
import PlayMenu from "../../components/Landingpage/PlayMenu.jsx" import PlayMenu from "../../components/Landingpage/PlayMenu.jsx"
export default function Home() { export default function Home() {
// a hook inicializálja a user-t a localStorage-ból és visszaadja a state-et + settert
const [user, setUser] = useRequireAuth()
// Dummy callbackok és user példa // Dummy callbackok és user példa
const handleJoinGame = (code) => { const handleJoinGame = (code) => {
alert(`Csatlakozás játékhoz: ${code}`) alert(`Csatlakozás játékhoz: ${code}`)
@@ -15,7 +19,9 @@ export default function Home() {
const handleCreateGame = () => { const handleCreateGame = () => {
alert("Új játék létrehozása") alert("Új játék létrehozása")
} }
const user = { name: "Teszt Elek" } const userObj = { name: user }
// ha szükséges a user módosítása máshol: setUser("újnév") automatikusan menti localStorage-be
return ( return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden"> <div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
@@ -26,7 +32,7 @@ export default function Home() {
<Navbar /> <Navbar />
</div> </div>
<main className="flex-1 flex flex-col items-center justify-start py-15 min-h-0 mt-[64px]"> <main className="flex-1 flex flex-col items-center justify-start py-15 min-h-0 mt-[64px]">
<PlayMenu onJoinGame={handleJoinGame} onCreateGame={handleCreateGame} user={user} /> <PlayMenu onJoinGame={handleJoinGame} onCreateGame={handleCreateGame} user={userObj} />
{/* Ide jöhetnek további szekciók, ha szeretnél még tartalmat */} {/* Ide jöhetnek további szekciók, ha szeretnél még tartalmat */}
</main> </main>
<Footer /> <Footer />
@@ -1,23 +1,23 @@
// src/pages/Landing/Landingpage.jsx // src/pages/Landing/Landingpage.jsx
// Főoldal - Landing Page // Főoldal - Landing Page
import { useState } from "react"
import { useNavigate } from "react-router-dom"
import Navbar from "../../components/Navbar/Navbar" import Navbar from "../../components/Navbar/Navbar"
import Footer from "../../components/Footer/Footer.jsx" import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx" import Background from "../../assets/backgrounds/Background.jsx"
import LandingPage from "../../components/Landingpage/LandingPage.jsx" import LandingPage from "../../components/Landingpage/LandingPage.jsx"
export default function LandingPageMain() { export default function LandingPageMain() {
// Navigációs callbackok const navigate = useNavigate();
const handleNavigateToPlay = () => { const handleNavigateToPlay = () => {
// Itt lehet navigálni a játék oldalra navigate("/login");
alert("Navigáció a játék oldalra") };
}
const handleNavigateToAuth = () => { const handleNavigateToAuth = () => {
// Itt lehet navigálni a bejelentkezés oldalra navigate("/register");
alert("Navigáció a bejelentkezés oldalra") };
}
return ( return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden"> <div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
@@ -32,5 +32,5 @@ export default function LandingPageMain() {
</main> </main>
<Footer /> <Footer />
</div> </div>
) );
} }
@@ -3,8 +3,12 @@ import { useState } from "react"
import Navbar from "../../components/Navbar/Navbar.jsx" import Navbar from "../../components/Navbar/Navbar.jsx"
import Footer from "../../components/Footer/Footer.jsx" import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx" import Background from "../../assets/backgrounds/Background.jsx"
import { getUserStats } from "../../api/userApi.js"
import useRequireAuth from "../../hooks/useRequireAuth.jsx"
export default function Reports() { export default function Reports() {
const [username] = useRequireAuth({ key: "username", redirectTo: "/login" })
return ( return (
<div className="w-full min-h-screen flex flex-col relative overflow-x-hidden"> <div className="w-full min-h-screen flex flex-col relative overflow-x-hidden">
{/* Háttér */} {/* Háttér */}
@@ -23,9 +27,8 @@ export default function Reports() {
{/* Fejléc */} {/* Fejléc */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<h2 className="text-3xl font-bold text-white">Játék Riportok</h2> <h2 className="text-3xl font-bold text-white">Játék Riportok</h2>
<p className="text-gray-300 mt-2"> <p className="text-gray-300 mt-2">Áttekintés a legutóbbi játékokról és statisztikákról</p>
Áttekintés a legutóbbi játékokról és statisztikákról {username && <p className="text-sm text-gray-400 mt-1">Bejelentkezett: {username}</p>}
</p>
</div> </div>
{/* Statisztikai kártyák */} {/* Statisztikai kártyák */}