From 22b50499e2c7b965d795252e0ebdf32e8d75eff7 Mon Sep 17 00:00:00 2001 From: Esteban Gondim Date: Mon, 10 Aug 2026 13:59:40 +0200 Subject: [PATCH 1/8] Fix first click on game mode buttons being swallowed after a refresh The F5 guard read the navigation type at GameRoute mount, but that entry stays "reload" for the document's whole lifetime, so the first click into any game mode after any refresh got bounced home. Decide once at bundle load, against the URL actually reloaded, before BrowserRouter reads it. Also unblock the build: PartyGame had duplicate setIsInGame/joinSentRef declarations from the #56/#57 merge, and the prod compose was missing the SELinux ,z relabel on the cert mounts that dev already had. --- docker-compose.yml | 8 +++++-- frontend/src/app/components/PartyGame.tsx | 29 ----------------------- frontend/src/app/hooks/reload.ts | 20 ++++++++++++---- frontend/src/app/routes/GameRoute.tsx | 10 -------- frontend/src/main.tsx | 4 ++++ 5 files changed, 25 insertions(+), 46 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 752508f..f92c328 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,7 +42,9 @@ services: postgres: condition: service_healthy volumes: - - ./nginx/certs:/etc/nginx/certs:ro + # ',z' = relabel for SELinux (Fedora/RHEL run it enforcing). Shared + # lowercase 'z', not 'Z': backend and nginx both mount this directory. + - ./nginx/certs:/etc/nginx/certs:ro,z networks: - transcend_net restart: unless-stopped @@ -76,7 +78,9 @@ services: ports: - "${HTTPS_PORT:-443}:443" volumes: - - ./nginx/certs:/etc/nginx/certs:ro + # ',z' = relabel for SELinux (Fedora/RHEL run it enforcing). Shared + # lowercase 'z', not 'Z': backend and nginx both mount this directory. + - ./nginx/certs:/etc/nginx/certs:ro,z depends_on: - frontend - backend diff --git a/frontend/src/app/components/PartyGame.tsx b/frontend/src/app/components/PartyGame.tsx index c0e456e..3e9b853 100644 --- a/frontend/src/app/components/PartyGame.tsx +++ b/frontend/src/app/components/PartyGame.tsx @@ -24,25 +24,6 @@ export default function QuizPage() const { setIsInGame } = useGame(); const [revealedAnswer, setRevealedAnswer] = useState(null); const [correctAnswer, setCorrectAnswer] = useState(null); - const { setIsInGame } = useGame(); - const joinSentRef = useRef(false); - useEffect(() => - { - if (mode === "tournament" && location.state) - { - initializeMatch(location.state); - } - }, []); - - useEffect(() => - { - setIsInGame(true); - - return () => - { - setIsInGame(false); - }; - }, []); const [questions, setQuestions] = useState([]); const [gameStarted, setGameStarted] = useState(false); @@ -77,16 +58,6 @@ export default function QuizPage() const isPlayer1Ref = useRef(false); const bracketTimeoutRef = useRef | null>(null); - useEffect(() => -{ - console.log("PARTY GAME MOUNT"); - - return () => - { - console.log("PARTY GAME UNMOUNT"); - }; -}, []); - useEffect(() => { if (mode === "tournament" && !location.state) diff --git a/frontend/src/app/hooks/reload.ts b/frontend/src/app/hooks/reload.ts index ee3d133..2c98cf9 100644 --- a/frontend/src/app/hooks/reload.ts +++ b/frontend/src/app/hooks/reload.ts @@ -1,10 +1,20 @@ -let handled = false; +const GAME_PATH = /^\/game\//; -export function consumeReload(): boolean { - if (handled) return false; - handled = true; +// +// Après un F5 sur /game/:mode la partie (socket, state) est perdue : +// on renvoie le joueur à l'accueil. La décision est prise une seule fois, +// au chargement du bundle, sur l'URL réellement rechargée — le type de +// navigation reste "reload" pour toute la durée du document, donc le +// tester plus tard (au montage de GameRoute) rejetait le premier clic +// sur un mode de jeu après n'importe quel rafraîchissement. +// +export function redirectHomeOnGameReload(): void { const entry = performance.getEntriesByType("navigation")[0] as | PerformanceNavigationTiming | undefined; - return entry?.type === "reload"; + + if (entry?.type !== "reload") return; + if (!GAME_PATH.test(window.location.pathname)) return; + + window.history.replaceState(null, "", "/"); } diff --git a/frontend/src/app/routes/GameRoute.tsx b/frontend/src/app/routes/GameRoute.tsx index e8f2ba8..12bffd6 100644 --- a/frontend/src/app/routes/GameRoute.tsx +++ b/frontend/src/app/routes/GameRoute.tsx @@ -1,6 +1,4 @@ import { Navigate, useNavigate, useParams } from "react-router-dom"; -import { useMemo } from "react"; -import { consumeReload } from "../hooks/reload.ts"; import GamePage from "../components/GamePage"; import PartyGame from "../components/PartyGame"; @@ -14,19 +12,11 @@ function isGameMode(value: string | undefined): value is GameMode { export default function GameRoute() { const { mode } = useParams<{ mode: string }>(); const navigate = useNavigate(); - const isReload = useMemo(() => consumeReload(), []); - if (!isGameMode(mode)) { return ; } - // - //To check if it's f5 or not - // - - if (isReload) return ; - const onBack = () => navigate("/"); if (mode === "tournament") { diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 3725e12..8f33678 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,8 +3,12 @@ import ReactDOM from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import App from './app/App.tsx' import { GameProvider } from "./app/context/GameContext"; +import { redirectHomeOnGameReload } from "./app/hooks/reload.ts"; import './styles/index.css' +// Doit tourner avant que BrowserRouter ne lise l'URL courante. +redirectHomeOnGameReload(); + ReactDOM.createRoot(document.getElementById('root')!).render( From 8e560e57d42c779ceaafd35fc4b904615084aeb3 Mon Sep 17 00:00:00 2001 From: elsikira Date: Mon, 10 Aug 2026 19:36:49 +0200 Subject: [PATCH 2/8] fix : started to translate Leaderboard, and also started fixing the leaderboard bug with profile pic --- frontend/src/app/components/Layout.tsx | 2 +- frontend/src/app/pages/Leaderboard.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/Layout.tsx b/frontend/src/app/components/Layout.tsx index a1a2a68..ed9ac04 100644 --- a/frontend/src/app/components/Layout.tsx +++ b/frontend/src/app/components/Layout.tsx @@ -124,7 +124,7 @@ return ( className="flex items-center gap-2 px-4 py-2 rounded-full bg-white/60 hover:bg-white text-gray-700 hover:text-amber-600 border border-gray-200 transition-all shadow-sm hover:shadow-md" > - Classement + Ranking {/* Bouton Profil (avec avatar dynamique) */} diff --git a/frontend/src/app/pages/Leaderboard.tsx b/frontend/src/app/pages/Leaderboard.tsx index d739830..736c755 100644 --- a/frontend/src/app/pages/Leaderboard.tsx +++ b/frontend/src/app/pages/Leaderboard.tsx @@ -80,9 +80,9 @@ export default function LeaderboardPage({ userId, onBack }: LeaderboardPageProps

