1 Commits

Author SHA1 Message Date
Walke d23328763b footer fix meg landingon 2025-10-15 15:24:06 +02:00
34 changed files with 400 additions and 795 deletions
-7
View File
@@ -32,10 +32,3 @@ 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
+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 #!/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 */
/** /**
@@ -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;
@@ -15,7 +15,6 @@ export interface ShortDeckDto {
type: number; type: number;
playedNumber: number; playedNumber: number;
ctype: number; ctype: number;
cardsCount: number;
} }
export interface DetailDeckDto { export interface DetailDeckDto {
@@ -9,7 +9,6 @@ export class DeckMapper {
type: deck.type, type: deck.type,
playedNumber: deck.playedNumber, playedNumber: deck.playedNumber,
ctype: deck.ctype, ctype: deck.ctype,
cardsCount: deck.cards.length,
}; };
} }
@@ -27,13 +26,6 @@ export class DeckMapper {
} }
static toShortDtoList(decks: DeckAggregate[]): ShortDeckDto[] { static toShortDtoList(decks: DeckAggregate[]): ShortDeckDto[] {
return decks.map(deck => ({ return decks.map(this.toShortDto);
id: deck.id,
name: deck.name,
type: deck.type,
playedNumber: deck.playedNumber,
ctype: deck.ctype,
cardsCount: deck.cards.length,
}));
} }
} }
@@ -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, type: 'auth' | 'refresh'): string | null { function extractToken(req: Request): string | null {
// First try to get token from cookie // First try to get token from cookie
const cookieToken = req.cookies[`${type}_token`]; const cookieToken = req.cookies['auth_token'];
if (cookieToken) { if (cookieToken) {
return 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) { export async function authRequired(req: Request, res: Response, next: NextFunction) {
try { try {
// Extract token from request // Extract token from request
const token = extractToken(req, "auth"); const token = extractToken(req);
const refreshToken = extractToken(req, "refresh"); if (!token) {
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',
@@ -96,9 +95,8 @@ 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, "auth"); const token = extractToken(req);
const refreshToken = extractToken(req, "refresh"); if (!token) {
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,7 +281,9 @@ 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);
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; return true;
+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.hu EMAIL_FROM=noreply@serpentrace.com
# CHAT SYSTEM CONFIGURATION # CHAT SYSTEM CONFIGURATION
CHAT_INACTIVITY_TIMEOUT_MINUTES=30 CHAT_INACTIVITY_TIMEOUT_MINUTES=30
+3 -3
View File
@@ -1,10 +1,10 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SerpentRace</title> <title>Vite + React</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
-2
View File
@@ -15,7 +15,6 @@ import About from "./pages/About/About"
import ScrollToTop from "./components/ScrollToTop" import ScrollToTop from "./components/ScrollToTop"
import GameScreen from "./pages/Game/GameScreen" import GameScreen from "./pages/Game/GameScreen"
import Reports from "./pages/Report/Reports" import Reports from "./pages/Report/Reports"
import Lobby from "./pages/Lobby/Lobby"
function App() { function App() {
const [isMobile, setIsMobile] = useState(false) const [isMobile, setIsMobile] = useState(false)
@@ -47,7 +46,6 @@ function App() {
<Router> <Router>
<Routes> <Routes>
<Route path="/about" element={<About />} /> <Route path="/about" element={<About />} />
<Route path="/lobby" element={<Lobby />} />
<Route path="/register" element={<AuthRegister />} /> <Route path="/register" element={<AuthRegister />} />
<Route path="/login" element={<AuthLogin />} /> <Route path="/login" element={<AuthLogin />} />
<Route path="/verify-email" element={<EmailVerification />} /> <Route path="/verify-email" element={<EmailVerification />} />
-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" import axios from "axios"
export const API_CONFIG = { export const API_CONFIG = {
baseURL: (import.meta.env.VITE_API_URL ? import.meta.env.VITE_API_URL : "") + "/api", baseURL: import.meta.env.VITE_API_URL + "/api",
wsURL: "http://localhost:3000", wsURL: "http://localhost:3000",
timeout: 10000, timeout: 10000,
retryAttempts: 3, retryAttempts: 3,
} }
export const apiClient = axios.create({ 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
@@ -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 //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 return response.data
} catch (error) { } catch (error) {
throw error throw error
} }
@@ -30,16 +58,16 @@ export const login = async (username, password) => {
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 return { ...response.data, status: response.status }
} catch (error) { } catch (error) {
throw error throw error
} }
} }
// Get current user's game statistics //verify email
export const getUserStats = async () => { export const verifyEmail = async (token) => {
try { try {
const response = await apiClient.get("/users/me/stats") const response = await apiClient.get(`/users/verify-email/${token}`)
return response.data return response.data
} catch (error) { } catch (error) {
throw error throw error
@@ -1,77 +1,62 @@
import React, { useEffect, useRef, useState } from "react" import React 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 = () => {
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 = () => { const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" }) window.scrollTo({ top: 0, behavior: "smooth" })
} }
return ( return (
<footer <footer
ref={footerRef} className="relative bg-zinc-900 text-white border-t-2 border-zinc-800 mt-auto py-8"
className="relative bg-zinc-900 text-zinc-400 border-t-2 border-zinc-800 mt-auto py-8"
style={{ transformOrigin: "bottom center" }} style={{ transformOrigin: "bottom center" }}
> >
<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"> <div className="flex flex-col items-center footer-animate">
<a href="/" className="hover:scale-105 hover:brightness-110"> <a 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"> <div className="flex flex-col gap-1 footer-animate">
<span className="text-lg font-semibold text-green-600 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-500"> <a href="/" className="hover:underline hover:text-green-400 transition">
Főoldal Főoldal
</a> </a>
<a href="/about" className="hover:underline hover:text-green-500"> <a href="/about" className="hover:underline hover:text-green-400 transition">
Rólunk Rólunk
</a> </a>
<a href="/contact" className="hover:underline hover:text-green-500"> <a href="/contact" className="hover:underline hover:text-green-400 transition">
Kapcsolat Kapcsolat
</a> </a>
</div> </div>
{/* Közösség */} {/* Közösség */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1 footer-animate">
<span className="text-lg font-semibold text-green-600 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 <a
href="https://discord.gg/" href="https://discord.gg/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="hover:underline hover:text-green-500" className="hover:underline hover:text-green-400 transition"
> >
Discord Discord
</a> </a>
@@ -79,36 +64,34 @@ const Footer = () => {
href="https://github.com/" href="https://github.com/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="hover:underline hover:text-green-500" className="hover:underline hover:text-green-400 transition"
> >
GitHub GitHub
</a> </a>
</div> </div>
{/* Elérhetőség */} {/* Elérhetőség */}
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1 footer-animate">
<span className="text-lg font-semibold text-green-600 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>
<span className="opacity-85">Email: info@serpentrace.hu</span> <span className="opacity-80">Email: info@serpentrace.hu</span>
<span className="opacity-85">Telefon: +36 30 123 4567</span> <span className="opacity-80">Telefon: +36 30 123 4567</span>
</div> </div>
</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. © {new Date().getFullYear()} SerpentRace. Minden jog fenntartva.
</div> </div>
{/* Scroll to top */} {/* Scroll to top */}
{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 /> </button>
</button>
)}
</footer> </footer>
) )
} }
@@ -7,7 +7,7 @@ export default function InputBox({ type, placeholder, value, onChange, width })
return ( return (
<input <input
type={type} type={type}
className={`${widthClass} py-3 px-4 border border-battleship-gray rounded-lg focus:border-mint-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} placeholder={placeholder}
value={value} value={value}
onChange={onChange} onChange={onChange}
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react" import React, { useState } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { import {
FaPlus, FaPlus,
@@ -20,7 +20,19 @@ const deckTypes = [
{ label: "Fun", 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"] const origins = ["Mind", "Vállalati", "Saját"]
@@ -63,46 +75,16 @@ 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")
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [showSortHelp, setShowSortHelp] = useState(false) const [showSortHelp, setShowSortHelp] = useState(false)
const [selectedDeck, setSelectedDeck] = useState(null) const [selectedDeck, setSelectedDeck] = useState(null)
const [decks, setDecks] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => { // Filter logic (mock)
let mounted = true let filteredDecks = mockDecks.filter((deck) => {
const load = async () => {
setLoading(true)
try {
const result = await import('../../api/deckApi').then(m => m.getDecksPage(0, 49))
if (!mounted) return
// map backend deck shape to UI shape
const mapped = result.decks.map(d => ({
id: d.id,
name: d.name,
type: d.type === 2 ? 'Question' : d.type === 1 ? 'Joker' : 'Luck',
created: d.creationdate ? new Date(d.creationdate).toLocaleDateString() : '',
origin: d.ctype === 2 ? 'Vállalati' : d.ctype === 0 ? 'Mind' : 'Saját',
raw: d
}))
setDecks(mapped)
} catch (err) {
console.error('Failed to load decks', err)
} finally {
setLoading(false)
}
}
load()
return () => { mounted = false }
}, [])
// Filter logic
const sourceDecks = decks
let filteredDecks = sourceDecks.filter((deck) => {
const typeMatch = selectedType === "All" || deck.type === selectedType const typeMatch = selectedType === "All" || deck.type === selectedType
const originMatch = selectedOrigin === "Mind" || deck.origin === selectedOrigin const originMatch = selectedOrigin === "Mind" || deck.origin === selectedOrigin
const searchMatch = !search || deck.name.toLowerCase().includes(search.toLowerCase()) const searchMatch = !search || deck.name.toLowerCase().includes(search.toLowerCase())
@@ -125,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-[1200px] mx-auto px-4 py-10"> <div className="w-full max-w-6xl mx-auto px-4 py-10">
{/* Filters */} {/* 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"> <div className="flex gap-2 items-center w-full md:w-auto">
<SearchBox <SearchBox
value={search} value={search}
@@ -266,21 +248,15 @@ 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" />
<span className="text-[color:var(--color-text)] font-semibold">Új pakli létrehozása</span> <span className="text-[color:var(--color-text)] font-semibold">Új pakli létrehozása</span>
</div> </div>
{/* Existing Decks (from backend) */} {/* Existing Decks (Mockup) */}
{loading && ( {filteredDecks.map((deck) => {
<div className="col-span-full text-center text-[color:var(--color-text-muted)]">Betöltés...</div>
)}
{!loading && filteredDecks.length === 0 && (
<div className="col-span-full text-center text-[color:var(--color-text-muted)]">Nincsenek mentett paklik.</div>
)}
{!loading && filteredDecks.map((deck) => {
const deckType = deckTypes.find((t) => t.label === deck.type) const deckType = deckTypes.find((t) => t.label === deck.type)
const borderColor = deckType ? deckType.color : "var(--color-success)" const borderColor = deckType ? deckType.color : "var(--color-success)"
return ( return (
@@ -318,9 +294,14 @@ const DeckManager = () => {
})} })}
</div> </div>
</div> </div>
{/* Deck Info Popup */} {/* Deck Info Popup */}
{selectedDeck && <DeckInfoPopUp deck={selectedDeck} onClose={() => setSelectedDeck(null)} />} {selectedDeck && (
<DeckInfoPopUp
deck={selectedDeck}
onClose={() => setSelectedDeck(null)}
/>
)}
</div> </div>
) )
} }
@@ -5,13 +5,14 @@ import logoImg from "../../assets/pictures/Logo.png"
import ButtonGreen from "../Buttons/ButtonGreen.jsx" import ButtonGreen from "../Buttons/ButtonGreen.jsx"
import { FaUsers, FaPaintBrush, FaHeadset } from "react-icons/fa" import { FaUsers, FaPaintBrush, FaHeadset } from "react-icons/fa"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import { isAuthenticated } from "../../hooks/useRequireAuth" // <-- added import
import { useNavigate } from "react-router-dom" // <-- NEW
const LandingPage = ({ onNavigateToPlay, onNavigateToAuth }) => { import { useNavigate } from "react-router-dom"
const auth = isAuthenticated() // <-- check without redirect
const navigate = useNavigate() // <-- NEW
const LandingPage = ({ onNavigateToPlay }) => {
const navigate = useNavigate()
const handleNavigateToAuth = () => {
navigate("/register")
}
return ( return (
<div className="w-full"> <div className="w-full">
{/* Hero Section */} {/* Hero Section */}
@@ -60,15 +61,8 @@ 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 }}
> >
{/* If not authenticated show Login/Register; if authenticated show Home button */} <ButtonGreen text="Játék" onClick={onNavigateToPlay} width="w-60" />
{!auth ? ( <ButtonGreen text="Regisztráció" onClick={handleNavigateToAuth} width="w-60" />
<>
<ButtonGreen text="Bejelntekezés" onClick={onNavigateToPlay} width="w-60" />
<ButtonGreen text="Regisztráció" onClick={onNavigateToAuth} width="w-60" />
</>
) : (
<ButtonGreen text="Játék" onClick={() => navigate("/home")} width="w-60" />
)}
</motion.div> </motion.div>
</div> </div>
</motion.section> </motion.section>
@@ -177,7 +171,7 @@ const LandingPage = ({ onNavigateToPlay, onNavigateToAuth }) => {
<ButtonGreen <ButtonGreen
text="Kapcsolatfelvétel" text="Kapcsolatfelvétel"
onClick={onNavigateToAuth} onClick={handleNavigateToAuth}
className="px-12 py-4 text-xl font-bold" className="px-12 py-4 text-xl font-bold"
/> />
</motion.div> </motion.div>
@@ -4,14 +4,9 @@ import logoImg from "../../assets/pictures/Logo.png" // <-- EZT ADD HOZZÁ
import ButtonDark from "../Buttons/ButtonDark.jsx" import ButtonDark from "../Buttons/ButtonDark.jsx"
import InputBoxDark from "../Inputs/InputBoxDark.jsx" import InputBoxDark from "../Inputs/InputBoxDark.jsx"
const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => { const PlayMenu = ({ onJoinGame, onCreateGame, user }) => {
const [joinCode, setJoinCode] = useState("") const [joinCode, setJoinCode] = useState("")
const [error, setError] = useState("") const [error, setError] = useState("")
const [guestName, setGuestName] = useState("")
const [guestError, setGuestError] = useState("")
// gyors username kiolvasás (ha a parent objektum user={ { name: ... } } küldi)
const username = user?.name ?? null
const handleJoin = () => { const handleJoin = () => {
if (!joinCode.trim()) { if (!joinCode.trim()) {
@@ -26,19 +21,9 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
onCreateGame() onCreateGame()
} }
// egyszerű segéd a kezdobetűk kinyerésére
const initials = username
? username
.split(" ")
.map((s) => s[0])
.join("")
.slice(0, 2)
.toUpperCase()
: ""
return ( return (
<section <section
className="w-[95%] max-w-6xl mx-auto my-16 flex flex-col md:flex-row items-center justify-center rounded-3xl shadow-2xl 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={{ 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%)",
}} }}
@@ -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"> <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="420px" containerHeight="450px"
containerWidth="420px" containerWidth="450px"
imageHeight="420px" imageHeight="450px"
imageWidth="420px" imageWidth="450px"
rotateAmplitude={7} rotateAmplitude={7}
scaleOnHover={1.03} scaleOnHover={1.03}
showMobileWarning={false} showMobileWarning={false}
@@ -58,49 +43,12 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
displayOverlayContent={false} displayOverlayContent={false}
/> />
</div> </div>
{/* Jobb oldali panel */} {/* Jobb oldali panel */}
<div className="flex-1 w-full flex items-center justify-center px-6 md:px-12 py-8"> <div className="flex-1 w-full flex flex-col items-center justify-center px-4 md:px-12 py-10">
<div <div className="w-full max-w-md rounded-2xl p-8 flex flex-col gap-8">
className="w-full max-w-md rounded-2xl p-6 md:p-8 flex flex-col gap-6"
style={{ background: "rgba(0,0,0,0.15)", backdropFilter: "blur(6px)" }}
>
<div className="flex items-center justify-between">
{username ? (
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold"
style={{ background: "rgba(34,197,94,0.12)", color: "var(--color-mint)" }}
>
{initials}
</div>
<div className="text-[32px]" style={{ color: "var(--color-muted, #cbd5e1)" }}>
<span className="font-medium" style={{ color: "var(--color-text, #fff)" }}>
{username}
</span>
</div>
</div>
) : (
<div className="w-full">
<div className="font-semibold mb-3 text-text">Nincs bejelentkezve játssz vendégként:</div>
<InputBoxDark
type="text"
placeholder="Nickname..."
value={guestName}
onChange={(e) => {
setGuestName(e.target.value)
setGuestError("")
}}
width="w-full"
/>
{guestError && <div className="text-xs mt-2 text-error">{guestError}</div>}
</div>
)}
</div>
<div> <div>
<h2 className="font-semibold mb-3 text-text">Csatlakozás játékhoz</h2> <h2 className="text-lg font-semibold mb-2 text-text">Csatlakozás játékhoz</h2>
<div className={`${error ? "border border-error rounded-lg p-2" : ""}`}> <div className={`${error ? "border border-error rounded-lg" : ""}`}>
<InputBoxDark <InputBoxDark
type="text" type="text"
placeholder="Játék kódja" placeholder="Játék kódja"
@@ -109,21 +57,17 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
width="w-full" width="w-full"
/> />
</div> </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"> <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>
{username ? ( {user && (
<div className="border-t border-white/10 pt-4"> <div>
{username && ( <h2 className="text-lg font-semibold mb-2 text-text">Új játék létrehozása</h2>
<div> <ButtonDark text="Játék létrehozása" type="button" onClick={handleCreate} width="w-full" />
<h3 className="font-semibold mb-3 text-text">Új játék létrehozása</h3>
<ButtonDark text="Játék létrehozása" type="button" onClick={handleCreate} width="w-full" />
</div>
)}
</div> </div>
) : null} )}
</div> </div>
</div> </div>
</section> </section>
@@ -1,90 +1,41 @@
import React, { useState } from "react" import React, { useState } from "react"
import Logo from "../../assets/pictures/Logo" 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 navLinkClass = "px-3 py-2 rounded-lg text-white transition-all duration-200 hover:bg-white/10"
const navLinkClassPlay =
"px-4 py-2 rounded-lg text-white bg-white/12 hover:bg-white/20 transition-all duration-200"
const Navbar = () => { const Navbar = () => {
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
const navigate = useNavigate()
// 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">
<Link to="/" className="flex items-center mt-1 h-9"> <a href="/" className="flex items-center mt-1 h-9">
<Logo size={36} /> <Logo size={36} />
</Link> </a>
<Link to="/" className="flex items-center h-9 text-white font-bold text-2xl tracking-tight"> <a href="/" className="flex items-center h-9 text-white font-bold text-2xl tracking-tight">
SerpentRace SerpentRace
</Link> </a>
</div> </div>
{/* Desktop Menu */} {/* Desktop Menu */}
<div className="hidden md:flex space-x-8 items-center"> <div className="hidden md:flex space-x-8">
{isLoggedIn ? ( <a href="/home" className={navLinkClass}>
<> Home
<Link to="/home" className={navLinkClass}> </a>
Home <a href="/report" className={navLinkClass}>
</Link> Stats
<Link to="/decks" className={navLinkClass}> </a>
Decks <a href="/about" className={navLinkClass}>
</Link>
<Link to="/report" className={navLinkClass}>
Stats
</Link>
</>
) : (
<Link to="/" className={navLinkClass}>
Home
</Link>
)}
<Link to="/about" className={navLinkClass}>
About About
</Link> </a>
<Link to="/companies" className={navLinkClass}> <a href="/companies" className={navLinkClass}>
Contact Contact
</Link> </a>
{!isLoggedIn && (
<Link to="/home" className={navLinkClassPlay}>
Play
</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">
@@ -118,61 +69,18 @@ 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">
{isLoggedIn ? ( <a href="#" className={navLinkClass}>
<Link to="/home" className={navLinkClass}> Home
Home </a>
</Link> <a href="#" className={navLinkClass}>
) : (
<Link to="/" className={navLinkClass}>
Home
</Link>
)}
<Link to="/leaderboard" className={navLinkClass}>
Leaderboard Leaderboard
</Link> </a>
<Link to="/about" className={navLinkClass}> <a href="#" className={navLinkClass}>
About About
</Link> </a>
<Link to="/companies" className={navLinkClass}> <a href="#" className={navLinkClass}>
Contact Contact
</Link> </a>
{!isLoggedIn && (
<div className="px-2">
<button
onClick={() => {
setMenuOpen(false)
navigate("/home")
}}
className="w-full text-left px-3 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-all"
>
Játék
</button>
</div>
)}
{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>
@@ -14,10 +14,6 @@ import {
export default function DeckInfoPopUp({ deck, onClose }) { export default function DeckInfoPopUp({ deck, onClose }) {
if (!deck) return null 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 // Scroll blokkolás amikor a popup nyitva van
useEffect(() => { useEffect(() => {
// Scroll letiltása // Scroll letiltása
@@ -37,25 +33,13 @@ export default function DeckInfoPopUp({ deck, onClose }) {
const currentDeckType = deckTypes[deck.type] || { label: deck.type, color: "var(--color-success)" } const currentDeckType = deckTypes[deck.type] || { label: deck.type, color: "var(--color-success)" }
// Use real deck data with safe fallbacks // Mock data - ezeket majd a backend adatokra cseréljük
const creator = deck.creatorName || deck.creator || (deck.user && deck.user.name) || "Ismeretlen"
const privacy = deck.origin === "Vállalati" || deck.ctype === 1 || deck.privacy === 'public' ? "Publikus" : "Privát"
// Get data from raw if available
const rawData = deck.raw || deck
// Use played number from raw data for answers count
const questionsCount = rawData.cardCount || 0
const answersCount = rawData.playedNumber || 0
console.log('Calculated counts:', { questionsCount, answersCount, rawData })
const mockData = { const mockData = {
...deck, creator: "John Doe",
creator, privacy: deck.origin === "Vállalati" ? "Publikus" : "Privát",
privacy, questionsCount: Math.floor(Math.random() * 50) + 10,
questionsCount, answersCount: Math.floor(Math.random() * 200) + 50,
answersCount ...deck
} }
const formatDate = (dateString) => { const formatDate = (dateString) => {
@@ -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]
}
-4
View File
@@ -8,13 +8,9 @@
--color-eerie-black: #181d23; --color-eerie-black: #181d23;
--color-mint: #15803d; --color-mint: #15803d;
--color-mint-dark: #136636; --color-mint-dark: #136636;
--color-mint-darker: #11522b; --color-mint-darker: #11522b;
--color-mint-light: #16a34a;
--color-mint-lighter: #22c55e;
/* Gombok */ /* Gombok */
--color-button-primary: #16a34a; --color-button-primary: #16a34a;
--color-button-primary-hover: #15803d; --color-button-primary-hover: #15803d;
@@ -5,7 +5,7 @@ import InputBox from "../../components/Inputs/InputBox"
import Button from "../../components/Buttons/Button" import Button from "../../components/Buttons/Button"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { useLocation, useNavigate } from "react-router-dom" import { useLocation } from "react-router-dom"
import { login } from "../../api/userApi" import { login } from "../../api/userApi"
export default function LoginForm() { export default function LoginForm() {
@@ -13,9 +13,7 @@ export default function LoginForm() {
const [password, setPassword] = useState("") const [password, setPassword] = useState("")
const [error, setError] = useState("") const [error, setError] = useState("")
const location = useLocation() const location = useLocation()
const navigate = useNavigate()
const [showSuccess, setShowSuccess] = useState(false) const [showSuccess, setShowSuccess] = useState(false)
const [showErrorPopup, setShowErrorPopup] = useState(false)
useEffect(() => { useEffect(() => {
if (location.state && location.state.success) { if (location.state && location.state.success) {
@@ -31,40 +29,22 @@ export default function LoginForm() {
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ő.")
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
return return
} }
if (!validateEmail(email)) { if (!validateEmail(email)) {
setError("Hibás email formátum.") setError("Hibás email formátum.")
setShowErrorPopup(true)
setTimeout(() => setShowErrorPopup(false), 2000)
return return
} }
// Backend API // Backend API
login(email, password) login(email, password)
.then((response) => { .then((data) => {
console.log(response) console.log(data)
// Csak a response.status-t ellenőrizd! console.log("Bejelentkezés:", { email, password })
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)
}) })
} }
@@ -82,11 +62,7 @@ export default function LoginForm() {
Sikeres regisztráció! Az email ellenőrzése után be tudsz lépni. Sikeres regisztráció! Az email ellenőrzése után be tudsz lépni.
</div> </div>
)} )}
{showErrorPopup && error && ( {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="email" type="email"
@@ -6,7 +6,7 @@ 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, useLocation } from "react-router-dom" import { useNavigate } from "react-router-dom"
export default function RegisterForm() { export default function RegisterForm() {
const [lastname, setLastname] = useState("") const [lastname, setLastname] = useState("")
@@ -19,7 +19,6 @@ export default function RegisterForm() {
const [error, setError] = useState("") const [error, setError] = useState("")
const [showErrorPopup, setShowErrorPopup] = useState(false) const [showErrorPopup, setShowErrorPopup] = useState(false)
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation()
function validateEmail(email) { function validateEmail(email) {
return /\S+@\S+\.\S+/.test(email) return /\S+@\S+\.\S+/.test(email)
@@ -51,19 +50,13 @@ export default function RegisterForm() {
const response = await register(username, email, password, firstname, lastname, phone) const response = await register(username, email, password, firstname, lastname, phone)
// Check for 201 Created status // Check for 201 Created status
if (response && response.status === 201) { if (response && response.status === 201) {
// Ha már a /login útvonalon van a user, a sima navigate nem biztos, hogy újraindítja a komponenst. navigate("/login", { state: { success: true } })
// Ilyenkor előbb beállítjuk a state-et, majd kényszerítünk egy teljes oldalletöltést. console.log(response)
if (location.pathname === "/login") { console.log("Regisztráció:", { username, email, password, firstname, lastname, phone })
navigate("/login", { state: { success: true } })
// teljes újratöltés, hogy a login oldal újra feldolgozza a state-et
window.location.reload()
} else {
navigate("/login", { state: { success: true } })
}
} else { } else {
let msg = "Sikertelen regisztráció." let msg = "Sikertelen regisztráció."
if (response && response.data && response.data.error) { if (response && response.error) {
msg = response.data.error msg = response.error
} }
setError(msg) setError(msg)
setShowErrorPopup(true) setShowErrorPopup(true)
@@ -7,7 +7,6 @@ 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)
@@ -87,23 +86,12 @@ 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: ' + (error?.response?.data?.error || error.message || String(error))) alert("❌ Hiba történt a mentés során!")
} }
} }
@@ -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/DeckCreator/DeckManager.jsx" import DeckManager from "../../components/Landingpage/DeckManager.jsx"
import Navbar from "../../components/Navbar/Navbar.jsx" import Navbar from "../../components/Navbar/Navbar.jsx"
export default function DeckManagerPage() { export default function DeckManagerPage() {
@@ -66,17 +66,12 @@ const GameScreen = () => {
{ id: 3, name: "Fürtös", position: 68, score: 14, color: "bg-yellow-600", emoji: "😂" }, { id: 3, name: "Fürtös", position: 68, score: 14, color: "bg-yellow-600", emoji: "😂" },
]) ])
// New: selected dice value from dropdown (null = none)
const [selectedDice, setSelectedDice] = useState(null)
// Sort players by position in descending order // Sort players by position in descending order
const sortedPlayers = [...players].sort((a, b) => b.position - a.position) const sortedPlayers = [...players].sort((a, b) => b.position - a.position)
// Handle dice roll completion // Handle dice roll
const handleDiceRoll = (value) => { const handleDiceRoll = (value) => {
console.log("Rolled:", value) console.log("Rolled:", value)
// reset dropdown selection after roll
setSelectedDice(null)
// You can add logic here to move the current player based on the dice value // You can add logic here to move the current player based on the dice value
} }
@@ -123,6 +118,9 @@ const GameScreen = () => {
{/* Háttér */} {/* Háttér */}
<div className="absolute w-full h-full opacity-10 pointer-events-none overflow-hidden"> <div className="absolute w-full h-full opacity-10 pointer-events-none overflow-hidden">
{[...Array(35)].map((_, i) => ( {[...Array(35)].map((_, i) => (
// Sajat pulse effect! => node_modules/tailwindcss/index.css:
// --animate-pulse8: pulse 6s cubic-bezier(0.4, 0.2, 0.6, 1) infinite;
<div <div
key={i} key={i}
className="absolute rounded-full bg-teal-600 animate-pulse8" className="absolute rounded-full bg-teal-600 animate-pulse8"
@@ -189,7 +187,7 @@ const GameScreen = () => {
{sortedPlayers.map((player, index) => ( {sortedPlayers.map((player, index) => (
<div <div
key={player.id} key={player.id}
className="flex items-center mb-3 p-2 bg-gray-900 rounded-lg hover:bg-gray-700 transition-colors" className="flex items-center mb-3 p-2 bg-gray-900 rounded-lg hover:bg-gray-800 transition-colors"
> >
<div <div
className={`w-8 h-8 ${player.color} rounded-full mr-3 flex items-center justify-center text-white text-sm font-bold shadow-md`} className={`w-8 h-8 ${player.color} rounded-full mr-3 flex items-center justify-center text-white text-sm font-bold shadow-md`}
@@ -224,31 +222,8 @@ const GameScreen = () => {
{/* Dice Container */} {/* Dice Container */}
<div className="bg-gray-800 rounded-xl p-4 shadow-lg border border-teal-700 text-center"> <div className="bg-gray-800 rounded-xl p-4 shadow-lg border border-teal-700 text-center">
<h2 className="text-xl font-semibold mb-3 text-teal-300">Dobókocka</h2> <h2 className="text-xl font-semibold mb-3 text-teal-300">Dobókocka</h2>
<p className="text-gray-300 text-sm mb-4"> <p className="text-gray-300 text-sm mb-4">Kattints a kockára dobáshoz!</p>
Kattints a kockára dobáshoz vagy válassz egy számot az alábbiból! <Dice onRoll={handleDiceRoll} />
</p>
{/* Dropdown to select number 1-6 (triggers animated roll to that number) */}
<div className="mb-3">
<select
value={selectedDice ?? ""}
onChange={(e) => {
const v = e.target.value ? Number(e.target.value) : null
setSelectedDice(v)
}}
className="bg-gray-900 text-gray-200 rounded-md p-2 border border-gray-700"
>
<option value="">Válassz számot...</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
</div>
<Dice onRoll={handleDiceRoll} selectedValue={selectedDice} />
</div> </div>
</div> </div>
</div> </div>
@@ -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 // src/pages/Home/Home.jsx
// Régi PlayMenu-s oldal, "Home" néven // Régi PlayMenu-s oldal, "Home" néven
import { useEffect } from "react" import { useState } from "react"
import useRequireAuth from "../../hooks/useRequireAuth" import Navbar from "../../components/Navbar/Navbar.jsx"
import Navbar from "../../components/Navbar/Navbar"
import Footer from "../../components/Footer/Footer.jsx" import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx" import Background from "../../assets/backgrounds/Background.jsx"
import PlayMenu from "../../components/Landingpage/PlayMenu.jsx" import PlayMenu from "../../components/Landingpage/PlayMenu.jsx"
export default function Home() { export default function Home() {
// a hook inicializálja a user-t a localStorage-ból és visszaadja a state-et + settert
const [user, setUser] = useRequireAuth({ redirect: false }) // no redirect on unauthenticated visitors
// Dummy callbackok és user példa // Dummy callbackok és user példa
const handleJoinGame = (code) => { const handleJoinGame = (code) => {
alert(`Csatlakozás játékhoz: ${code}`) alert(`Csatlakozás játékhoz: ${code}`)
@@ -19,9 +15,7 @@ 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 userObj = { name: user } const user = { name: "Teszt Elek" }
// 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">
@@ -31,13 +25,8 @@ export default function Home() {
<div className="fixed top-0 left-0 right-0 z-30"> <div className="fixed top-0 left-0 right-0 z-30">
<Navbar /> <Navbar />
</div> </div>
<main className="flex-1 min-h-[calc(100vh-64px)] flex mt-[64px] flex-col items-center justify-center"> <main className="flex-1 flex flex-col items-center justify-start py-15 min-h-0 mt-[64px]">
<PlayMenu <PlayMenu onJoinGame={handleJoinGame} onCreateGame={handleCreateGame} user={user} />
onJoinGame={handleJoinGame}
onCreateGame={handleCreateGame}
user={userObj}
setUser={setUser}
/>
{/* Ide jöhetnek további szekciók, ha szeretnél még tartalmat */} {/* Ide jöhetnek további szekciók, ha szeretnél még tartalmat */}
</main> </main>
<Footer /> <Footer />
@@ -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() {
const navigate = useNavigate(); // Navigációs callbackok
const handleNavigateToPlay = () => { const handleNavigateToPlay = () => {
navigate("/login"); // Itt lehet navigálni a játék oldalra
}; alert("Navigáció a játék oldalra")
}
const handleNavigateToAuth = () => { const handleNavigateToAuth = () => {
navigate("/register"); // Itt lehet navigálni a bejelentkezés oldalra
}; 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>
); )
} }
@@ -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 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 */}
@@ -27,8 +23,9 @@ 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">Áttekintés a legutóbbi játékokról és statisztikákról</p> <p className="text-gray-300 mt-2">
{username && <p className="text-sm text-gray-400 mt-1">Bejelentkezett: {username}</p>} Áttekintés a legutóbbi játékokról és statisztikákról
</p>
</div> </div>
{/* Statisztikai kártyák */} {/* Statisztikai kártyák */}
+85 -117
View File
@@ -1,103 +1,74 @@
import React, { useState, useEffect, useRef } from "react" import React, { useState, useEffect, useRef } from 'react';
import { dotPositions } from "./diceDotPositions"
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 = [ const diceFaces = [
[<div key="center" className="dice-dot" style={dotPositions.center}></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" style={dotPositions.topLeft}></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="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></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="top-left" className="dice-dot" style={dotPositions.topLeft}></div>, <div key="center" className="dice-dot"></div>,
<div key="center" className="dice-dot" style={dotPositions.center}></div>, <div key="bottom-left" className="dice-dot"></div>, <div key="bottom-right" className="dice-dot"></div>],
<div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></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>]
<div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>, ];
<div key="top-right" className="dice-dot" style={dotPositions.topRight}></div>,
<div key="bottom-left" className="dice-dot" style={dotPositions.bottomLeft}></div>,
<div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
],
[
<div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>,
<div key="top-right" className="dice-dot" style={dotPositions.topRight}></div>,
<div key="center" className="dice-dot" style={dotPositions.center}></div>,
<div key="bottom-left" className="dice-dot" style={dotPositions.bottomLeft}></div>,
<div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
],
[
<div key="top-left" className="dice-dot" style={dotPositions.topLeft}></div>,
<div key="top-right" className="dice-dot" style={dotPositions.topRight}></div>,
<div key="middle-left" className="dice-dot" style={dotPositions.middleLeft}></div>,
<div key="middle-right" className="dice-dot" style={dotPositions.middleRight}></div>,
<div key="bottom-left" className="dice-dot" style={dotPositions.bottomLeft}></div>,
<div key="bottom-right" className="dice-dot" style={dotPositions.bottomRight}></div>,
],
]
useEffect(() => { useEffect(() => {
return () => { return () => {
if (animationRef.current) cancelAnimationFrame(animationRef.current) if (animationRef.current) cancelAnimationFrame(animationRef.current);
if (rollTimeoutRef.current) clearTimeout(rollTimeoutRef.current) if (rollTimeoutRef.current) clearTimeout(rollTimeoutRef.current);
} };
}, []) }, []);
// Helper that starts the rolling animation and finishes with targetValue if provided const rollDice = () => {
const startRoll = (targetValue = null) => { if (isRolling) return;
if (isRolling) return
setIsRolling(true);
setIsRolling(true)
let duration = 0;
let duration = 0 const rollInterval = 80; // ms between dice face changes
const rollInterval = 80 // ms between dice face changes const maxDuration = 1500; // total animation time
const maxDuration = 1500 // total animation time
const rollAnimation = () => { const rollAnimation = () => {
const randomValue = Math.floor(Math.random() * 6) + 1 const randomValue = Math.floor(Math.random() * 6) + 1;
setDiceValue(randomValue) setDiceValue(randomValue);
duration += rollInterval duration += rollInterval;
if (duration < maxDuration) { if (duration < maxDuration) {
// Speed effect: slow down towards the end // Speed effect: slow down towards the end
const nextInterval = rollInterval * (1 + (duration / maxDuration) * 2) const nextInterval = rollInterval * (1 + (duration / maxDuration) * 2);
rollTimeoutRef.current = setTimeout(() => { rollTimeoutRef.current = setTimeout(() => {
animationRef.current = requestAnimationFrame(rollAnimation) animationRef.current = requestAnimationFrame(rollAnimation);
}, nextInterval) }, nextInterval);
} else { } else {
// Final roll (use targetValue if provided) // Final roll
const finalValue = targetValue != null ? Number(targetValue) : Math.floor(Math.random() * 6) + 1 const finalValue = Math.floor(Math.random() * 6) + 1;
setDiceValue(finalValue) setDiceValue(finalValue);
setIsRolling(false) setIsRolling(false);
if (onRoll) onRoll(finalValue) if (onRoll) onRoll(finalValue);
} }
} };
animationRef.current = requestAnimationFrame(rollAnimation) animationRef.current = requestAnimationFrame(rollAnimation);
} };
// Click to roll randomly
const rollDice = () => {
startRoll(null)
}
// If parent provides a selectedValue, animate to that value
useEffect(() => {
if (selectedValue != null) {
startRoll(Number(selectedValue))
}
}, [selectedValue])
return ( return (
<div className={`dice-container ${isRolling ? "rolling" : ""}`} onClick={rollDice}> <div
<div className="dice">{diceFaces[diceValue - 1]}</div> className={`dice-container ${isRolling ? 'rolling' : ''}`}
onClick={rollDice}
>
<div className="dice">
{diceFaces[diceValue - 1]}
</div>
<style jsx>{` <style jsx>{`
.dice-container { .dice-container {
width: 80px; width: 80px;
@@ -107,15 +78,15 @@ const Dice = ({ onRoll, selectedValue }) => {
margin: 0 auto; margin: 0 auto;
transition: transform 0.2s; transition: transform 0.2s;
} }
.dice-container:hover { .dice-container:hover {
transform: scale(1.05); transform: scale(1.05);
} }
.dice-container:active { .dice-container:active {
transform: scale(0.95); transform: scale(0.95);
} }
.dice { .dice {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -127,45 +98,42 @@ const Dice = ({ onRoll, selectedValue }) => {
justify-content: center; justify-content: center;
background-color: #e63946; background-color: #e63946;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); box-shadow: 0 0 10px rgba(0,0,0,0.3);
padding: 8px; padding: 8px;
} }
.rolling .dice { .rolling .dice {
animation: roll 0.5s infinite linear; animation: roll 0.5s infinite linear;
} }
@keyframes roll { @keyframes roll {
0% { 0% { transform: rotateX(0deg) rotateY(0deg); }
transform: rotateX(0deg) rotateY(0deg); 25% { transform: rotateX(90deg) rotateY(45deg); }
} 50% { transform: rotateX(180deg) rotateY(90deg); }
25% { 75% { transform: rotateX(270deg) rotateY(135deg); }
transform: rotateX(90deg) rotateY(45deg); 100% { transform: rotateX(360deg) rotateY(180deg); }
}
50% {
transform: rotateX(180deg) rotateY(90deg);
}
75% {
transform: rotateX(270deg) rotateY(135deg);
}
100% {
transform: rotateX(360deg) rotateY(180deg);
}
} }
.dice-dot { .dice-dot {
width: 14px; width: 14px;
height: 14px; height: 14px;
border-radius: 50%; border-radius: 50%;
background-color: white; background-color: white;
position: absolute; position: absolute;
box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.3); 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> `}</style>
</div> </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%)" },
}