Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/components/flags/FlagsApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class FlagsApi extends React.Component {
gameEnded = false;
answerLocked = false;

state = {
loading: true
};

async handleClick(action) {
if (action === 'api') {
const res = await axios.get(api.url+'/api/flags/test');
Expand Down Expand Up @@ -115,8 +119,12 @@ class FlagsApi extends React.Component {
async startGame() {
this.gameEnded = false;
this.answerLocked = false;
this.setState({ loading: true });
this.stopTimer();
await this.handleClick('api').then(() => this.startTimer()).then(() => this.prepareStat());
await this.handleClick('api')
.then(() => this.startTimer())
.then(() => this.prepareStat())
.then(() => this.setState({ loading: false }));
}

async showFlags() {
Expand Down Expand Up @@ -249,6 +257,10 @@ class FlagsApi extends React.Component {
}

render() {
if (this.state.loading) {
return <div className="p-5 text-center">Fetching question...</div>;
}

return (
<div>
{/*// <Container style={{ 'display' : 'flex', 'justify-content' : 'center', width: '600px'}}>*/}
Expand Down Expand Up @@ -301,7 +313,9 @@ class FlagsApi extends React.Component {
'margin' : '10px',
'margin-bottom' : '0px'
}}>
<span>{this.props.text}&nbsp;</span>
<span className={this.props.text.includes('RIGHT') ? 'feedback-correct' : this.props.text.includes('NO') ? 'feedback-incorrect' : ''}>
{this.props.text}&nbsp;
</span>
<span>Total time: {this.props.sessionTimer}</span>
</div>
<div style={{
Expand Down
8 changes: 5 additions & 3 deletions src/components/flags/Profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,14 @@ const Profile = () => {
style={{
position: 'absolute',
top: '10px',
right: '10px',
right: '15px',
background: 'none',
border: 'none',
fontSize: '1.5rem',
fontSize: '2rem',
fontWeight: 'bold',
cursor: 'pointer',
opacity: 0.5
opacity: 0.7,
lineHeight: 1
}}
aria-label="Close"
>
Expand Down
42 changes: 42 additions & 0 deletions src/components/flags/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,45 @@ td:nth-of-type(2){
font-size: 15px;
font-weight: bold;
}

/* Feedback animations */
@keyframes pop-in {
0% {
transform: scale(0.5);
opacity: 0;
}
50% {
transform: scale(1.3);
}
100% {
transform: scale(1);
opacity: 1;
}
}

@keyframes shake {
0%, 100% {
transform: translateX(0);
}
20%, 60% {
transform: translateX(-5px);
}
40%, 80% {
transform: translateX(5px);
}
}

.feedback-correct {
display: inline-block;
color: #28a745;
font-weight: bold;
font-size: 1.1em;
animation: pop-in 0.4s ease-out;
}

.feedback-incorrect {
display: inline-block;
color: #dc3545;
font-weight: bold;
animation: shake 0.4s ease-out;
}
153 changes: 76 additions & 77 deletions src/components/home/Home.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, {useEffect, useState} from 'react';
import "./styles.css";
import axios from "../../config/Axios";
import Card from "react-bootstrap/Card";
import api from "../../config/Api";
import Container from "react-bootstrap/Container";
import Col from "react-bootstrap/Col";
Expand All @@ -23,15 +22,21 @@ const Home = () => {
if (!token) return false;

const expiresAt = localStorage.getItem('tokenExpiresAt');
if (expiresAt) {
// Check if token is expired (with 60s buffer)
if (Date.now() >= parseInt(expiresAt) - 60000) {
// Token expired, clear it
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('tokenExpiresAt');
return false;
}

// Token exists but no expiration - legacy token, treat as expired
if (!expiresAt) {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
return false;
}

// Check if token is expired (with 60s buffer)
if (Date.now() >= parseInt(expiresAt) - 60000) {
// Token expired, clear it
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('tokenExpiresAt');
return false;
}

return true;
Expand Down Expand Up @@ -67,9 +72,9 @@ const Home = () => {
{isLoggedIn ? (
<>
<Button
variant="outline-secondary"
size="lg"
onClick={handlePlay}
className="btn-cta"
style={{ textTransform: 'uppercase' }}
>
Play
Expand All @@ -86,10 +91,10 @@ const Home = () => {
</>
) : (
<Button
variant="outline-secondary"
size="lg"
onClick={oauthLogin}
disabled={isLoading}
className="btn-cta"
style={{ textTransform: 'uppercase' }}
>
{isLoading ? 'Logging in...' : 'Login to Play'}
Expand All @@ -100,72 +105,66 @@ const Home = () => {
</Row>

{/* Leaderboard Section */}
<Row>
<Col xs={12} lg={10} className="mx-auto">
<Card>
<Card.Header as="h3" className="text-center">
High Scores
</Card.Header>
<Card.Body>
{loading ? (
<div className="text-center py-5">Loading leaderboard...</div>
) : leaderboard.length === 0 ? (
<p className="text-center text-muted">No scores yet. Be the first to play!</p>
) : (
<Table striped hover responsive className="leaderboard-table">
<thead>
<tr>
<th className="text-center">Rank</th>
<th>Player</th>
<th className="text-center">Top Score</th>
<th className="text-center">Best Time</th>
<th className="text-center">Games</th>
<th className="text-center">Time Total</th>
</tr>
</thead>
<tbody>
{leaderboard.map((player, index) => {
const rank = index + 1;
let medal = null;
if (rank === 1) medal = <span role="img" aria-label="gold medal">🥇 </span>;
else if (rank === 2) medal = <span role="img" aria-label="silver medal">🥈 </span>;
else if (rank === 3) medal = <span role="img" aria-label="bronze medal">🥉 </span>;
{loading ? (
<div className="text-center py-5">Loading leaderboard...</div>
) : leaderboard.length === 0 ? (
<p className="text-center text-muted">No scores yet. Be the first to play!</p>
) : (
<Table striped hover responsive className="leaderboard-table" style={{ width: '100%' }}>
<thead>
<tr>
<th colSpan="6" className="text-center" style={{ fontSize: '1.5rem', padding: '1rem' }}>
High Scores
</th>
</tr>
<tr>
<th className="text-center">Rank</th>
<th>Player</th>
<th className="text-center">Top Score</th>
<th className="text-center">Best Time</th>
<th className="text-center">Games</th>
<th className="text-center">Time Total</th>
</tr>
</thead>
<tbody>
{leaderboard.map((player, index) => {
const rank = index + 1;
let medal = null;
if (rank === 1) medal = <span role="img" aria-label="gold medal">🥇 </span>;
else if (rank === 2) medal = <span role="img" aria-label="silver medal">🥈 </span>;
else if (rank === 3) medal = <span role="img" aria-label="bronze medal">🥉 </span>;

return (
<tr key={index}>
<td className="text-center rank-cell">{medal}{rank}</td>
<td>{player.firstName}</td>
<td className="text-center score-cell">{player.highScore}</td>
<td className="text-center time-cell">
{(() => {
const totalSeconds = player.bestTime;
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const pad = (n) => String(n).padStart(2, '0');
return `${pad(minutes)}:${pad(seconds)}`;
})()}
</td>
<td className="text-center">{player.gamesTotal}</td>
<td className="text-center time-cell">
{(() => {
const totalSeconds = player.timeTotal;
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const pad = (n) => String(n).padStart(2, '0');
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
})()}
</td>
</tr>
);
})}
</tbody>
</Table>
)}
</Card.Body>
</Card>
</Col>
</Row>
return (
<tr key={index}>
<td className="text-center rank-cell">{medal}{rank}</td>
<td>{player.firstName}</td>
<td className="text-center score-cell">{player.highScore}</td>
<td className="text-center time-cell">
{(() => {
const totalSeconds = player.bestTime;
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const pad = (n) => String(n).padStart(2, '0');
return `${pad(minutes)}:${pad(seconds)}`;
})()}
</td>
<td className="text-center">{player.gamesTotal}</td>
<td className="text-center time-cell">
{(() => {
const totalSeconds = player.timeTotal;
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const pad = (n) => String(n).padStart(2, '0');
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
})()}
</td>
</tr>
);
})}
</tbody>
</Table>
)}
</Container>

{/* Footer */}
Expand Down
41 changes: 41 additions & 0 deletions src/components/home/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,47 @@
}
}

/* Call to Action Button */
.btn-cta {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
border: none;
color: white;
font-weight: 600;
letter-spacing: 1px;
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.4);
transition: all 0.3s ease;
}

.btn-cta:hover {
background: linear-gradient(135deg, #218838 0%, #1db88a 100%);
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.5);
transform: translateY(-2px);
color: white;
}

.btn-cta:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(40, 167, 69, 0.4);
}

/* Subtle pulse animation for CTA */
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.4);
}
50% {
box-shadow: 0 4px 25px rgba(40, 167, 69, 0.6);
}
}

.btn-cta {
animation: pulse-glow 2s ease-in-out infinite;
}

.btn-cta:hover {
animation: none;
}

/* Footer */
.app-footer {
background-color: #1a1a2e;
Expand Down