- Classement + Ranking

-

Les meilleurs joueurs, classés par XP

+

Best players, ranked by XP

From 232a1952fe4fbc5f916affcaa25d9178d807fe2a Mon Sep 17 00:00:00 2001 From: elsikira Date: Mon, 10 Aug 2026 19:40:58 +0200 Subject: [PATCH 3/8] fix: profile pic fixed --- frontend/src/app/pages/Leaderboard.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/pages/Leaderboard.tsx b/frontend/src/app/pages/Leaderboard.tsx index 736c755..6eb3ded 100644 --- a/frontend/src/app/pages/Leaderboard.tsx +++ b/frontend/src/app/pages/Leaderboard.tsx @@ -55,6 +55,8 @@ export default function LeaderboardPage({ userId, onBack }: LeaderboardPageProps }, [userId]); const isMe = (entry: LeaderboardEntry) => entry.userId === userId; + const isImageUrl = (avatar: string | null): boolean => + !!avatar && /^(https?:\/\/|\/)/.test(avatar); return (
@@ -117,10 +119,19 @@ export default function LeaderboardPage({ userId, onBack }: LeaderboardPageProps )}
-
- {entry.avatar || "😊"} -
- +
+ {isImageUrl(entry.avatar) ? ( + {entry.username} { e.currentTarget.style.display = "none"; }} + /> + ) : ( + entry.avatar || "😊" + )} +

{entry.username} From b9bb537dc33e32b6fb3c83bda3fc5e560b8e5672 Mon Sep 17 00:00:00 2001 From: elsikira Date: Tue, 11 Aug 2026 15:07:51 +0200 Subject: [PATCH 4/8] fix: translated the whole site; ranking pdp is fixed --- frontend/src/app/components/Layout.tsx | 4 +-- frontend/src/app/components/LoginPage.tsx | 32 ++++++++++----------- frontend/src/app/components/ProfilePage.tsx | 8 +++--- frontend/src/app/pages/Leaderboard.tsx | 8 +++--- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/frontend/src/app/components/Layout.tsx b/frontend/src/app/components/Layout.tsx index ed9ac04..07c84e7 100644 --- a/frontend/src/app/components/Layout.tsx +++ b/frontend/src/app/components/Layout.tsx @@ -176,7 +176,7 @@ return (

- Chat Global + Global Chat {onlineCount} online @@ -200,7 +200,7 @@ return ( {/* Messages (Zone de texte) */}
{Array.isArray(messages) && messages.length === 0 ? ( -

Aucun message pour l'instant...

+

No message for now...

) : ( Array.isArray(messages) && messages.map((msg, index) => { const authorName = msg.author?.username || msg.user || "Inconnu"; diff --git a/frontend/src/app/components/LoginPage.tsx b/frontend/src/app/components/LoginPage.tsx index efdd970..36d6771 100644 --- a/frontend/src/app/components/LoginPage.tsx +++ b/frontend/src/app/components/LoginPage.tsx @@ -43,7 +43,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps) if (response.ok) { if (isSignUp) { setIsSignUp(false); - setErrorMsg("Compte créé avec succès ! Connectez-vous."); + setErrorMsg("Account created! Please log in."); return; } @@ -55,11 +55,11 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps) console.log("Connecté avec succès !", data); if (onLogin) onLogin(); } else { - setErrorMsg(data.message || "Erreur lors de la connexion"); + setErrorMsg(data.message || "Error while connecting"); } } catch (error) { console.error("Network Error:", error); - setErrorMsg("Impossible de contacter le serveur NestJS."); + setErrorMsg("Impossible to reach NestJS server."); } }; @@ -83,8 +83,8 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)

{is2FARequired - ? "Vérification en deux étapes" - : (isSignUp ? "Créez votre compte pour commencer" : "Bienvenue ! Connectez-vous pour continuer")} + ? "Two steps verification" + : (isSignUp ? "Create your account to start" : "Welcome ! Log in to continue")}

@@ -103,7 +103,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps) {is2FARequired ? (
- +
{isSignUp && (
- +
setEmail(e.target.value)} - placeholder="votre@email.com" + placeholder="your@email.com" className="w-full pl-12 pr-4 py-3 rounded-xl bg-gray-50 border-2 border-gray-100 focus:border-purple-400 focus:outline-none focus:ring-2 focus:ring-purple-200 transition-all" required /> @@ -152,7 +152,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)
- +
)} @@ -180,7 +180,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps) type="submit" className="w-full py-4 rounded-xl bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white font-bold shadow-lg hover:shadow-xl hover:from-indigo-600 hover:via-purple-600 hover:to-pink-600 transition-all transform hover:-translate-y-0.5 mt-2" > - {is2FARequired ? "Vérifier le code" : (isSignUp ? "Créer mon compte" : "Se connecter")} + {is2FARequired ? "Verify the code" : (isSignUp ? "Create account" : "Login in")} @@ -191,7 +191,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)
- ou continuer avec + or continue with
@@ -201,14 +201,14 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps)
42
- Se connecter avec l'intra 42 + Log in with 42 intra - Se connecter avec GitHub + Log in with GitHub
@@ -221,9 +221,9 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps) className="text-gray-500 hover:text-gray-700 font-medium text-sm transition-colors" > {isSignUp ? ( - <>Vous avez déjà un compte ? Se connecter + <>Already have an account ? Se connecter ) : ( - <>Pas encore de compte ? S'inscrire + <>No account yet ? S'inscrire )}
diff --git a/frontend/src/app/components/ProfilePage.tsx b/frontend/src/app/components/ProfilePage.tsx index 3e2a2b0..6e944d6 100644 --- a/frontend/src/app/components/ProfilePage.tsx +++ b/frontend/src/app/components/ProfilePage.tsx @@ -528,7 +528,7 @@ export default function ProfilePage({

- Statistiques globales + Global Stats

@@ -577,7 +577,7 @@ export default function ProfilePage({
))} {(stats?.categories ?? []).length === 0 && ( -

Aucune donnée pour l'instant — joue une partie !

+

No data for now, run a game !

)}
@@ -586,7 +586,7 @@ export default function ProfilePage({

- Badges + Achievements

{(stats?.badges ?? []).map((badge) => ( @@ -671,7 +671,7 @@ export default function ProfilePage({

- Historique des parties + Game History

{historyLoading ? ( diff --git a/frontend/src/app/pages/Leaderboard.tsx b/frontend/src/app/pages/Leaderboard.tsx index 6eb3ded..e17361c 100644 --- a/frontend/src/app/pages/Leaderboard.tsx +++ b/frontend/src/app/pages/Leaderboard.tsx @@ -67,12 +67,12 @@ export default function LeaderboardPage({ userId, onBack }: LeaderboardPageProps className="flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors" > - Retour + Back {myRank !== null && (
- Ton rang : #{myRank} + Your rank: #{myRank}
)}
@@ -135,10 +135,10 @@ export default function LeaderboardPage({ userId, onBack }: LeaderboardPageProps

{entry.username} - {isMe(entry) && (toi)} + {isMe(entry) && (you)}

- Niveau {entry.level} · {entry.gamesPlayed} partie(s) · {entry.wins} victoire(s) + Level {entry.level} · {entry.gamesPlayed} games · {entry.wins} wins

From b7677ba72f8e04bb8af6cf8d3263eb91bf95e6ad Mon Sep 17 00:00:00 2001 From: elsikira Date: Tue, 11 Aug 2026 17:02:45 +0200 Subject: [PATCH 5/8] fix(frontend): render OAuth avatar URLs as images instead of raw text Avatars were rendered as plain text, which worked for emoji avatars from email/password signup but printed the raw URL for OAuth users, breaking the layout on the leaderboard, chat and profile pages. - add reusable Avatar component that detects URLs vs emoji - use object-cover + overflow-hidden so remote images stay in the circle - set referrerPolicy=no-referrer to avoid 403s on Google-hosted avatars - fall back to the default emoji when an image fails to load --- frontend/src/app/components/Avatar.tsx | 36 +++ frontend/src/app/components/Layout.tsx | 19 +- frontend/src/app/components/LoginPage.tsx | 8 +- frontend/src/app/components/ProfilePage.tsx | 16 +- frontend/src/app/pages/Leaderboard.tsx | 244 ++++++++++---------- 5 files changed, 171 insertions(+), 152 deletions(-) create mode 100644 frontend/src/app/components/Avatar.tsx diff --git a/frontend/src/app/components/Avatar.tsx b/frontend/src/app/components/Avatar.tsx new file mode 100644 index 0000000..03c8186 --- /dev/null +++ b/frontend/src/app/components/Avatar.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; + +const isImageUrl = (avatar?: string | null): boolean => +!!avatar && /^(https?:\/\/|\/)/.test(avatar); + +interface AvatarProps { + src?: string | null; + alt?: string; + className?: string; +} + +export default function Avatar({ + src, + alt = "avatar", + className = "w-10 h-10 text-xl", +}: AvatarProps) { + const [failed, setFailed] = useState(false); + const showImage = isImageUrl(src) && !failed; + return ( +
+ {showImage ? ( + {alt} setFailed(true)} + /> + ) : ( + {(!isImageUrl(src) && src) || "😊"} + )} +
+ ); +} diff --git a/frontend/src/app/components/Layout.tsx b/frontend/src/app/components/Layout.tsx index 07c84e7..6b45187 100644 --- a/frontend/src/app/components/Layout.tsx +++ b/frontend/src/app/components/Layout.tsx @@ -3,6 +3,7 @@ import { Outlet, useNavigate } from "react-router-dom"; import { useState, useEffect, useRef } from "react"; import { socket } from "../../socket/socket"; import { useGame } from "../context/GameContext"; +import Avatar from "./Avatar"; interface LayoutProps { username: string; @@ -133,13 +134,7 @@ return ( className="group flex items-center gap-3 px-4 py-2 rounded-full bg-gradient-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 transition-all shadow-md hover:shadow-lg" > {username} -
- {userAvatar ? ( - {username} - ) : ( - 😊 - )} -
+ {/* Bouton Déconnexion */} @@ -204,18 +199,14 @@ return ( ) : ( Array.isArray(messages) && messages.map((msg, index) => { const authorName = msg.author?.username || msg.user || "Inconnu"; - return (
{/* Avatar */} -
- {msg.avatar || "😊"} -
- -
+ +
{/* Pseudo Cliquable */} setNewMessage(e.target.value)} diff --git a/frontend/src/app/components/LoginPage.tsx b/frontend/src/app/components/LoginPage.tsx index 36d6771..96e642e 100644 --- a/frontend/src/app/components/LoginPage.tsx +++ b/frontend/src/app/components/LoginPage.tsx @@ -128,7 +128,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps) type="text" value={username} onChange={(e) => setUsername(e.target.value)} - placeholder="Votre pseudo" + placeholder="Your username" className="w-full pl-12 pr-4 py-3 rounded-xl bg-gray-50 border-2 border-gray-100 focus:border-purple-400 focus:outline-none focus:ring-2 focus:ring-purple-200 transition-all" required /> @@ -180,7 +180,7 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps) type="submit" className="w-full py-4 rounded-xl bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white font-bold shadow-lg hover:shadow-xl hover:from-indigo-600 hover:via-purple-600 hover:to-pink-600 transition-all transform hover:-translate-y-0.5 mt-2" > - {is2FARequired ? "Verify the code" : (isSignUp ? "Create account" : "Login in")} + {is2FARequired ? "Verify the code" : (isSignUp ? "Create account" : "Log in")} @@ -221,9 +221,9 @@ export default function LoginPage({ onLogin, force2FA = false }: LoginPageProps) className="text-gray-500 hover:text-gray-700 font-medium text-sm transition-colors" > {isSignUp ? ( - <>Already have an account ? Se connecter + <>Already have an account ? Log in ) : ( - <>No account yet ? S'inscrire + <>No account yet ? Sign in )}
diff --git a/frontend/src/app/components/ProfilePage.tsx b/frontend/src/app/components/ProfilePage.tsx index 6e944d6..cd94015 100644 --- a/frontend/src/app/components/ProfilePage.tsx +++ b/frontend/src/app/components/ProfilePage.tsx @@ -287,8 +287,8 @@ export default function ProfilePage({

{twoFAModal === "enable" - ? "Scannez ce QR Code avec Google Authenticator ou Authy." - : "Saisissez un code de votre application pour confirmer."} + ? "Scan the QR Code with Google Authenticator" + : "Write the code from your app to confirm"}

@@ -297,14 +297,14 @@ export default function ProfilePage({ {qrCodeUrl ? ( QR Code 2FA ) : ( -
Chargement...
+
Loading...
)}
)}
- + - Confirmer l'activation + Confirm activation ) : ( )}
@@ -675,10 +675,10 @@ export default function ProfilePage({ {historyLoading ? ( -

Chargement...

+

Loading...

) : matchHistory.length === 0 ? (

- Aucune partie 1v1 pour l'instant — lance-toi ! + No 1v1 game yet, launch a game! !

) : (
diff --git a/frontend/src/app/pages/Leaderboard.tsx b/frontend/src/app/pages/Leaderboard.tsx index e17361c..672fb06 100644 --- a/frontend/src/app/pages/Leaderboard.tsx +++ b/frontend/src/app/pages/Leaderboard.tsx @@ -24,136 +24,128 @@ const RANK_STYLES: Record = { }; export default function LeaderboardPage({ userId, onBack }: LeaderboardPageProps) { - const [entries, setEntries] = useState([]); - const [myRank, setMyRank] = useState(null); - const [loading, setLoading] = useState(true); + const [entries, setEntries] = useState([]); + const [myRank, setMyRank] = useState(null); + const [loading, setLoading] = useState(true); - useEffect(() => { - const load = async () => { - setLoading(true); - try { - const [leaderboardRes, rankRes] = await Promise.all([ - fetch(`/api/stats/leaderboard?limit=20`, { credentials: "include" }), - fetch(`/api/stats/${userId}/rank`, { credentials: "include" }), - ]); + useEffect(() => { + const load = async () => { + setLoading(true); + try { + const [leaderboardRes, rankRes] = await Promise.all([ + fetch(`/api/stats/leaderboard?limit=20`, { credentials: "include" }), + fetch(`/api/stats/${userId}/rank`, { credentials: "include" }), + ]); + if (leaderboardRes.ok) { + setEntries(await leaderboardRes.json()); + } + if (rankRes.ok) { + const data = await rankRes.json(); + setMyRank(data.rank); + } + } catch (err) { + console.error("Erreur lors du chargement du classement :", err); + } finally { + setLoading(false); + } + }; + load(); + }, [userId]); + const isMe = (entry: LeaderboardEntry) => entry.userId === userId; + const isImageUrl = (avatar: string | null): boolean => + !!avatar && /^(https?:\/\/|\/)/.test(avatar); + return ( +
+
+
+ + {myRank !== null && ( +
+ Your rank: #{myRank} +
+ )} +
+
+
+ +
+

+ Ranking +

+

Best players, ranked by XP

+
- if (leaderboardRes.ok) { - setEntries(await leaderboardRes.json()); - } - if (rankRes.ok) { - const data = await rankRes.json(); - setMyRank(data.rank); - } - } catch (err) { - console.error("Erreur lors du chargement du classement :", err); - } finally { - setLoading(false); - } - }; - - load(); - }, [userId]); - - const isMe = (entry: LeaderboardEntry) => entry.userId === userId; - const isImageUrl = (avatar: string | null): boolean => - !!avatar && /^(https?:\/\/|\/)/.test(avatar); - - return ( -
-
-
- - - {myRank !== null && ( -
- Your rank: #{myRank} -
- )} -
- -
-
- -
-

- Ranking -

-

Best players, ranked by XP

-
- -
- {loading ? ( -

Chargement...

- ) : entries.length === 0 ? ( -

Aucun joueur classé pour l'instant.

- ) : ( -
- {entries.map((entry) => ( -
-
+ {loading ? ( +

Loading...

+ ) : entries.length === 0 ? ( +

No players ranked yet.

+ ) : ( +
+ {entries.map((entry) => ( +
+
- {entry.rank <= 3 ? ( - entry.rank === 1 ? ( - + {entry.rank <= 3 ? ( + entry.rank === 1 ? ( + ) : ( - - ) - ) : ( - entry.rank - )} -
- -
- {isImageUrl(entry.avatar) ? ( - {entry.username} { e.currentTarget.style.display = "none"; }} - /> - ) : ( - entry.avatar || "😊" - )} -
-
-

- {entry.username} - {isMe(entry) && (you)} -

-

- Level {entry.level} · {entry.gamesPlayed} games · {entry.wins} wins -

-
- -
-

- {entry.xp} -

-

XP

-
-
- ))} -
- )} -
-
-
- ); + + ) + ) : ( + entry.rank + )} +
+
+ {isImageUrl(entry.avatar) ? ( + {entry.username} { e.currentTarget.style.display = "none"; }} + /> + ) : ( + entry.avatar || "😊" + )} +
+
+

+ {entry.username} + {isMe(entry) && (you)} +

+

+ Level {entry.level} · {entry.gamesPlayed} games · {entry.wins} wins +

+
+
+

+ {entry.xp} +

+

XP

+
+
+ ))} +
+ )} +
+
+
+ ); } From 83f3fa219fb0b30d83f9a8602347ec36aa31d49f Mon Sep 17 00:00:00 2001 From: elsikira Date: Tue, 11 Aug 2026 19:40:25 +0200 Subject: [PATCH 6/8] fix(profile): return avatar from /api/auth/me and load it on the profile page JwtStrategy already fetched the full user record but omitted from the object attached to req.user, so /api/auth/me never returned it. The navbar and profile page both fell back to the default emoji even for accounts with a valid OAuth picture. - add to AuthenticatedRequestUser and the strategy's return - fetch it in ProfilePage on mount for the current user --- backend/src/auth/jwt/jwt.strategy.ts | 2 ++ frontend/src/app/components/ProfilePage.tsx | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/backend/src/auth/jwt/jwt.strategy.ts b/backend/src/auth/jwt/jwt.strategy.ts index 7c74989..d11b349 100644 --- a/backend/src/auth/jwt/jwt.strategy.ts +++ b/backend/src/auth/jwt/jwt.strategy.ts @@ -29,6 +29,7 @@ export interface AuthenticatedRequestUser { * pending token is `isTwoFactorEnabled: true, tfa: 'pending'`. */ isTwoFactorEnabled: boolean; + avatar: string | null; } const cookieExtractor = (req: Request): string | null => { @@ -68,6 +69,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { username: user.username, tfa: payload.tfa, isTwoFactorEnabled: user.isTwoFactorEnabled, + avatar: user.avatar, }; } } diff --git a/frontend/src/app/components/ProfilePage.tsx b/frontend/src/app/components/ProfilePage.tsx index cd94015..063c065 100644 --- a/frontend/src/app/components/ProfilePage.tsx +++ b/frontend/src/app/components/ProfilePage.tsx @@ -5,6 +5,7 @@ import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } f import { socket } from "../../socket/socket"; import { statsSocket } from "../../socket/socket"; import { useParams } from "react-router-dom"; +import Avatar from "./Avatar"; interface ProfilePageProps { username: string; @@ -133,7 +134,13 @@ export default function ProfilePage({ useEffect(() => { fetchStats(); fetchMatchHistory(); - }, [displayUsername]); + if (isMyProfile) { + fetch(`/api/auth/me`, { credentials: "include" }) + .then((res) => (res.ok ? res.json() : null)) + .then((data) => { if (data?.avatar) setAvatar(data.avatar); }) + .catch(() => {}); + } + }, [displayUsername]); // Temps réel : écoute les mises à jour de stats poussées par le serveur useEffect(() => { @@ -354,9 +361,8 @@ export default function ProfilePage({
-
-
- {avatar} +
+
{isMyProfile && ( @@ -740,10 +746,10 @@ export default function ProfilePage({ }`} > {match.result === "win" - ? "Victoire" + ? "Victory" : match.result === "loss" - ? "Défaite" - : "Égalité"} + ? "Defeat" + : "Draw"}

@@ -752,6 +758,5 @@ export default function ProfilePage({ )}
-
); } From 3e972e078172c776a2a45e261525c96feed1fcaa Mon Sep 17 00:00:00 2001 From: elsikira Date: Tue, 11 Aug 2026 20:33:15 +0200 Subject: [PATCH 7/8] fix: finished translation --- frontend/src/app/components/ProfilePage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/ProfilePage.tsx b/frontend/src/app/components/ProfilePage.tsx index 063c065..a07ae5b 100644 --- a/frontend/src/app/components/ProfilePage.tsx +++ b/frontend/src/app/components/ProfilePage.tsx @@ -290,7 +290,7 @@ export default function ProfilePage({ {twoFAModal === "enable" ? : }

- {twoFAModal === "enable" ? "Activer le 2FA" : "Désactiver le 2FA"} + {twoFAModal === "enable" ? "Activate the 2FA" : "Deactivate the 2FA"}

{twoFAModal === "enable" @@ -425,7 +425,7 @@ export default function ProfilePage({ {/* Progression du niveau */}

- Niveau {stats?.level ?? 1} + Level {stats?.level ?? 1} {stats?.currentLevelXp ?? 0} / 100 XP
@@ -496,7 +496,7 @@ export default function ProfilePage({ onClick={fetchStats} className="px-5 py-2 rounded-xl bg-gray-900 hover:bg-gray-800 text-white font-semibold transition-all" > - Filtrer + Filter {(startDate || endDate) && (